8. Pattern matching
open result
struct Point
x: i32
y: i32
end
type LookupError
NotFound(i32)
Unknown
end
def show(x: i32, y: i32) -> string
"#{x},#{y}"
end
def log(p: Point) -> string
"point"
end
def classify!(result: Result<Point, LookupError>) -> string [Crash]
match result
Ok(Point(x, y)) if x > 0 -> show(x, y)
Ok(p@_) -> log(p)
Err(NotFound(id)) -> crash!("missing ##{id}")
Err(_) -> crash!("unknown")
end
end
- Arms use
->, in the Rust spelling. There is noofkeyword. - Guards:
pattern if cond -> expr. - As-patterns:
p@Patternbinds the whole and destructures. - Wildcards:
_. - Struct destructuring binds a struct's fields inside a pattern, either positionally in declaration order (
Point(x, y)) or by name (Point(y=b, x=a), where the written order need not match the declaration order). A keyword struct pattern must name every field, as construction does; an unknown, duplicate or missing field is a compile error. The keyword form applies to structs alone, a sum variant's payload being positional. Both forms nest (Line(a=Point(x, yy), b)), compose with guards and as-patterns, and lower identically on both tiers: the checker reorders a keyword pattern to declaration order, and exhaustiveness and binding see the fields by slot. - List patterns:
[a, b, c]matches a 3-element list;[first, rest @ ..](TBD) for head and tail. - Numeric literal patterns compare in the scrutinee's tier. A
decimalpattern uses exact numeric equality:1.50matches1.5. Anf32pattern is rounded to the single-precision grid before comparison. Bothf32andf64use ordered IEEE equality: the two signed zeros match, and a NaN matches no literal pattern. This is the same comparison a named comptime constant pattern uses after folding, and never bitwise equality. - A named comptime constant is a pattern. A top-level binding is compile-time-evaluated (§16),
OP_CHAR: i32 = 0i32is a known value where a pattern needs one, andOP_CHAR -> …matches it. It behaves as the literal it folds to: refutable and binding nothing, and amatchover an open type still needs its catch-all. Primitives only (int, the fixed widths,string,bool,float,()); an aggregate constant is refused, structural equality in patterns being a question of its own.
A binder cannot be confused with it, and there is no mistyped-constant trap: the two spellings are separate already. A pattern beginning with an uppercase letter is a constructor position, a name there that resolves to neither a constructor nor a constant is an error, and a lowercase name binds.
OP_CHAR: i32 = 0i32
OP_SET: i32 = 1i32
def op_name(op: i32) -> string
match op
OP_CHAR -> "char"
OP_SET -> "set"
_ -> "unknown"
end
end
- An arm body is one expression, like an inline closure body (§13) and for the same reason: an arm has no
endof its own, and the next pattern begins where the body stops. A single self-delimiting expression is fine (if,match,loop,try…end). To run a sequence, a binding or side effect and then a result, wrap it indo … end:
open io
open result
def classify!(input: string, target: i32) -> string [io]
match i32.parse(input)
Ok(guess) -> do # do … end groups the two statements
io.print!("checking #{guess}")
if guess < target then "low" elif guess > target then "high" else "hit"
end
Err(e) -> e.reason
end
end
ifhas two forms and one tree. The block form,if COND…[elif COND …]* [else …] end, takes statement sequences and is closed byend. The inline form isif COND then A [elif COND then B]* [else E]: one expression per branch, noend, the form amatch-arm body and an inline closure body already take, and an expression like any other (x = if c then 1 else 2;xs.try_each(|x| if x > 0 then Ok(()) else Err(x))).thenselects it. The block form never takesthen, andif c then a else b endis no spelling at all: the inlineifends atband theendis stray. Without anelsethe inline form is()typed, the statement guard (if done then break). Itselifandelsecontinue on the same logical line, and a line break ends it. A danglingelsebinds to the nearestif:if a then if b then 1 else 2isif a then (if b then 1 else 2). That is the language's one precedence rule of the kind,endmaking every block nesting explicit; parenthesise the innerifto bind anelseoutward.hanki fmtpreserves whichever form was written, and it neither folds a one-expression blockifinto an inline one nor breaks an inline one into a block. It drops grouping parentheses the nearest-ifrule already implies, preserves the ones that bind anelseoutward, and parenthesises an inlineifstanding as an operator's operand (x + (if c then 1 else 2)), its last branch running as far right as the grammar allows. A closure gets the same treatment.do … endis a block expression: a sequence of expressions evaluated in order whose value and type are the last one's, and an emptydo endis(). It is the escape hatch wherever the grammar wants a single expression, amatch-arm body, an inline closure body (§13), an=-bound value. It is an ordinary block scoped like aniforelsebranch, and it reads and assigns bindings in the enclosing scope. It is not a closure, and it has no capture and no isolation.- Exhaustive by default. Every
matchmust cover all values of the scrutinee: a sum type needs an arm per variant, or a_;boolneeds bothtrueandfalse; an open type (int,string,List) needs a_. A non-exhaustivematchis a compile error, falling off the end being an unmarked crash, which a total language (§6) forbids. The escape for a branch you have proven unreachable is an explicit_ -> crash!(...). Guarded arms (pattern if cond) never count toward coverage, the guard being able to fail, and they always need an unguarded fallback. - Exhaustiveness is complete and witness-based. Nested patterns are recognised as covering whenever the value space they leave open is covered by the other arms:
Some(Red) | Some(Green) | NoneoverOption<Color>, withColor = {Red, Green}, is exhaustive with no catch-all. A non-exhaustivematchreports a concrete uncovered example,Some(Blue)or a deeperOk(Some(Green)). Coverage closes only over types with a finite, enumerable value space: sums, structs,bool,(). The open types above,int,string,List, functions and an uninstantiated generic, can never be closed by listing values and always require a_. - Unreachable arms are an error. An arm already fully covered by earlier arms, a duplicate or anything after a
_, is a compile error. A guarded arm neither shadows a later arm nor is shadowed by earlier ones, its guard being able to fail. - An
if/elifchain that dispatches one binding is amatch, and a compile error (H0618). Where three or more of a chain's conditions compare the same binding against a constant, a literal, a variant, or a constructor of constants (c == Some(93u8),s == "GET",x == 3), amatchon that binding is the intended form: it binds the payload once, a_arm captures the fallthrough, and coverage is checked. Rewriteif c == A … elif c == B … elif c == C … else …asmatch c. The lint fires only on==against a match-able constant, and a!=chain, anor-compound condition, or a comparison against a runtime value (another binding, a call) is left alone. A leading non-==guard (if n < 24u8 … elif n == 24u8 …) becomes a guarded arm (n if n < 24u8 -> …).