Pattern Matching
match evaluates one scrutinee and selects an arm. Its arms must produce values
that unify to one result type. Default and ML use different delimiters but lower
to the same Expr::Match and Pattern nodes.
Literal and binding patterns
A scalar literal compares with the scrutinee. A lower-case name binds the scrutinee and is therefore a catch-all.
let label = match value {
0 => "zero"
1 => "one"
n => "other: ${n}"
}
Union patterns
A nullary variant is matched by name. A payload has two destructuring forms, and the form written — not how the payload was declared — decides how binders map onto slots:
Ctor { a, b }binds each binder to the field of that same name, independent of their order in the pattern.Ctor(a, b)binds by slot: the binder in column i takes payload slot i, whatever it is spelled (TYPE-UNION-POSITIONAL).
A named payload accepts either form, so Some(n) is legal beside
Some { value }. A positionally declared payload accepts only the second in
practice, because its slots have no spellable names.
Deciding this from the declaration instead is what let the type checker and the
code generator disagree: the checker read Ctor(a, b) by column while codegen
read it by name for any named payload, so a binder that named no field bound
nothing, and one that named the wrong field silently loaded another slot —
returning a payload pointer out of an -> int function.
type Option = Some { value: int } | None
type Tree = Leaf | Node(Tree, Tree)
let message = match option {
Some { value } => "value=${value}"
None => "none"
}
let side = match tree {
Node(left, _) => left
Leaf => Leaf
}
ML drops the payload delimiters (Some value, Node left _). It has only the
one form, and that form is the positional one — ML Some value is the same
constructor pattern as Default Some(value), so an ML binder always takes its
column and may be renamed freely. Success/Error are the single exception:
their payload binds by role (value, message), so ML spells them by name like
Default's Success { value }.
Wildcard patterns
_ matches without binding:
let category = match score {
100 => "perfect"
_ => "other"
}
List patterns
List patterns may require an exact length or bind a remaining suffix. The rest binder is permitted only once and only at the end (TYPE-LIST-PATTERNS).
let first = match values {
[] => 0
[head, ...tail] => head
}
Type annotation patterns
name: Type binds a value under the written compile-time type. It is not a
runtime type test: code generation treats the arm as a catch-all. Use it only
when the scrutinee already has that static type; to discriminate at runtime,
match the structure instead.
let label = match person {
p: Person => p.name
}
Structural patterns — [PATTERN-STRUCTURAL]
A brace pattern matches a value's row (TYPE-ROW)
and binds each named field. It is the runtime counterpart of structural
unification, and the only way to discriminate an any
(TYPE-ANY):
let described = match value {
{ message } => "just=${message}"
{ code, .. } => "coded=${code}"
_ => "unknown"
}
A pattern is closed by default — { message } selects a row of exactly
message — and trailing .. opens it to any row carrying at least the named
fields. Closed-by-default is what keeps arm order from changing meaning: an open
{ x, y } would select a three-dimensional point and shadow a later
{ x, y, z } arm. The compiler rejects a ..-opened arm that shadows a later
one rather than resolving it by order.
A binder always takes its field's own name. Nested rows
({ origin: { x, y }, .. }) and field: binder renames do not parse — see the
status note below.
Tuple patterns — [PATTERN-TUPLE]
A tuple pattern binds by position (TYPE-TUPLE). Because a tuple is a row with decimal field names, it is the positional spelling of a structural pattern, not a separate mechanism:
match pair {
(n, label) => "${label}=${n}"
}
(x) is grouping, not a one-element tuple. A tuple pattern is closed: its
length must match, and .. does not apply.
Status: implemented for flat rows in both flavors: closed
{ f }and open{ f, .. }arms select concrete records statically and erased values by runtime shape descriptor, and the tuple spelling(a, b)works the same way over positional rows. A match overanyrequires its catch-all, and the..-shadowing rule above is enforced. Nested rows ({ origin: { x, y }, .. }) andfield: binderrenames do not parse yet — a binder always takes its field's own name, and a tuple slot is a binder or_.
Exhaustiveness and unreachable arms [TYPE-MATCH-EXHAUSTIVE]
A match over bool, Result, or a known union must cover every case, either
explicitly or with a catch-all. A duplicate variant arm is unreachable, as is
every arm after _ or a lower-case binding; the compiler rejects both.
A match over any is never exhaustive — its row is not known until run time —
so it always requires a catch-all
(TYPE-ANY). Among structural arms,
a ..-opened arm makes every later arm whose row extends it unreachable, and
the compiler rejects that shadowing rather than resolving it by arm order
(PATTERN-STRUCTURAL).
Matches over open scalar domains such as int need a catch-all when total
behavior is required, but the compiler does not prove scalar exhaustiveness.
Result patterns
Result<T, E> uses the built-in Success { value } and Error { message }
variants:
let calculation = intDiv(10, 0)
match calculation {
Success { value } => print("result=${value}")
Error { message } => print("error=${message}")
}
The error type of fallible built-ins is Error. Arithmetic operators produce no Result and are matched no differently from any other total expression (ARITH-TOTAL).
Non-Result Scrutinees Auto-Wrap — [PATTERN-RESULT-AUTOWRAP]
A Success/Error pattern may be matched against a value that is not a
Result: the scrutinee is treated as if wrapped in Success, so Success
binds the value itself and the Error arm is unreachable. Both the type
checker and code generation apply this rule identically.
The Success arm is therefore taken unconditionally for a non-Result
scrutinee. A value's magnitude or sign MUST NOT select the arm — a negative
scalar is a Success payload, not an error sentinel. Under a
negative-means-error heuristic match -1 { Success { value } => value ... }
would take the Error arm and abs(-1) would observe 0, silently wrong on
every negative value.
This rule governs hand-written Success/Error arms only. ?: does not
inherit it: its scrutinee must be a real Result, and a plain value on its
left is a compile-time error
(PATTERN-RESULT-DEFAULT).
Ternary Match (Syntactic Sugar)
Default has a structural form:
structuralTernary ::= expression "{" field ("," field)* "}" "?" expression ":" expression
Lowering binds each named field by direct field access and evaluates the then expression. It does not perform a runtime pattern test and does not evaluate the else expression; a missing field is a type error.
let value = record { value } ? value : 0
ML has no structural-ternary surface.
Result Default ?: — [PATTERN-RESULT-DEFAULT]
For a Result, result ?: fallback yields the Success payload or lazily
evaluates fallback for Error.
let safe = intDiv(10, 2) ?: 0
let failed = intDiv(10, 0) ?: 0
?: is right-associative and binds below every other operator
(Syntax). The same spelling is available in ML.
It is an explicit handling operation: the result of ?: is the unwrapped
success type, and the fallback must have that same plain type.
The scrutinee must be a Result; ?: is not a boolean operator and never
reinterprets a plain value as Success. Use the ordinary boolean ternary for a
boolean condition. This separation prevents Result handling from becoming an
implicit truthiness conversion.
Boolean ternary
Default condition ? yes : no lowers to a boolean match:
let status = active ? "active" : "inactive"
ML writes the equivalent match directly.
if / else (Syntactic Sugar) [GRAMMAR-IF-ELSE]
Default also provides a boolean if expression. else is mandatory, and each
branch is one expression. else if nests another if in the false branch.
fn tier(score) = if score >= 2000 { Epic } else if score >= 500 { Solid } else { Starter }
Lowering produces nested two-arm boolean matches; no if node reaches type
checking or code generation. ML writes the matches directly.