Migrating to 0.15

0.15.0 opens a new compatibility key: it reads pins of 0.15.x only. A project pinned to 0.14.x is refused by name until it is upgraded, and listen-fire upgrade moves its pin once every file checks clean.

One idea runs through the release: a type says what the run will meet. A name is never a word the language reads as itself; coalesce answers the kinds its arguments hold; a key, a field or a built-in's answer that may be empty says so; and a collection in no order is never read as if it had one. 0.14 let each of these check clean, and the run failed or answered something arbitrary.

Contents #

1. How the pin moves #

2. A word an expression reads as itself is never a name #

true, false, null, and, or, not, in, contains, exists, if, then, else and end, in any letter case, are refused as a binding, a parameter, an alias or a function name (MOV_V5_HIDES_BUILTIN). 0.14 checked true = 5 clean, and every later true was the literal.

0.140.15
end = Date.addDays(start, 7)deadline = Date.addDays(start, 7)
(in) => …(item) => …

3. coalesce is typed as a ?? b #

coalesce answers the union of what each argument it reaches holds, as TypeScript types a ?? b. An argument that is always there is the last one reached. 0.14 typed arguments of different kinds as unknown, and every later check said nothing.

0.14, checked clean0.15
held = coalesce(i.Text, []) then map(held, …)refused: held is text or an empty list (MOV_COLLECTION_OP_NOT_A_COLLECTION)
coalesce(n, "n/a") + 1refused: a number or a text (MOV_ARITH_NON_NUMERIC); write coalesce(n, 0) + 1

A fallback of the value's own kind types as before: coalesce(tags, []) is a list of text.

4. Airtable and Google Sheets cells that can be empty #

Airtable leaves an empty cell out of a row. A field that can be empty is now typed T | null: every kind of text, a number, a date, a single select, a collaborator, a barcode, a formula, a rollup, AI text, the last-modified time and author. A Google Sheets table's every column, and a named or addressed cell's Value, may be absent too.

0.14, checked clean0.15
ops-[v:Vendors WHERE v.Spend > 0]->ops-[v:Vendors WHERE v.Spend != null && v.Spend > 0]->
write crm-[:Leads]-> { Name: r.Title }write crm-[:Leads]-> { Name: coalesce(r.Title, "Untitled") }, or Name ?: r.Title to leave it unset

These stay always there: a checkbox (an empty one reads false), a list field such as a multiple select, attachments or linked rows (an empty one reads []), an autonumber, a count, the created time and the creator. A write still gives a field a value.

5. A List.groupBy or List.keyBy key that may be absent #

The run fails at the first member whose key answers nothing, so such a key is now an error (MOV_DICT_KEY_MAY_BE_ABSENT; a warning in 0.14).

0.14, warned0.15
List.groupBy(rows, (r) => r.Owner)List.groupBy(rows, (r) => coalesce(r.Owner, "Unassigned"))

6. The records a block answers come in the walk's order #

A block over a walk with no ORDER BY answers in no order. first, [0], Text.join and reduce over the records it answers are refused (MOV_FOLD_NEEDS_ORDER), as they were over the values it answers. A list literal that spreads a collection in no order is in no order too: a spread keeps the order of what it splices in.

0.14, checked clean0.15
ps = c-[p:People]-> { return p } then first(ps)order the walk (c-[p:People ORDER BY p.Name]->), or only(…) where exactly one is meant
Text.join([...names], ", "), names from a walk with no ORDER BYText.join(sort(names), ", ")

7. Inside walk brackets a built-in is spelled as outside them #

A built-in has one spelling: Text.lower(c.Name), Text.trim(…), Number.round(…), coalesce(…), exists(…), inside a walk's WHERE and ORDER BY as in a function body. The walk words 0.14 read there are refused (MOV_V5_SPELLING), and listen-fire upgrade rewrites them: the predicate a run evaluates, and a host receives, is the same. The logic words stay: AND, OR, NOT, CONTAINS, IN inside the brackets.

0.140.15
ch-[m:Messages WHERE LOWER(m.Text) CONTAINS "urgent"]->ch-[m:Messages WHERE Text.lower(m.Text) CONTAINS "urgent"]->
crm-[c:Companies WHERE EXISTS(c-[:Team]->)]->crm-[c:Companies WHERE exists(c-[:Team]->)]->
ch-[m:Messages WHERE LENGTH(m.Tags) > 0]->, Tags a listch-[m:Messages WHERE count(m.Tags) > 0]->, by hand: count reads into a different predicate, so upgrade leaves it

8. A built-in that can answer nothing says so #

A built-in's answer is T | absent exactly when its run can answer nothing, as TypeScript types a function that can return undefined, and T otherwise. 0.14 typed these answers as always there while the run answered null, so a comparison or a write read the null without a word.

0.14, checked clean0.15
due = Date.addDays(Date.parse(m.Subject), 7) then due < todayrefused (MOV_ABSENT_REQUIRED): test the parsed date first, if (d == null) { fail("…") }
Description: Currency.codeFromFigure(m.Body) in a writerefused: Description ?: Currency.codeFromFigure(m.Body), or coalesce(…, "unknown")
Number.format(Currency.numberFromFigure(m.Body), "compact")refused: coalesce(Currency.numberFromFigure(m.Body), 0), or test it first

A date or datetime that is there still gives an answer that is there: Date.addDays(Date.today("UTC"), 7) and Date.format(m.Date, "D MMMM") are unchanged. listen-fire upgrade adds no fallback, since the fallback is the author's to choose.

9. A whole program #

A grouping key with a fallback, keys sorted before they are joined, and a record tested before it is read:

gh = connect `@lf/github` using my_github
team = connect `@lf/slack` using team_workspace

function `Open Issues`(go: <manual-[:Invocation]->>) {
  authors = gh-[r:Repositories WHERE r.Name == "api"]->-[i:Issues WHERE i.State == "open"]-> { return i.Author }
  by = List.groupBy(authors, (who) => coalesce(who, "nobody"))
  lines = map(sort(Dict.keys(by)), (who) => "${who}: ${count(by[who])}")
  ops = only(team-[ch:Channels WHERE ch.Name == "ops"]->)
  if (!exists(ops)) { fail("no #ops channel") }
  write ops-[:Messages]-> { Message: Text.join(lines, "\n") }
}

10. Diagnostic code index #

codewhat 0.15 refuses
MOV_V5_HIDES_BUILTINa name that is a word an expression reads as itself
MOV_COLLECTION_OP_NOT_A_COLLECTION, MOV_ARITH_NON_NUMERIC, MOV_BUILTIN_ARG_TYPEa coalesce whose union is not what the read needs
MOV_ABSENT_REQUIREDan Airtable or Google Sheets field that may be empty, or a built-in's answer that may be absent (section 8), read where a value is needed
MOV_DICT_KEY_MAY_BE_ABSENTa grouping key that may be absent (now an error)
MOV_FOLD_NEEDS_ORDERfirst, [0], Text.join or reduce over the records an unordered walk's block answers, or over a list that spreads them
MOV_V5_SPELLINGa CAPS walk word (LOWER, EXISTS, …) inside walk brackets; rewritten by listen-fire upgrade, except LENGTH of a list, which is count(xs) by hand