Pattern Matching
Pattern matching is the only branching construct. No if, no ternary, no
switch. Every branch uses match, and the compiler checks exhaustiveness.
Match with Scrutinee
fn describe(shape) {
match shape {
Circle(r) -> "circle with radius {r}"
Rect(w, h) -> "rect {w}x{h}"
}
}
Each arm starts on its own line; there is no separator between arms. A match
with one arm may be written on one line: match pair { (a, b) -> a + b }.
An arm’s body is one expression; statements take a block, p -> { ... },
whose last expression is the arm’s value. One expression needs no braces, and
silt fmt takes them away: 0 -> { "done" } becomes 0 -> "done". (It
leaves them where taking them away would move a comment to the next arm or
change how the next arm is read, and inside a string it keeps as written.)
Match without Scrutinee (Boolean Dispatch)
Omit the scrutinee for boolean conditions:
fn classify(n) {
match {
n == 0 -> "zero"
n > 0 -> "positive"
_ -> "negative"
}
}
The arms are tried in order and the first true condition wins. Such a match
must end with a _ -> ... arm, the value when no condition is true; leaving
it out is a type error.
Literal Patterns
Including negative numbers:
match n {
0 -> "zero"
1 -> "one"
-1 -> "negative one"
_ -> "other"
}
Works for Int, Float, String, and Bool.
Constructor and Nested Patterns
fn handle(result) {
match result {
Ok(Some(value)) -> use(value)
Ok(None) -> handle_empty()
Err(e) -> handle_error(e)
}
}
Qualified Patterns
Constructors and record types from another module are named through
the module (or its alias): m.Variant, or m.Enum.Variant, which
always works, even when two enums of the module share a variant name.
Inside the declaring module, Enum.Variant works too:
-- noexec (multi-file: shapes.silt declares `pub type Shape { Circle(Float), Rect(Float, Float) }`)
import shapes
fn describe(s) {
match s {
shapes.Circle(r) -> "circle {r}"
shapes.Shape.Rect(w, h) -> "rect {w}x{h}"
}
}
Qualified patterns work in every pattern position — match arms,
when let, let, or-patterns, and nested sub-patterns — and the
qualifier picks the right type when two imported modules export the
same name (a.Pt { x, .. } vs b.Pt { label, .. }). Naming the wrong
module or enum is a type error.
Tuple Patterns
fn fizzbuzz(n) {
match (n % 3, n % 5) {
(0, 0) -> "FizzBuzz"
(0, _) -> "Fizz"
(_, 0) -> "Buzz"
_ -> "{n}"
}
}
Record Patterns
Use .. to ignore remaining fields:
fn greet(user) {
match user {
User { name, active: true, .. } -> "hello {name}"
User { name, .. } -> "{name} is inactive"
}
}
List Patterns
Three forms: empty [], exact length [a, b, c], head/tail [h, ..t]:
fn sum(xs) {
match xs {
[] -> 0
[head, ..tail] -> head + sum(tail)
}
}
Nested patterns work in list elements: [Some(a), Some(b), ..rest]. The
.. syntax matches the record rest pattern (User { name, .. }).
Map Patterns
match config {
#{ "name": name, "greeting": greeting } -> "{greeting}, {name}!"
#{ "name": name } -> "hello, {name}!"
_ -> "hello, stranger!"
}
Or-Patterns
Match multiple alternatives in a single arm with |:
match n {
0 | 1 -> "small"
2 | 3 -> "medium"
_ -> "large"
}
A long list of alternatives may continue on the next line, which then
starts with its |:
match day {
"Saturday"
| "Sunday" -> "weekend"
_ -> "weekday"
}
All alternatives must bind the same variables (and, as in any match,
all alternatives must have the same type — you can’t mix Some(x) with
Ok(x) in one or-pattern):
-- OK: both alternatives bind `x`
fn unwrap_either(r: Result(Int, Int)) -> Int {
match r {
Ok(x) | Err(x) -> x
}
}
fn main() {
println("{unwrap_either(Ok(7))}") -- 7
}open in playground-- ERROR: first alternative binds x, second binds y
Ok(x) | Err(y) -> ...
-- compile error: or-pattern alternatives must bind the same variables
Guards
match n {
0 -> "zero"
x when x > 0 -> "positive"
_ -> "negative"
}
Guard expressions are checked after the pattern matches. Pattern bindings are available in the guard.
Range Patterns
Inclusive numeric ranges:
match score {
90..100 -> "A"
80..89 -> "B"
70..79 -> "C"
_ -> "F"
}
Float ranges work too: 0.0..1.0 matches value >= 0.0 && value <= 1.0.
Pin Operator (^)
Matches against the value of an existing variable instead of creating a new binding:
let expected = 42
match input {
^expected -> "got the expected value"
other -> "got {other} instead"
}
The variable is one from outside the pattern. A name the pattern binds
itself is not it: (x, ^x) compares the second element with an x bound
before the match, and is an error when there is none.
Works in any pattern position. Common with channel.select:
match channel.select([channel.Recv(ch1), channel.Recv(ch2)]) {
(^ch1, channel.Message(msg)) -> handle_first(msg)
(^ch2, channel.Message(msg)) -> handle_second(msg)
_ -> panic("unexpected")
}
when let/else for Inline Assertions
Asserts a pattern match and binds on success, or diverges on failure:
fn process(input) {
when let Ok(value) = parse(input) else {
return Err("parse failed")
}
when let Admin(perms) = user.role else {
return Err("unauthorized")
}
do_admin_thing(value, perms)
}
The else block must diverge: end it with return or panic, or,
inside a loop body, with loop(...) to restart the loop. A boolean form
(when cond else { ... }) also exists for flat guard sequences, and both
forms can be mixed.
See Error Handling for
the full treatment, including when to reach for when let-else instead
of ?.
Exhaustiveness Checking
The compiler checks that your match covers all possible cases. Missing a
variant produces a compile-time error. This is one of the strongest benefits
of match as the sole branching construct.
The check looks inside patterns: the fields of constructors, tuples and records, the elements of list patterns, the alternatives of or-patterns. Arms may split a value any way they like, as long as together they leave nothing out:
fn describe(xs: List(Option(Int))) -> String {
match xs {
[] -> "empty"
[None, ..rest] -> "starts with nothing"
[Some(n)] -> "just {n}"
[Some(n), _, ..rest] -> "{n} and more"
}
}
fn main() {
println(describe([Some(1), None]))
}open in playgroundAn arm with a guard may not be taken, so it covers nothing. Number and
string literals, ranges, pins and map patterns never cover their type
between them: a match on such a value needs an arm that takes every value
(_ or a name).
A match with so many interdependent patterns that the check cannot finish
is an error too (“could not verify that the match is exhaustive”); a _
arm settles it.
Trade-off: no if. Simple boolean checks are more verbose (a match debug { ... } with a true arm and a false arm, each on its own line). In
practice, guardless match and when-else cover most cases.