Elly is a small language layered on Muon (see
muon-spec.md). Muon defines only lexical and structural
syntax; Elly assigns evaluation semantics to Muon structures — chains,
items, and tuples. Elly source is therefore valid Muon: Muon is a
superset of Mu source code, and Elly is one front-end over it.
This document specifies only a first, deliberately small subset of Elly:
&).foo)Int, arbitrary
precision) and their __Int_* builtins[…]) and keyed
maps ({ … }) — with ()
reserved for syntactic grouping / argument spreadFor the broader vision (records, typing, uniqueness) see
elly-intro.md. Everything outside this subset is collected
under deferred at the end.
Elly evaluates Muon structure. Each construct in this subset is a reinterpretation of a Muon production:
| Elly construct | Muon production |
|---|---|
| reference | <sym> (identifier) |
| symbol literal | <prefixed> — . (see below) |
| integer literal | <sym> (numeric) |
| application | <chain> (juxtaposition) |
| abstraction / bind | <prefixed> — & (see below) |
| list value | <list> — […] |
| grouping / spread | <tuple> — (…) |
Elly uses Muon's whitespace and separator rules unchanged, but — unlike Muon, which gives neither any meaning — Elly distinguishes them semantically:
(f x) is one chain, the
application of f to x.<sep>
(, or newline) delimits the chains of a
parenthesized or bracketed <seq>: (f, x)
are two chains and [f, x] two list elements.The two brackets do different jobs, and this is the one place a reader must keep the Muon layer in mind:
(…) is pure syntax — grouping,
argument spread, and the nullary marker (see application and abstraction). It never denotes a value:
(f x) is one chain (the application f x), and
f (a, b) spreads to the curried call
f a b.[…] is the only list value constructor
(see lists): [],
[.x] (a genuine 1-list, distinct from .x),
[f, x].A program in this subset is a single expression (one
<chain>). Top-level sequencing of multiple chains in
a <seq> is deferred — it needs binding/sequencing
semantics not in this subset.
Comments (<comm>) may appear between items as in
Muon; they are ignored by evaluation.
The & extension to muon.
Abstraction needs a marker that is itself part of the notation, so
Muon's <item> is extended with a prefixed
item:
(* no whitespace *)
<prefixed> ::= "&" <item>
At the Muon layer this is purely structural and carries no meaning,
exactly as : or -1 are just
<sym>s. Elly gives &<item> the
meaning "introduce a binding". & attaches to the item
immediately after it (no whitespace): &x is one
prefixed item. This addition is reflected in muon-spec.md's
<item> production.
The literal values in this subset are:
A list is written […]; the empty list [] is
unit — there is no separate unit value. A map is
written { … }.
The wider numeric tower (Num: rationals, floats,
complex), of which Int is the first, integer-only slice, is
deferred. Strings get a first, deliberately small design — literals, map
keys, comparison, and a codepoint round-trip — with most string
functionality still deferred (see strings).
Sym, symbolsElly symbols are .-prefixed literals that evaluate to
themselves (unless the context gives them another meaning, e.g. list
projection). A symbol is a . directly glued to a single
name or number segment:
.foo // a symbol `.foo`
.0 // a symbol `.0`
A symbol wraps exactly one segment. Because . is a Muon
sigil that breaks symbols (see muon-spec.md),
.foo.bar is not one symbol but the chain
.foo .bar — two symbols juxtaposed, i.e. successive
projections; the two spell the same value. The bare dot .
(nothing glued to its right) is a Muon <punct>, not a
symbol — Elly reserves the spaced dot for a future application /
composition combinator and rejects it as an atom for now.
Symbols are the subset's tags and enumerations.
// map a status tag to a code, falling through to a default
&s __eq s .ok (&_ 0) (&_ __eq s .warn (&_ 1) (&_ 2))
// a catch handler: rescue .div_by_zero, re-raise anything else
// (see error handling for __Err_catch / __Err_raise)
__Err_catch
(&exc __eq exc .div_by_zero (&_ .undefined) (&other __Err_raise other))
(&_ (__Int_divrem 1 0).0) // ⇒ .undefined
Int, integersIntegers are arbitrary precision, signed, and
self-evaluating. This subset provides only
Int; the wider numeric tower — rationals, floats, complex —
is deferred under a future Num, with Int as
its integer-only subset.
An integer literal is a Muon <sym> that is not a
<symbol> (not .-prefixed) and
matches:
(* recognized before <name>. `_` separates digit groups and may not lead,
trail, or double. *)
<int> ::= <sign>? <magnitude>
<sign> ::= "-" | "+"
<magnitude> ::= <dec> | <hex> | <bin>
<dec> ::= <digit> ("_"? <digit>)* (* base 10 *)
<hex> ::= "0" ("x"|"X") <hexdig> ("_"? <hexdig>)* (* base 16 *)
<bin> ::= "0" ("b"|"B") <bit> ("_"? <bit>)* (* base 2 *)
0
-123
+7
1_000_000
0xCAFE // == 51966
0b1010 // == 10
The base prefix, sign and separators are notational only:
0xF, 15, 0b1111 and
+15 all denote the same value. A <sym>
that is neither a <symbol>, a valid
<int>, nor a valid <name> (e.g.
1a, 0xZZ, --1) is an error;
operator-like syms remain deferred. Note that .0 is the
symbol .0 (a .-prefixed
0, a projection index), never the integer
0.
+0,
-0, 0 are all equal).0,
15, -123.__Int_* builtinsArithmetic and elimination are builtins in the
reserved __ namespace (see bindings, names): free names resolved from
the runtime environment, referenceable but not bindable. All are
curried (f x y = (f x) y) and
strict in their integer arguments; supplying a
non-integer where an integer is required raises
.not_an_int (see errors).
| builtin | shape | meaning |
|---|---|---|
__Int_add x y |
Int → Int → Int |
x + y |
__Int_sub x y |
Int → Int → Int |
x − y (in this order) |
__Int_mul x y |
Int → Int → Int |
x × y |
__Int_pow x y |
Int → Int → Int |
x to the power y (y ≥ 0) |
__Int_divrem x y |
Int → Int → [Int, Int] |
Euclidean quotient and remainder [q, r] |
__Int_for from to state onEach |
Int → Int → s → (Int → s → s) → s |
ascending fold over from … to−1 |
__Int_sub x y subtracts in written order:
__Int_sub 2 5 is −3.__Int_pow x y raises x to the power
y, with 0^0 = 1. The exponent must be
non-negative: y < 0 would give a non-integer and
raises .negative_exponent.__Int_divrem x y returns the Euclidean pair as a list
[q, r] with q = x ÷ y and
r = x − y·q normalized to 0 ≤ r < |y|, so
x = y·q + r always holds; project it with
.0/.1. A zero divisor (y = 0)
raises .div_by_zero.= <ref> pattern (or the
general __eq, see equality) and strict order is the
< <ref> / > <ref> ordering
patterns (see patterns), which
compose with = into <= /
>=. A __match over those clauses is the
subset's conditional.__Int_for from to state onEach computes
onEach (to−1) (… (onEach (from+1) (onEach from state)) …) —
onEach takes the index first, accumulator
second, returning the next accumulator. It iterates ascending
only over the half-open range [from, to);
from ≥ to runs zero iterations and yields
state (so an inverted range is a harmless no-op). It is the
bounded iteration primitive, standing in for the fixpoint combinator
this subset still defers.// factorial: iterate i from 1 to n inclusive, acc ← acc × i
&n __Int_for 1 (__Int_add n 1) 1 (&i &acc __Int_mul acc i)
// sum 0 … n−1
&n __Int_for 0 n 0 (&i &acc __Int_add acc i)
// absolute value, via a sign test (0 named explicitly; no __Int_neg)
&n __match [&(< 0) __Int_sub 0 n, &_ n] n
A comparison returning a symbol
(.lt/.eq/.gt) is deliberately
not provided: ordering is expressed directly with the
< <ref> / > <ref> patterns
(and = for equality), which compose into the rest
(<=, >=, …) without a symbol to
eliminate.
Str, stringsA string is an immutable, ordered sequence of Unicode scalar values —
the subset's textual data, a runtime kind Str alongside
Int Sym List Map Fun. This is a first, deliberately small
string design: literals, map keys, comparison, and a codepoint
round-trip, with most string functionality still deferred (see
deferred).
A string literal is a Muon <str> — text between
double quotes. Muon validates only the escape syntax and
carries the text raw; Elly is the higher layer that reads meaning into
it, decoding a literal into the scalar values it
denotes, once, at parse time:
\" \\ \/ \n \t \r \b \f stand for their
usual characters;\uXXXX names one UTF-16 code unit, and a surrogate
pair — a high unit (\uD800–\uDBFF)
followed by a low unit (\uDC00–\uDFFF) —
combines into the single scalar it encodes (\uD834\uDD1E
denotes U+1D11E);"hello"
"" // the empty string
"line\nbreak" // contains a newline
"\u0041" // == "A"
"\uD83D\uDE00" // one supplementary-plane scalar (above U+FFFF), U+1F600
The value a literal denotes keeps no trace of how it was spelled —
"A" and "\u0041" are the same string. A string
is self-evaluating: it evaluates to itself.
"foo",
.foo, and foo are three different values.Str joins Int as a comparable kind for the
< <ref> / > <ref> ordering
patterns (see patterns)..sym /
<num> ("foo" ≡ = "foo"); a
string may also be a map key, and a lookup key in a map
pattern, keyed by that same equality (see maps).__Str_* /
__Sym_* builtinsReserved __ namespace, curried, and
strict in their one typed argument — each
raises its own tag on the wrong kind
(.not_a_str, .not_an_int, or
.not_a_sym). These primitives view a string as its sequence
of codepoints — the Unicode scalar values, so .a (U+0061)
is 97 — and string manipulation for now is a simple but
inefficient round-trip through codepoint lists.
| builtin | shape | meaning |
|---|---|---|
__Int_str n |
Int → Str |
signed decimal of an integer (15 → "15",
-123 → "-123"). A non-int
raises .not_an_int. |
__Sym_str s |
Sym → Str |
a symbol's name without the dot (.foo →
"foo"), the inverse of __Sym_from. A
non-symbol raises .not_a_sym. |
__Str_pack cps |
List → Str |
build a string from a list of codepoints; a non-Int
element raises .not_an_int, and one
outside 0 … 0x10FFFF or in the surrogate range
raises .bad_codepoint. |
__Str_unpack s |
Str → List |
the codepoints of a string, in order ("abc" →
[97, 98, 99]); the inverse of __Str_pack. |
__Sym_from s |
Str → Sym |
the symbol named by a string ("foo" →
.foo), the inverse of __Sym_str. The text must
be a single Muon <sym> segment — the only form that
re-lexes from .<text> — so a string with whitespace,
an interior ., or any non-<symchar>, and
the empty string, raise .bad_symbol. |
__Int_str 15 // ⇒ "15"
__Sym_str .foo // ⇒ "foo"
__Sym_from "foo" // ⇒ .foo
__Str_unpack "abc" // ⇒ [97, 98, 99]
&s __Str_pack (__Str_unpack s) // the identity on strings
Scalar→text is split by kind — __Int_str and
__Sym_str, each monomorphic — rather than one overloaded
conversion; stringifying a compound value (list, map, function) wants a
real inspect and is deferred. Concatenation, interpolation,
multiline literals, and the rest of string manipulation are deferred too
(see deferred); concatenate meanwhile by
unpacking both strings, joining the lists with
__List_append, and packing the result.
List, listsA list is a positional, immutable sequence of zero or more values,
written as a Muon <list> — a <seq>
in brackets. Elements are the
<chain>s of the sequence, delimited by
<sep> (, or newline); each element chain
is evaluated as an <expr>.
[] // the empty list — this is unit
[.a] // a 1-list (distinct from the symbol .a)
[.a, .b] // a 2-list, elements indexed .0 and .1
[.a, .b, .c] // a 3-list
Brackets are the only list constructor, so every
arity is spelled the same way — including the 1-list [.a],
which is a real one-element list, not the bare element. That
removes the old ambiguity: grouping is () (which carries no
value — see grouping and spread), a
list value is [].
(x) // grouping: == x (no list)
[x] // a 1-list holding x (a value)
The empty list [] is unit: it is what
the nullary marker () feeds (f () ==
f []), what a thunk is forced with, and the "none" of the
[]/[v] option convention. There is no separate
unit value.
<list> ::= "[" <seq> "]" (* zero or more element chains; a muon.list.
[] is unit; [e] is a 1-list; <sep> per muon-spec.md *)
Projection is not a separate form — it is syntax-level
"application" of a list to an index symbol that desugars into a
getter (like List.get_elem in Elixir). Since
juxtaposition is application, [.zero, .one].0 is an
<iexpr>: the list "applied" to the symbol
.0, selecting the named position:
[.zero, .one].0 evaluates to .zero[.zero, .one].1 evaluates to .one[].0 and [.foo].1 do not name a valid
position (raise .projection_out_of_range)[.a, .b].foo — a non-index symbol — raises
.bad_projectionNote: the typeless interpreter may implement this as a special-cased
App(<list>, <symbol>), but a typechecker will
not accept a list in function position, so it must treat projection as
the distinct getter it desugars to. No bracket-indexing syntax is added:
[i] is itself a list value, so t[i] would mean
"apply the list t to the list [i]" (an error).
Dynamic access and destructuring are builtins (see below).
__List_* builtinsReserved __ namespace, curried, strict in the list/index
arguments; a non-list where a list is required raises
.not_a_list.
| builtin | shape | meaning |
|---|---|---|
__List_size t |
List → Int |
element count |
__List_get t i |
List → Int → val |
element at 0-based i; out of range raises
.projection_out_of_range, a non-integer i
raises .not_an_int |
__List_append a b |
List → List → List |
concatenation [a…, b…] |
__List_with t i v |
List → Int → val → List |
t with index i replaced by v;
out of range raises .projection_out_of_range |
__List_split n t |
Int → List → [List, List] |
split at position n into [left, right]
(left is the first n elements); n outside
[0, size] raises .projection_out_of_range |
__List_get generalizes .N projection to a
computed index (.0 only reaches literal
positions). __List_size gives the bound for an
__Int_for loop over a list.__List_append, __List_with, and
__List_split are the persistent update
operations the rpds::Vector backing makes cheap (structural
sharing, no full copy): concatenate two lists, replace one index, or
split at a position. __List_split n t returns a
[left, right] pair, the inverse of
__List_append left right when split at left's
size.// sum a 3-list of ints by destructuring
[1, 2, 3] |> (&[a, b, c] __Int_add a (__Int_add b c)) // ⇒ 6
// safe head: branch on size first
&t __eq (__List_size t) 0 (&() .empty) (&() __List_get t 0)
// persistent update: grow, replace, split
__List_append [.a, .b] [.c] // ⇒ [.a, .b, .c]
__List_with [.a, .b, .c] 1 .B // ⇒ [.a, .B, .c]
__List_split 1 [.a, .b, .c] // ⇒ [[.a], [.b, .c]]
The __Int_divrem builtin (see integers) returns a list [q, r],
so its result is projected (.0/.1) or
destructured by a pattern (&[div, rem]) like any
other.
Named fields / records are deferred; positional indices
.0, .1, … are all this subset has.
Map, mapsA map is an immutable, persistent,
unordered association from keys to values, written
{ key: value, … }. It is Elly's keyed collection, the
counterpart to the positional […] list.
.foo,
.0), integers, unit, lists, other maps — and even closures
— may all be keys; there is no key restriction and no bad-key error. Key
identity is the structural value equality of
__eq (see equality):
integers by mathematical value (0 = +0),
symbols by text, strings by scalar sequence, lists/maps structurally —
so the symbol .0, the integer 0, and the
string "0" are three distinct keys.
Callables compare by identity, so two separately
written &x x are distinct keys.__Map_for visits pairs (and in which a
(k) capture pattern peels them) is unspecified — stable
within one build, but not a language guarantee, so a program should not
depend on it. There is no language-level ordering of keys, not even
among numbers.<block> → Map)A literal is { <seq> }; each chain of the sequence
is one entry
<key> ":" <value>. The key is the single head
item; the : is a <punct>; the value is
the rest of the chain lowered as an <expr>. An
entry's key item is one of:
one) — the
symbol of that spelling, exactly as .one
would be (a key is just a token, so no keyword checks apply). A
digit-leading bare key (0, 5)
is rejected to avoid the number/symbol confusion — write .0
for the symbol or (0) for the integer;.-prefixed symbol
(.one, .0) — the same symbol key, written
explicitly;(…) group — a computed key,
the enclosed expression evaluated ((one) is the value of
variable one; (0) is the integer
0);[…] list — a list key; the empty
list [] is the unit key;"…" string — a string key, keyed by
its text (distinct from the like-spelled symbol).So { one: 1 } is shorthand for { .one: 1 },
and to key on a variable's value or an integer you must parenthesize:
(one), (0). An empty value tail is
sugar for the unit value [] (so { .foo: } ==
{ .foo: [] }). A later entry with a key equal to an earlier
one overwrites it.
{} // the empty map (== __Map_nil)
{ []: .unit } // the unit key
{ one: 1, two: 2 } // symbol keys .one .two (== { .one: 1, .two: 2 })
{ .0: .zero } // key is the SYMBOL .0
{ 0: .zero } // also the SYMBOL .0 (bare atom → symbol)
{ (0): .zero } // computed key → the INTEGER 0 (distinct from .0)
{ (one): 1 } // computed key → the VALUE of variable one
{ (__Int_sub 2 2): .zero } // computed key → integer 0
{ "foo": 1 } // a STRING key (distinct from the symbol .foo)
{ "0": 0 } // STRING key "0" — distinct from .0 and the integer 0
There is no bracket / projection syntax yet; access is explicit via
__Map_get and __Map_gets. A missing key
raises .missing_key (a catchable tag on
the single error channel), so a safe lookup is
__Err_catch (&tag …) (&_ __Map_get m k).
TODO: add a projection syntax (bracket m[k] or a
. projection).
__Map_* builtinsReserved __ namespace, curried, strict in the map / key
arguments; a non-map where a map is required raises
.not_a_map. Any value is a key, so there is no bad-key
error. Callbacks are applied with the same apply the
__Int_* eliminators use.
| builtin | shape | meaning |
|---|---|---|
__Map_nil |
Map |
the empty map (a value, not a function) |
__Map_with m k v |
Map → key → val → Map |
m with k set to v
(overwrites) |
__Map_get m k |
Map → key → val |
value at k; a missing key raises
.missing_key — branch safely with
__Err_catch |
__Map_gets m ks |
Map → (key…) → (val…) |
list of values for a list of keys (parallel); any missing key raises
.missing_key |
__Map_for m state (&key &value &state e) |
Map → s → (key → val → s → s) → s |
fold over the map (unspecified order) |
__Map_merge m1 m2 (&key &l &r e) |
Map → Map → (key → opt → opt → opt) → Map |
generic merge: for each key in the union, the
callback gets each side's value as an option
([] none / [v] some) and returns an option —
[] drops the key, [v] sets it |
__Map_cat a b |
Map → Map → Map |
union; on a key conflict b wins |
__Map_without m k |
Map → key → Map |
m with k removed (a no-op if absent); the
persistent-update counterpart of __Map_with |
__Map_size m |
Map → Int |
element count, symmetric with __List_size (emptiness is
also just __eq {} m) |
__Map_merge's option encoding reuses lists: none is the
empty list [], some is the 1-list [v] (built
and size-matched with __List_*; see lists). A callback result that is not a
[] / [v] list raises
.list_size_mismatch (or .not_a_list if it is
not a list at all).
// build { .a: 1, .b: 2 } explicitly
__Map_with (__Map_with __Map_nil .a 1) .b 2
// count entries: fold ignoring key/value, +1 each
&m __Map_for m 0 (&_ &_ &st __Int_add st 1)
// value at .a, or .missing if absent (safe lookup via catch)
&m __Err_catch (&_ .missing) (&_ __Map_get m .a)
// union preferring the left map's value on a conflict
&a &b __Map_merge a b (&_ &l &r __eq (__List_size l) 1 (&_ l) (&_ r))
// refutable lookup: value at .a, or .absent — a miss falls through, not raises
&m __match [&{ .a: v, ...rest } v, &_ .absent] m
Fun, functionsFunctions are first-class values. There are two kinds of callable:
__ namespace (__Int_add,
__List_get, …), each curried and strict in its typed
arguments;&), which capture
the environment in which they are written.Both are applied by juxtaposition (see application), and both flow like any other value
— passed as arguments, returned, stored in lists and maps. Functions
compare by identity, never by structure (see __eq); Fun is
the kind that matches any callable.
__Fun_* builtinsTODO
Elly is a strict functional programming language, with arguments evaluated before application.
A bare name is a reference, never a binding. It
evaluates to the value bound by the nearest enclosing
&-binder of the same name; if there is none, it is a
free name resolved in the surrounding (e.g. top-level / builtin)
environment.
x // the value bound by an enclosing &x, else a free name
Because binding is always marked with &, shadowing
is explicit — a name is never rebound by accident. The corollary is an
accepted hazard in this subset: omitting a & where you
meant to bind silently turns an intended binding into a reference.
The standalone name _ is the discard
pattern (see abstraction), not a name: it
never binds a value and is not a valid reference.
Names beginning with a double underscore (__) are
reserved for builtins and special forms (e.g.
__call, __keys in elly-intro.md).
They may be referenced — they resolve in the surrounding
environment like any other free name — but a &-binder
may not introduce a new one.
A few words are keywords: let (the
local binding form; see local binding),
as (the type-narrowing pattern qualifier; see patterns), and the reserved-but-unused
with, when, match,
case, and of (parked for a future
with-binding form, a
<pat> when <cond> guard, and surface
case / match sugar over the
__match builtin). Unlike __-names, a keyword
is not even a reference — it is syntax, so it may be neither read nor
bound anywhere. This is the one exception to "a bare name is always a
reference": these words are recognized as keywords first. (Using a
reserved-but-unused keyword at all is an error until its form is
designed.)
Abstraction introduces a function: & prefixes a
binder, and the body is the rest of the expression.
&x x // the identity function
&x &y x // &x (&y x) — a constant function of x
Binders curry, right-nested, mirroring left-nested application, and because &
reaches to the end of the expression a lambda used as a non-final
argument must be parenthesized:
&x &y e == &x (&y e) // abstraction, right-associative
f x y == (f x) y // application, left-associative
f x &y g y == (f x) (&y (g y)) // the trailing lambda captures the tail
f (&x x) y == ((f (&x x)) y) // parens keep the lambda as one argument
A binder is a pattern, so it can destructure or
narrow the argument, and a (…) binder
group curries several parameters at once:
&[a, b] e // destructures a 2-list argument
&(x as Int) e // narrows the argument to an Int
&(a, b) e == &a &b e // a binder group: two curried parameters
&() e == &[] e // nullary: a thunk asserting a unit argument, forced by f ()
An abstraction is a Muon <prefixed> item
(& glued to a binder header) followed by a body
expression; it evaluates to a one-parameter closure (see Fun). & has the
lowest precedence, so the body is the rest of the
expression, extending to the end of the enclosing expression. An
abstraction with a binder but no body, or a body but no binder
(&x alone), is ill-formed.
(* no whitespace between "&" and the binder header *)
<abs> ::= "&" <binder-header> <expr> (* body is the rest of the expr, right-nested *)
<binder-header> ::= <item-pattern> (* a pattern occupying one Muon <item> (see below) *)
| <binder-group> (* a (…) group that curries *)
<binder-group> ::= "(" ")" (* nullary: == "[]" *)
| "(" <pattern> (<sep> <pattern>)* ")" (* == &p0 &p1 … *)
Because & binds a single Muon
<item>
(<prefixed> ::= "&" <item>), a bare header
— one written without a surrounding (…) group — must be a
pattern that fits in one item. Any pattern that spans
several items (= <ref>,
< <ref>, > <ref>,
<pat> as <Type>, the at-pattern
<name> = <pat>, the or-pattern
<pat> | <pat>) is not a legal
bare header and must be wrapped in a (…) group. A bare
matching literal — a <num>, a .sym, or a
string — is rejected too, though it fits in one item:
&(42) / &(.foo) /
&("s"), never &42 /
&.foo / &"s", so a lone literal is not
read as a stray value in binder position. The bare forms — each equal to
the same pattern grouped, &(<pat>) — are:
| bare header | ≡ | binds / matches |
|---|---|---|
&<name> |
&(<name>) |
binds the argument to <name> |
&_ |
&(_) |
discards the argument (binds nothing) |
&[<pat>, …] |
&([<pat>, …]) |
a list pattern |
&{ <key>: <pat>, … } |
&({ <key>: <pat>, … }) |
a map pattern |
&(…) is not a table row because it is the
binder group itself: a single chain is one grouped
pattern (&(p) == &p), while several
comma-separated chains curry into successive parameters
(&(a, b) == &a &b).
Application is the elimination form: applying an
abstraction substitutes the argument for the bound name, β-reduction
cancelling one & against one applied argument —
(&x e) a → e[x := a]. Abstraction and application are
thus the introduction and elimination forms of functions.
The binder header is matched in an irrefutable
position: a structural mismatch (wrong list size, wrong kind, a failed
= <ref>) raises rather than falling
through — the refutable form is __match (see patterns and matching). Two
constraints hold on any binder:
_ is the discard pattern:
&_ e still consumes one applied argument (β-reduction
cancels the & as usual) but binds no name.__-prefixed name — those
are reserved (see bindings, names) — so
&__x e is ill-formed.The (…) binder group is the mirror of
application spread (grouping and
spread): its chains curry into successive parameter patterns, and
the empty group () is the nullary marker, desugaring to
&[] (the unit pattern). A "nullary function" is thus a
thunk that asserts it was forced with the unit []:
&() e suspends e behind one parameter
until f () (feeding the unit []) fires it —
there is no zero-argument function distinct from a value. The two roles
of (…) diverge here: inside a binder group commas
curry into successive parameters, while inside a single
pattern they are an error — so &[a, b] is one 2-list
parameter, &(a, b) is two parameters, and
&((a, b)) is ill-formed.
let)Binding a value to a name is, at bottom, abstraction-and-application:
(&x e) v evaluates e with x
bound to v. But that reads backwards — the value sits
after the body. let is sugar for
the same thing, written name-first:
let (x = v) e // ≡ (&x e) v — evaluate e with x bound to v
let (x = .foo) x // → .foo
let adds no evaluation semantics of its own: it lowers
to App/Abs and inherits everything from them —
strict, value-first evaluation, and the fact that a binder may be
_ (discard) but not a __-name.
A binding's left-hand side is a pattern (see patterns), exactly like a
& parameter: let (pat = v) e desugars to
(&pat e) v. The separator is the first
top-level = (one not nested inside
(…)/[…]/{…}), so a pattern that
itself carries a top-level = — an at-pattern
n = p or an equality = <ref> — must be
parenthesized to keep the binding's = unique. A top-level
| (an or-pattern) must be parenthesized for the same
reason, so it does not compete with the binding =:
let ([a, b] = v) e // atomic list pattern — parens optional
let ((n = [a, b]) = v) e // at-pattern — the wrap makes the binding `=` unique
let ((= 3) = v) e // an equality pattern in binder position is an assert
let ((.a | .b) = v) e // or-pattern LHS — wrap the `|`, never `let (.a | .b = v)`
Unlike &(a, b) (two curried parameters), a
let binding takes one pattern: there is no
let ((a, b) = v) curry-group. By happenstance
let ((= <val>) = e) is an assert —
it binds nothing and raises .no_match unless e
equals <val>.
The body is the rest of the expression, exactly like
&: let has the lowest precedence and
extends to the end, so let (x = 1) f x is
let (x = 1) (f x), and a let used as a
non-final argument must be parenthesized.
A binder group may hold several bindings, separated
by <sep> (, or newline). They are
sequential: each right-hand side sees the binders to
its left, so let (a = 1, b = a) … is valid and
b is 1. This desugars to nested
lets:
let (a = va, b = vb) e ≡ let (a = va) (let (b = vb) e) ≡ (&a ((&b e) vb)) va
so va is evaluated in the enclosing scope and
vb in the scope where a is bound. Being nested
&/apply, let is therefore
non-recursive — a right-hand side never sees its own
binder. Because the group is a Muon <seq>, blank
lines, hanging separators, and comments between bindings are allowed, so
a group can be laid out as a block:
let (
a = 1
// b builds on a
b = a
) __Int_add a b // → 2
TODO: a binder group (a = 1, b = 2) is written with
parens like an argument group, but let reads it as a
left-to-right binding group — scope threads through the
commas, so a right-hand side sees the binders to its left (unlike an
argument spread f (a, b), whose chains are independent
expressions in one scope). This is the same syntax-is-context hazard the
parens carry everywhere (grouping vs spread vs binder group); a future
parallel form (all right-hand sides in the enclosing scope) is what the
reserved keyword with is earmarked for.
Recursion is still not directly expressible: an abstraction is
anonymous and a name refers only to an enclosing binder, and
let — desugaring to it — is the same. It can be recovered
with a fixpoint combinator once the evaluation strategy is fixed; both
are deferred.
Juxtaposition of items in a chain is function application, left-associative:
f x // apply f to x
f x y // (f x) y
A parenthesized <tuple> in a chain does
not contribute one argument — it
spreads: each of its chains becomes one successive
argument (see grouping and spread below), so
f (a, b) is the curried call f a b. A list
value is written with brackets instead (see lists).
(* top-level expressions: mapped from a muon.chain *)
<expr> ::=
| <let> (* local binding; see "local binding" *)
| <iexpr>? <abs>
| <iexpr>
(* "itemic" expressions: mapped from a muon.chain of muon.item. Each <arg> is one
item; a parenthesized <group> is *not* one arg but a spread — it contributes
its chains as successive args (grouping / spread / nullary; see below). *)
<iexpr> ::=
| <iexpr> <arg> (* left-associative *)
| <arg>
<arg> ::=
| <aexpr> (* one atom = one argument *)
| <group> (* a (…) spread: zero, one, or many arguments *)
(* a muon.tuple `( … )`, read by chain count:
() -> the nullary marker: feeds one unit value [] (an empty call)
(e) -> grouping: the single expression e (one argument)
(e0, e1…) -> spread: each chain as one successive argument *)
<group> ::= "(" <seq-of-exprs> ")"
(* "atomic" expressions: mapped from a muon.item *)
<aexpr> ::=
| <name> (* a reference to a binding *)
| <symbol> (* an atomic symbol like `.x`, `.0`, etc *)
| <int> (* an arbitrary-precision integer literal; see integers *)
| <str> (* a string literal, a muon.str `"…"`; see strings *)
| <list> (* a runtime list value, a muon.list `[ … ]`; see lists *)
| <block> (* a map literal, a muon.block `{ … }`; see maps *)
(* an identifier: latin letters, digits and `_`, not starting with a digit, and
not the standalone `_` (the discard pattern; see abstraction). A name may
start with `__`, but only to reference a builtin — never to bind a new name. *)
<name> ::= (* a <muon.sym> matching the above that is not a number *)
(* a `.`-prefixed literal: `.` glued to a single name or number segment. *)
<symbol> ::= (* a <muon.prefixed> with `.` sigil wrapping a name or number *)
(* an integer literal: `0`, `-123`, `+7`, `1_000_000`, `0xCAFE`, `0b1010`.
Recognized before <name>; a `.`-prefixed item is a <symbol>, not an <int>.
See integers for the full digit/base/separator grammar. *)
<int> ::= (* a <muon.sym> matching the integer grammar in "integers" *)
(* a string literal: `"…"`, a muon.str. JSON escapes and `\uXXXX` (surrogate
pairs combined) are decoded at parse; a lone surrogate is a parse error.
See strings. *)
<str> ::= (* a <muon.str>, decoded per "strings" *)
(* abstraction; the binder is a single name or a (…) group of binders that
curries — &(a, b) e == &a &b e, &() e == &_ e. See "abstraction". *)
<abs> ::= "&" <binder-header> <expr> (* the header is mapped from <muon.prefixed> *)
(* local binding; the keyword "let" leads a muon.chain, then a binder group
(a muon.tuple) and the body. See "local binding". *)
<let> ::= "let" "(" <binding> (<sep> <binding>)* ")" <expr>
<binding> ::= <pattern> "=" <expr> (* LHS pattern carries no top-level "=" / "|" — wrap those in (…) *)
A trailing <abs> is the application's last
argument and captures the rest of the expression.
Parentheses are pure syntax — they never build a
value. A ( <seq> ) in a chain is read by how many
chains it holds, on the rule one chain → one
argument:
(e) // grouping: the one expression e
(f x) // == f x (one chain: an application)
f (a, b) // spread: == f a b (two chains → two arguments)
f (a) b // == f a b (one-chain group is just grouping)
The single exception is the empty group
(), the nullary marker: an empty call
still calls, so () feeds one unit value
[]:
f () // == f [] (a nullary call feeds the unit [])
So the count is: N ≥ 1 chains spread to N arguments; 0 chains feed
one unit. This keeps the familiar f () and the thunk idiom
(&() body, forced with (); see abstraction). A standalone () with
no function to feed degenerates to the unit [].
Because () carries no value, a genuine list — including
a 1-list — is written with brackets: [a, b],
[x] (distinct from x), [] (see lists).
|> is the pipe: it threads its left
operand in as the final argument of the application on its right.
L |> R is exactly R L — the whole
right-hand spine applied to one more argument, L:
x |> f // == f x
h x |> g y // == g y (h x) (the left spine is one argument)
3 |> __Int_add 1 // == __Int_add 1 3 → 4 (piped value is the last argument)
It is left-associative, so a pipeline reads left-to-right as a data flow, each stage receiving the previous result as its last argument:
h x |> g y |> f z // == f z (g y (h x))
|> is a <punct> at the Muon layer
(> is a punctchar, so |> munches as one
token; see muon-spec.md), given meaning only here —
lowering-time sugar to application (App), adding no
evaluator semantics.
It binds looser than application (juxtaposition) but tighter than
&/let (abstraction / local
binding):
x |> f a b is f a b x,
and xs |> map (&x f x) is
map (&x (f x)) xs — the pipe idiom, with the lambda
parenthesized exactly as any non-final-argument lambda must be (see abstraction).&/let means a
&/let captures the pipe as (part of) its
body: &x x |> f is &x (f x) — a
composition — not (&x x) |> f, and
let (x = a) x |> f runs f x with
x bound to a.Both operands must be present: |> f,
x |>, and x |> |> f are
ill-formed.
A pattern matches an input value, binding names as
it goes. One grammar serves every binder position — a &
parameter, a let binding's left-hand side, and a
__match clause. A pattern is a first-class AST
node the evaluator matches directly:
&<pat> body is the one binding form, and applying
it matches the argument against <pat>,
extending the environment or refuting. Patterns are
not compiled into other primitives at parse time; they carry no
runtime value of their own. (A later bytecode backend may compile a
whole match into branch instructions — see elly-patterns-native.md.)
Every structural mismatch refutes (⟂) —
a wrong kind, a wrong arity, a failed = <ref> /
< <ref> / as <Type> — carrying
the original error it stands for (.not_a_list,
.list_size_mismatch, .no_match, …). A pattern
is a closed grammar: it embeds no general expression,
so matching itself does not raise. The comparand of
= / < / > and a computed
map key are each a <ref> — a name or
a literal — evaluated with the ordinary expression
default; a literal cannot raise, a bound name resolves, and the
only real Error a match can produce is referencing
an unbound name in a <ref> (a normal
reference error, not a refutation), which propagates. A pattern is
used two ways: in binder position
(& / let) a refutation is uncaught, so it
surfaces as its original error; the refutable
__match builtin (below) catches the refutation and falls
through to a fallback instead.
| pattern | meaning |
|---|---|
_ |
matches anything, binds nothing (discard) |
name |
binds the subject to name, sugar for
name = _ |
name = <pat> |
at-pattern: binds the subject to name,
then matches <pat> against it |
(<pat>) |
grouping — a single subpattern (commas are an error here) |
(<pat> | <pat>) |
or-pattern: try left, on refutation try right (same binder set) |
<pat> as <Type> |
type-narrowing (postfix): matches iff the subject is of
<Type>, then matches <pat> |
(as <Type>) <pat> |
type-narrowing (prefix): the same, qualifier parenthesized |
.sym |
matches a symbol literal (bare-literal equality, like
<num>) |
<num> |
matches an integer literal (= <num> sugar); binds
nothing |
"str" |
matches a string literal (= "str" sugar); binds
nothing |
= <ref> |
matches iff the subject equals <ref>'s value;
binds nothing |
< <ref> |
matches iff the subject is ordered before
<ref>'s value; binds nothing |
> <ref> |
matches iff the subject is ordered after
<ref>'s value; binds nothing |
[<pat>, …] |
list pattern — fixed arity, with an optional trailing rest |
{ <key>: <pat>, … } |
map pattern — closed by default, with an optional trailing rest |
where a <ref> (the comparand of
= / < / >) is a single
atom — a <name> (= x)
or a literal (= 5, = .foo,
= "s"). It is not a general expression: a pattern
never recurses into expression syntax, so a compound = f x
/ < (f x) is a parse error.
Rules:
name always binds; .sym,
<num>, "str", and
= <ref> never bind. The
= <ref> comparand is a name or literal evaluated in
the enclosing scope extended by the bindings to its left, so
[x, = x] matches a pair of equal elements. (Left-to-right
order between independent sub-patterns is not yet a language
guarantee.).sym, a <num>, or a string in pattern
position matches by equality — 42 ≡ = 42,
.foo ≡ = .foo, "s" ≡
= "s" — so (1 | 2 | 3) reads better than
(=1 | =2 | =3). One exception: a lone matching
literal that is a whole &-header or let
LHS must be parenthesized — &(42) /
&(.foo) / &("s"),
let ((42) = v), never &42 /
&.foo / &"s" /
let (42 = v) — so a bare literal is not mistaken for a
stray value in binder position. A literal nested in […] /
{…} / (…) / an or-pattern needs no
parens.< <ref> / > <ref> are
strict comparisons of the subject against
<ref>'s value. Each comparable kind orders only
against itself — Int numerically, Str
lexicographically by scalar (see strings) —
so a subject and bound that are not both of the same comparable
kind refute (a refutation, not a host raise):
.not_string when the bound is a string, otherwise
.not_int. An in-kind comparison that does not hold refutes
.no_match. <= e and >= e
are not primitives — write them as or-patterns,
(< e) | (= e) and (> e) | (= e). Both
directions are provided (rather than just <) because the
subject is fixed on the left, so neither bound can be expressed by
flipping the other.as narrows to a closed, capitalized
set — Int Sym Str List Map Fun, the six runtime
kinds (Fun = any callable). as binds tighter
than =, so name = p as Int reads
name = (p as Int). as <Type> is not a
pattern on its own: the bind-nothing type test is
_ as <Type>, and (as Int) alone is an
error. The kind test is total, so a wrong kind refutes
.no_match (fall-through) rather than raising a host
.not_an_int.[p0, …, ...rest] binds rest to a list of the
remainder, and ... (no name) ignores it. Rest-in-the-middle
is deferred.(p1 | p2) are committed,
left-to-right. The left arm is tried first; if its
pattern refutes, the right arm is tried. Once an arm's pattern
matches it is committed — a later refutation in the
continuation (a sibling sub-pattern, or the body) propagates rather than
backtracking into the other arm. Both arms must bind exactly the same
set of names, checked at parse time. Multiple | is
left-associative: (a | b | c) parses as
((a | b) | c).{ .a: p } matches a map with exactly key
.a (whose value matches p); an extra key
refutes (.map_size_mismatch), a missing key refutes
(.missing_key), a non-map refutes
(.not_a_map). A trailing rest opens it:
{ .a: p, ... } matches at least .a,
and { .a: p, ...rest } binds rest to the
remainder map (the matched keys removed). A key is itself a
pattern, of two kinds:
.sym, a string literal "foo", a bare
non-digit sym, or a parenthesized
(= <ref>) / (<lit>) for a computed
or numeric key. A digit-leading bare key is rejected
({ 5: p }) to avoid the number/symbol confusion — write
.5 for the symbol key, or (5) /
(= 5) for the integer key 5. Both
(5) and (= 5) are lookups (a
(<pat>) group whose pattern is an equality), but
(= 5) reads best here: a bare
(k) in key position is a capture, so the
= marks "lookup, not binder". (This is also why the pattern
writes (= k) where a map literal writes
(k).)(k) / (_) — the map analogue of the list
[a, ...rest]. It peels an unspecified remaining
entry, binding its key (and, via the value pattern, its value):
so &{ (k): v } { .one: 1 } binds
k = .one, v = 1. Multiple captures peel that many entries;
a map is unordered, so which entries they peel (and in what
order) is unspecified and a program should not depend on it.
Lookups run first: the specific-key lookups claim and
remove their keys, then the captures pop from what remains,
then closedness / the rest apply to the leftover — so a capture
never steals a key a lookup wanted. A capture on an empty (or
fully-consumed) map refutes .missing_key.__match, the
multi-clause form__match <clauses> <val>
__match takes a list of clauses (each a
matcher, typically &(<pat>) <body>) and
tries them against <val> in order:
NoMatch) is
skipped and the next is tried;Error —
a real failure in the body (a pattern itself cannot raise, save an
unbound name in a <ref>) —
propagates;.no_match: uncaught it surfaces as
.no_match, but nested inside another matcher it is still
catchable.Because every structural probe refutes, __match catches
a wrong kind, a wrong arity, and a failed
=/.sym/as alike; only a real
error escapes. A multi-clause match is just a longer list — no nesting
needed:
// classify a symbol, else fall through
&x __match [&(.a) .got_a, &(.b) .got_b, &_ .other] x
A &_ final clause is the total fallback; drop it and
an unmatched value refutes .no_match. Surface
case / match / of sugar over
__match is reserved but not yet built.
__eq, value equality__eq compares any two values and
branches on whether they are equal. It is the general equality
eliminator.
| builtin | shape | meaning |
|---|---|---|
__eq x y onEqual onElse |
a → a → (a → r) → (a → r) → r |
onEqual x if a equals b, else
onElse x |
a → r)The equality relation it eliminates:
0 = +0), symbols by their
text, strings by their scalar sequence (no Unicode normalization), and
lists elementwise (same length, equal elements). So the symbol
.0, the integer 0, and the string
"0" are pairwise not equal, and
[] (unit) equals only [].&x x are distinct. Builtins are equal
when they are the same operation applied to equal arguments
(__Int_add = __Int_add,
__Int_add 1 = __Int_add 1, but
__Int_add 1 ≠ __Int_add 2), and a builtin is
never equal to a closure.There is no total order over all values. Ordering is defined only
within the comparable kinds Int and
Str, and only through the < <ref> /
> <ref> patterns — numerically for integers,
lexicographically by scalar value for strings. Maps are hash maps keyed
by this structural equality, so their iteration order is unspecified
(see maps).
// discriminate two values
__eq .a .a (&_ .same) (&_ .diff) // ⇒ .same
__eq .0 0 (&_ .same) (&_ .diff) // ⇒ .diff (symbol vs integer)
__eq [1, 2] [1, 2] (&_ .same) (&_ .diff) // ⇒ .same
// identity: a closure equals itself, not a twin
(&f __eq f f (&_ .same) (&_ .diff)) (&x x) // ⇒ .same
__eq (&x x) (&x x) (&_ .same) (&_ .diff) // ⇒ .diff
Asking a value's kind and falling through on "no" is done with
<pat> as <Type> patterns (see
patterns), not a builtin.
x as Int narrows x to Int,
refuting .no_match off-kind; the six kinds are
Int Sym Str List Map Fun (Fun = anything
applicable — a closure or an unsaturated builtin). Classify a value with
a __match:
// classify any value: the first matching clause wins
&x __match [&(_ as Int) .int, &(_ as Sym) .sym, &_ .other] x
// prove a value is callable, then use it
&g __match [&(f as Fun) f 2 3, &_ .not_a_fun] g
The kind test is total — it never raises, it only
matches or refutes — which is exactly what puts it on the
pattern-refutation path. Fun is also the name the future
standard module/type will carry (as Fun Int Int ≙
Int -> Int).
The evaluator matches a value v against a pattern by
structural recursion, threading the environment (this is the native
match_pattern in eval.rs; there is no
eliminator desugaring):
_ succeeds binding nothing; name binds
v.name = p binds v to name,
then matches p against v.= e evaluates e (which may raise a real
Error) and succeeds iff v equals it, else
refutes .no_match. < e /
> e likewise, but succeed iff v is strictly
ordered before / after e's value within a shared
comparable kind, refuting off-kind (.not_string when
the bound is a string, else .not_int) and
.no_match on an in-kind miss.p as T succeeds iff v is of kind
T (a total test, never raising), then matches
p against the narrowed v; off-kind refutes
.no_match.[p0 … pn] refutes .not_a_list on a
non-list, .list_size_mismatch on a wrong arity (exact
without a rest, a minimum with one), else matches each element
left-to-right and binds a named rest to the remainder.{ … } matches in two passes: the
concrete-key lookups claim and remove their keys first
(a missing key refutes .missing_key), then each
capture pops an unspecified remaining entry (an empty
remainder refutes .missing_key), then closedness / a named
rest apply to what is left (a leftover under a closed pattern refutes
.map_size_mismatch). Lookup keys and embedded expressions
each evaluate once, in source order.(p1 | p2) is committed: matching
returns the first arm whose pattern matches (with that arm's bindings)
and the body runs once afterward, so a later refutation propagates
rather than backtracking into the other arm.A &<pat> body binder and
let (pat = v) body share this single matching path
(let desugars to (&<pat> body) v,
reusing &/App). A backend may instead
compile a whole __match into a decision tree of explicit
tests and branches, turning a refutation into a jump; the operational
rules above are the meaning it must preserve. See
elly-patterns-native.md.
⟂ — NoMatch vs ErrorThe error channel carries two kinds of unwinding
value (see errors): an Error
— a genuine failure (__Err_raise, a host abort) — and a
NoMatch — a pattern refutation, produced by the matcher on
a structural miss and carrying the original error v the
refutation stands for. The distinction is by construction, not
an inspectable tag: user code only ever produces Error (via
__Err_raise), so it cannot forge a
NoMatch — a plain __Err_raise .no_match is an
Error, not a refutation. Only __match singles
out NoMatch; every other boundary
(__Err_catch, the top level) treats it exactly as its
payload v, so an uncaught refutation surfaces as
its original error.
Evaluation has a single raised-value channel. Any
expression either evaluates to a value or raises one. A
raise abandons the surrounding computation — arithmetic, list
construction, application, projection — and unwinds to the nearest
enclosing __Err_catch, or to the top level if there is none
(where it surfaces as the program's error).
A raise is one of two kinds: an Error
(a genuine failure) or a NoMatch (a pattern refutation
carrying the original error it stands for; see patterns). They differ only in what
stops them — __match catches NoMatch and lets
Error through; every other boundary treats a
NoMatch as its payload, so
__Err_catch and the top level see an uncaught refutation as
its original error. Host failures and __Err_raise produce
Error; only the pattern matcher produces
NoMatch (on a structural miss), so the refutation signal
cannot be forged.
Host failures raise, they do not silently abort:
__Int_divrem x 0 raises .div_by_zero, a
non-integer operand raises .not_an_int, a negative exponent
raises .negative_exponent, an out-of-range projection
raises .projection_out_of_range, and so on. This is the
same channel __Err_raise uses, so all of them are catchable
uniformly.
| host failure | raised value |
|---|---|
divisor 0 in __Int_divrem |
.div_by_zero |
non-integer operand to an __Int_* builtin |
.not_an_int |
negative exponent in __Int_pow |
.negative_exponent |
projecting (or __List_get) past a list's end |
.projection_out_of_range |
| projecting with a non-index symbol | .bad_projection |
| applying a non-function, non-list value | .not_applicable |
| a reference with no binding | .unbound_name |
a non-list where __List_* needs one |
.not_a_list |
a non-map where __Map_* needs one |
.not_a_map |
__Map_get / __Map_gets on an absent
key |
.missing_key |
a non-string where __Str_* / __Sym_from
needs one |
.not_a_str |
a non-symbol argument to __Sym_str |
.not_a_sym |
a codepoint out of 0…0x10FFFF (or a surrogate) in
__Str_pack |
.bad_codepoint |
a string that is not one Muon <sym> segment in
__Sym_from |
.bad_symbol |
an uncaught pattern refutation — decays to its original
tag |
e.g. .not_a_list / .list_size_mismatch /
.map_size_mismatch / .not_int /
.not_string / .no_match |
The tags are bare symbols for now; carrying the offending value (or a
source span / backtrace) is a deferred, additive refinement.
.unbound_name is a check that could later move to lowering;
promoting it is subtractive from this channel, not a redesign of it.
__Err_* builtins| builtin | shape | meaning |
|---|---|---|
__Err_raise x |
a → ⊥ |
raise x as an Error; never returns
normally |
__Err_catch onErr wrapped |
(e → r) → ([] → r) → r |
wrapped (), but on any raise
onErr <payload> |
__match clauses val |
[a → r] → a → r |
try each clause on val in order; the first to match
wins, a NoMatch clause is skipped, an Error
propagates; an empty/exhausted list refutes .no_match |
__Err_raise x raises the value x and does
not return. It is strict only in that x is already
evaluated (call-by-value; if computing x itself raised,
that short-circuited first); it wraps x untouched — a
symbol, list, or any other value.__Err_catch onErr wrapped forces
wrapped (). If that yields a value, it is the
result of __Err_catch and evaluation continues in the
enclosing expression (the implicit continuation — there is no
onOk). If it instead raises, the result is
onErr <payload> — __Err_catch is the
universal boundary, catching both an Error and a
NoMatch (an uncaught refutation, delivered as the original
error it carried; only __match singles NoMatch
out). wrapped must be a thunk
(&_ …): under real short-circuiting an eager argument
would unwind past the catch before it ran, so the thunk defers
the risky work into the builtin's control — the same shape as the
__eq branch thunks. A raise from
onErr propagates; it is not re-caught here. Handler-first
curries: __Err_catch onErr is a reusable catcher awaiting
the risky computation.// a safe divide: catch the zero-divisor raise, report a symbol instead
&x &y __Err_catch (&_ .undefined) (&_ (__Int_divrem x y).0)
// bubble a domain error, then rescue it at the boundary
__Err_catch (&exc exc) (&_ __Err_raise .not_found) // ⇒ .not_found
// success flows straight through the catch into the continuation
__Int_add 1 (__Err_catch (&_ 0) (&_ 41)) // ⇒ 42
Discriminating a caught tag — handling .div_by_zero but
re-raising the rest — uses __eq (see equality): the handler tests
exc against a tag, and its onElse branch,
which binds the original value, re-raises it. That handler appears as
the second example under symbols.
Intentionally out of this subset (carrying TODO: here
and/or in elly-intro.md):
Num —
rationals, floats, complex (with Int as a subset), and any
numeric coercions[foo: 1, bar: 2]) — a list-layer ([…])
featureinspect); the Str value itself
(literals, map keys, comparison, a codepoint round-trip) has landed (see
strings)__match builtin are in patterns; still to come are pattern
rest-in-the-middle (only a trailing rest is allowed
today) and surface case/match sugar over
__match. composition combinator — a
spaced dot (f . g) lexes as an infix
<punct> awaiting these semantics (the
|> pipe combinator is now specified; see the pipe combinator)<expr> as <ty> on
the value side; the pattern-side as <Type> is in patterns), including the full
<tyexpr> (arrows, __Type_of)^
is reserved (not &)elly-intro.md)