Pattern Matching
match is an expression: it evaluates a value and runs the first arm whose pattern matches, binding any captured names along the way.
let shape = Shape::Circle(3)
let label = match shape {
Shape::Circle(r) => "circle with radius {r}",
Shape::Rectangle { w, h } => "rectangle {w}x{h}",
Shape::Empty => "empty",
}
print(label)
Arms are tried top to bottom; the first matching arm wins. A match with no matching arm at runtime raises an error — the checker warns at parse time when an enum match misses variants (see Exhaustiveness).
Patterns
Literal
Matches a specific value:
match n {
0 => "zero",
1 => "one",
42 => "the answer",
_ => "something else",
}
Literal patterns use the same cross-type numeric rule as == — 1 matches both Int(1) and Number(1.0).
Wildcard _
Matches anything, binds nothing. Typical “default” arm.
Bindings
A lowercase identifier captures the scrutinee value under that name for the arm’s guard and body:
match request {
request => handle(request), // `request` is bound here
}
Inside nested patterns, bindings capture the piece they sit at:
match pair {
[first, second] => first + second,
}
Struct patterns
Match a user struct with an exact type identity. Each field pattern runs against the corresponding field’s value:
struct Point { x, y }
let p = Point { x: 3, y: 4 }
match p {
Point { x: 0, y: 0 } => "origin",
Point { x, y } => "at ({x}, {y})",
}
Field patterns can be bindings (x), literals (x: 0), or any nested pattern.
Enum variant patterns
match shape {
Shape::Circle(r) => r * r,
Shape::Rectangle { w, h } => w * h,
Shape::Empty => 0,
}
Unit, tuple, and struct variants all work. Like struct patterns, field / tuple entries can themselves be patterns.
Namespaced patterns
Types imported through an aliased use must be matched through the same namespace:
use paint as p
match c {
p.Color::Red => "stop",
p.Color::Green => "go",
_ => "?",
}
The matcher compares the value’s full (module_path, type_name) identity against what the alias resolves to — p.Color::Red only matches values that came from the paint module.
Array patterns
match items {
[] => "empty",
[only] => "one: {only}",
[a, b] => "two: {a} and {b}",
[head, ..] => "starts with {head}",
[head, ..tail] => "{head} then {tail}",
}
[..]— matches any array, captures nothing.[..name]— captures the trailing elements as an array.- Patterns before the rest must all match; the rest is optional.
Or-patterns
p1 | p2 | p3 — matches if any of the alternatives matches. Each alternative must bind the same set of names so the arm body has a consistent view:
match day {
"Sat" | "Sun" => "weekend",
_ => "weekday",
}
Guards
An arm can add a boolean guard after if. The arm only fires when the pattern matches and the guard is true:
match n {
x if x < 0 => "negative",
0 => "zero",
x if x < 10 => "small positive",
_ => "big positive",
}
Guards can see any names the pattern bound.
As an expression
match is an expression — every arm’s body is an expression, and the whole thing evaluates to the winning arm’s body:
let grade = match score {
s if s >= 90 => "A",
s if s >= 80 => "B",
s if s >= 70 => "C",
_ => "F",
}
All arms should produce compatible types if you rely on the result — Bop is dynamically typed, so heterogeneous arms aren’t a parse error, but they usually signal a bug.
Exhaustiveness
Bop’s static checker warns when a match over an enum misses variants:
enum Color { Red, Green, Blue }
let _ = match Color::Red {
Color::Red => "r",
Color::Green => "g",
// warning: missing variant `Color::Blue`
}
A wildcard or a bare-name catch-all (_ or other) marks the match as exhaustive. Guards don’t count toward coverage — a guarded arm covers only the guarded subset, so a partially-guarded match still needs a catch-all.
The checker follows use statements when the embedder supplies a module resolver, so imported enums aren’t opaque — missing-variant warnings fire on them too.