Repository navigation
v0.5.0
A value that may be absent now has a type — and a way back out of it.
Until now the only type for a value whose shape was not fixed was Any, and the only way out of Any was cast, which fails where a program wanted to test. A lookup that might find nothing had to fail rather than report it, and a published module could not write first without one copy per element type. v0.5.0 gives the language unions, a case that dispatches on them, type parameters and optional record fields — and nearly triples the built-in library, so that fewer tasks have to shell out to get ordinary work done.
Union types and type dispatch
T | Nullis the type of a value that may be absent.find_env("PORT")returnsString | Nullinstead of failing on an unset variable or inventing a sentinel;get_envremains the form for a variable the task requires in order to run at all.- A union is never inferred (4.3). It is the type of an expression only where an annotation or a callee's signature says so, so a heterogeneous array literal is still
Array<Any>and two branches of differing types are still a type error. Every union in a program is one that someone wrote. caseis the multi-way conditional, in three forms: value heads compared by==, type heads dispatched on the runtime type, and a scrutinee-less condition form in place of the chainedelse ifthe grammar never had. Exactly oneelsearm, last, is required — exhaustiveness is never inferred from the type of the scrutinee (E-SYNTAX-CASE-ELSE).- An arm narrows its scrutinee. Where the scrutinee is written as a plain local name, the
elsearm has the members that are left: afterNull -> 8080,to_number(p)sees aString.caseandcastrun the same runtime check (15.8) and differ only in what happens when it does not hold — one tests, the other fails. - Every form of
casenormalizes to nestedchoose, with the scrutinee bound once and evaluated exactly once however many arms are tested, so it adds no evaluation rule of its own. Two new error codes:E-SYNTAX-CASE-ELSEandE-TYPE-CASE-DUPLICATE, the latter for a later arm that can never be selected.
Type parameters
- A function declaration and a type alias may bind them:
first_or<T>(xs: Array<T>, fallback: T): T,type Opt<A> = A | Null,type Pair<A, B>. The cost of not having them was measurable — 43 of the library's 112 signatures were polymorphic, and every one was a function a user could not have written. - They are never written at a use site. A call instantiates them from the argument types and the expected type, in no particular order, by the rules built-in symbols already followed; 4.4 stops being "built-in polymorphism" and becomes one set of rules for both.
- Inside the body of its declaration a type parameter is rigid. It conforms only to itself and to
Any, so it is not comparable, not ordered, not stringifiable, not acasttarget and not acasetype head. A body that needs an operation takes it as an argument, which is what makes the feature safe without bounded quantification. - A default value is checked once, at the declaration, with the parameters rigid, so it has to hold for every instantiation:
--xs: Array<T> = []and--y: T | Null = nullare admitted,--y: T = 1is not. For the same reasonlask runcan instantiate every parameter atAny— whatever it hands over, the body can only pass along. E-TYPE-ARITYnow also covers a type argument count that does not match an alias's parameters.
Optional record fields
?after a field name makes the key optional —Record<name: String, tags?: Array<String>>— which is a different question from whether the value may be null. A JSON producer omits an optional field far more often than it writes an explicit null, andcastused to reject exactly that. The notation is TypeScript's, and so is the meaning:a?: Tqualifies the key alone.- An absent key and a null value stay different where it matters.
castaccepts a value whose optional keys are missing and still rejects one missing a required key (15.8); serialization omits an absent optional field while writing"b": nullfor a null one (13.1). - In memory the two read alike. An optional field reads as
T | Null(6.8), an absent key reading as null, which is what keeps field access from failing at run time. A program that must tell them apart casts the record to aMap<T>and askshas_key. - Conformance stays invariant. The required set, the optional set and each field type must all be identical, so
Record<a: String>conforms toRecord<a?: String>in neither direction: whether a key has to be there is part of the type. Optionality is never inferred, as unions are not.
Built-in library
- 42 functions to 115, with no existing signature changed. The additions sit where writing a task meant shelling out.
- Filesystem and path operations, both new.
read_filewrite_filefile_existsremove_filemake_dirlist_dirglob, andpath_joindirnamebasenameextnamenormalize_pathis_absolute_path. Each filesystem function takes theEnvironmentas its last argument, so where it reads is as explicit as where a command runs. There is deliberately no recursive removal: a destructive traversal stays a command, where the execution log can see it. - Arrays:
findfind_indexeveryanysortsort_byzipuniquerangeenumerateflattenflat_mapslicetakedropreversefirstlastsizeis_emptycontains_arrayindex_of_array.rangeis how aforexpression iterates a number of times andenumeratehow it iterates with an index, sincefortraverses an array and has no numeric form. - Strings:
to_stringto_numberlinessubstringpad_startpad_endrepeatstarts_withends_withindex_ofcontains, andregex_testregex_matchregex_replace. - Maps:
get_orsetremovemergeentriesfrom_entriesmap_values. Environment:find_envhas_envget_env_or. Data:base64_encodebase64_decodesha256md5. Alsoshell_quote,log,uuid,random_string, andminmaxsumpowsqrtclamp. - Absence is reported two ways, deliberately. A function that returns a position reports it as
-1(index_of,find_index); one that returns a value reports it asNull(find,find_env). Two new runtime error codes:E-RUNTIME-VALUEandE-RUNTIME-REGEX.
Language and editor
- Type annotations on local bindings. A binding inside a block takes one in the same shape as a top-level declaration,
!!marker included:cfg: Record<name: String> = cast(from_json(stdin)). This is how an expression that needs an expected type gets one inside a block (6.5), andcastis the case that motivates it. - Instantiation no longer depends on argument order. An argument whose own type comes from its position — a
cast, afail— is checked once the other arguments and the expected type have determined that position, and may stand anywhere in the call. Arguments still evaluate left to right. - Hover and
--helpshow a declaration with its binder,first_or<T>. Where the name is instead something to type — the usage line, a completion candidate, the function argument oflask run— it stays plain (11.6). Record field completion shows an optional field at the type it reads as, andcaseis painted as a control keyword.
Breaking changes
caseis a reserved word. A module using it as a declaration name or an identifier-form record field is nowE-SYNTAX-UNEXPECTED-TOKEN; such a field is written{"case": ...}and read asx["case"]. No module in this repository was affected.- An expression of type
Anyis refused in a polymorphic position. 4.4 has always said the way out ofAnyis a runtime check, but the matcher for polymorphic signatures accepted one anyway, somap(v, f)on av: Anywas quietly admitted and the element type became whatever the lambda said. Insert acast, or narrow withcase, first.
Lask is pre-1.0 and every feature is experimental until 1.0, so breaking changes remain possible; see compatibility.md.
Fixes and internals
- Running a command in a container now works on Windows. A bind mount was passed as
-v <source>:<target>, which packs source, target and mode into one colon-separated field. Every Windows path carries a colon in its drive letter, soC:\projwas read as sourceC, target\projand mode/work, which the daemon refused. Mounts now use--mount type=bind,source=...,target=..., where each field is named; the same break was reachable on POSIX, where a colon is legal in a directory name. - A CLI argument that does not match its parameter type says what did not fit. The one fact the user needs,
expected Number, got String, used to arrive wrapped in theShowoutput of the internal failure record, so the line read like a crash rather than a usage error. It is the first thing a new user sees on a typo. - The mount regression is now exercised in CI rather than by hand, the install test drives a released binary the way the README tells a reader to install it, and the
.debis named the way the release page names it. The build is warning-free again.
Documentation
- Quick Reference — the whole language and CLI on one page, ten minutes end to end, with every section linking to the chapter of the specification that defines it. spec.md is 4,300 lines and answers the questions nobody asks first; this is the way in.
- The README documents the
.debattached to every release, distinguishes it from the APT repository that is still planned, and leads with what it costs to start rather than with verification.
Full Changelog: v0.4.0...v0.5.0