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}"
  }
}

Match without Scrutinee (Boolean Dispatch)

Omit the scrutinee for boolean conditions:

fn classify(n) {
  match {
    n == 0 -> "zero"
    n > 0  -> "positive"
    _      -> "negative"
  }
}

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)
  }
}

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"
}

All alternatives must bind the same variables:

-- OK: both sides bind x
Some(x) | Ok(x) -> use(x)

-- ERROR: left binds x, right binds y
Some(x) | Ok(y) -> ...   -- compile error

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"
}

Works in any pattern position. Common with channel.select:

match channel.select([ch1, ch2]) {
  (^ch1, Message(msg)) -> handle_first(msg)
  (^ch2, 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 Some(user) = find_user(value) else { return Err("not found") }
  when let Admin(perms) = user.role else { return Err("unauthorized") }
  do_admin_thing(user, perms)
}

The else block must diverge (return or panic). This flattens the “staircase of doom” that nested match creates.

Boolean form — also accepted, for flat guard sequences:

fn buy(qty, balance, price) {
  when qty > 0 else { return Err("out of stock") }
  when balance >= price else { return Err("not enough money") }
  Ok("purchased")
}

Both forms can be mixed freely in the same function.

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.

Trade-off: no if. Simple boolean checks are more verbose (match debug { true -> ..., false -> () }). In practice, guardless match and when-else cover most cases.