A pattern describes a value’s shape and names the parts to bind from it. One
pattern language serves four constructs: destructuring bindings, if pattern :=
conditions, for pattern in / for pattern := loop heads, and match arms.
struct Point { x: int, y: int }
fn manhattan(p: Point): int {
Point { x, y } := p
return x.abs() + y.abs()
}Bindings and wildcards
A fresh identifier matches anything and binds it. _ matches anything and binds
nothing.
fn f(values: []int): int {
mut count := 0
for _ in values {
count += 1
}
return count
}fn classify(value: int): string {
return match value {
0 => "zero"
other => "non-zero: ${other}"
}
}Whether a bare identifier is a new binding or a visible constant is resolved by the compiler: a name that resolves to a constant matches that constant’s folded value instead of binding.
const HTTP_OK = 200
fn label(status: int): string {
return match status {
HTTP_OK => "ok"
other => "status ${other}"
}
}Literals
Boolean, integer, character, and string literals match by equality.
fn dispatch(command: string): int {
return match command {
"start" => 1
"stop" => 2
"" => 0
_ => -1
}
}fn digit_class(c: char): string {
return match c {
'0' => "zero"
'a' => "letter a"
_ => "other"
}
}fn describe(flag: bool): string {
return match flag {
true => "on"
false => "off"
}
}Avoid floating-point literal patterns where rounding makes exact equality an unstable rule; compare with a tolerance instead.
Ranges
Inclusive a..=b and exclusive a..b range patterns match membership.
fn status_class(status: int): string {
return match status {
100..=199 => "informational"
200..=299 => "success"
300..=399 => "redirect"
400..=499 => "client error"
_ => "server error"
}
}Ranges work over characters too:
fn kind(c: char): string {
return match c {
'0'..='9' => "digit"
'a'..='z' => "lower"
'A'..='Z' => "upper"
_ => "other"
}
}Endpoints must be compile-time values of a compatible ordered type.
Tuples
A tuple pattern matches arity and destructures positions. The outer parentheses may be omitted where a comma is unambiguous — binding position and loop heads.
fn endpoint(): (string, int) {
return ("localhost", 8080)
}
fn port(): int {
(host, number) := endpoint()
return number + host.len()
}fn shipping(order: (int, bool)): string {
return match order {
(0, _) => "empty"
(count, true) => "${count} items, express"
(count, false) => "${count} items, standard"
}
}Structs and records
A struct pattern names the fields it inspects. Bare field binds a name equal
to the field name; field: subpattern renames or destructures further.
struct User { id: int, name: string, active: bool }
fn summarize(user: User): string {
User { id: user_id, name, active } := user
return "${user_id}/${name}/${active}"
}.. ignores the fields you did not list:
struct Request { method: string, path: string, body: string, token: string }
fn route(request: Request): string {
Request { method, path, .. } := request
return "${method} ${path}"
}Field subpatterns can be literals or ranges, which makes the whole pattern refutable and so only usable where failure has a meaning:
struct Event { code: int, source: string }
fn triage(event: Event): string {
return match event {
Event { code: 0, source } => "heartbeat from ${source}"
Event { code: 400..=499, .. } => "client fault"
Event { code, .. } => "code ${code}"
}
}Anonymous records use the same shape without a type name, and match structurally:
fn total(): int {
reading := { x: 3, y: 4 }
return match reading {
{ x, y } => x + y
}
}Variants
Enum, Option, Result, and declared error variants use constructor-shaped
patterns. Tuple-style variants bind positionally, struct-style variants bind by
field name, and unit variants have no payload.
enum Shape {
Circle(float)
Rect { width: float, height: float }
Empty
}
fn area(shape: Shape): float {
return match shape {
Circle(radius) => radius * radius * 3.14159
Rect { width, height } => width * height
Empty => 0.0
}
}Option and Result are ordinary variants:
error LoadError { NotFound }
fn load(id: int): int ! LoadError {
if id < 0 { error NotFound }
return id * 2
}
fn describe(id: int, cached: int?): string {
hit := match cached {
Some(value) => "cached ${value}"
None => "cold"
}
outcome := match load(id) {
Ok(value) => "loaded ${value}"
Err(err) => "failed"
}
return "${hit} / ${outcome}"
}Bind the error payload as err or e. error is a keyword and can never be an
identifier, so Err(error) does not parse.
Qualify a variant with its type when a bare name would be ambiguous, or simply for clarity:
enum NetworkState { Ready, Down }
fn connect(state: NetworkState): int {
return match state {
NetworkState.Ready => 1
NetworkState.Down => 0
}
}Nesting
Patterns compose to any depth, and the compiler checks coverage structurally.
enum Response {
Loaded(int?)
Failed(string)
}
fn describe(response: Response): string {
return match response {
Loaded(Some(value)) => "value ${value}"
Loaded(None) => "empty"
Failed(message) => "failed: ${message}"
}
}struct Address { city: string, zip: int }
struct Person { name: string, home: Address? }
fn city_of(person: Person): string {
return match person {
Person { home: Some(Address { city, .. }), .. } => city
Person { name, home: None } => "${name} has no address"
}
}Slices
Slice patterns match a sequence by length and position. ..rest absorbs the
remainder.
fn headline(values: []int): string {
return match values {
[] => "empty"
[only] => "single ${only}"
[first, second] => "pair ${first}/${second}"
[head, ..tail] => "head ${head} plus more"
_ => "unreachable"
}
}Slice patterns alone are not treated as covering a list, so keep a _ arm:
fn headline(values: []int): string {
return match values {
[] => "empty"
[only] => "single ${only}"
[head, ..tail] => "many"
}
}Bound patterns
name @ subpattern binds the whole matched value while still requiring the
subpattern — no reconstructing a value from its parts just to keep the original.
fn checked(value: int): int {
return match value {
digit @ 0..=9 => digit
big @ 1000..=9999 => big / 1000
_ => -1
}
}Alternatives
left | right matches either side. Every alternative should bind the same names
with compatible types, because the arm body sees one environment.
enum Event {
Created(int)
Updated(int)
Deleted(int)
}
fn affected(event: Event): int {
return match event {
Created(id) | Updated(id) => id
Deleted(id) => -id
}
}Alternatives combine with literals and ranges too:
fn weekend(day: string): bool {
return match day {
"sat" | "sun" => true
_ => false
}
}Type patterns
name: Type tests an anonymous union’s constituent and binds the refined value.
fn render(value: int | string | bool): string {
return match value {
n: int => "int ${n}"
s: string => "string ${s}"
b: bool => "bool ${b}"
}
}The binding has the refined type inside the arm, so n is a plain int there.
Guards
if condition after a pattern adds a boolean requirement. The pattern binds
first, then the guard runs; a false guard resumes matching at the next arm.
struct Order { total: int, expedited: bool }
fn fee(order: Order): int {
return match order {
Order { total, expedited: true } if total > 100 => 0
Order { total: _, expedited: true } => 15
Order { total, expedited: false } if total > 100 => 5
_ => 10
}
}A guard never proves a variant exhausted, because arbitrary boolean logic cannot be shown to cover every value. Keep one unguarded arm for the remainder.
Refutability
An irrefutable pattern always matches its input type: a plain binding, _,
a complete tuple destructure, a single-variant struct pattern. A refutable
pattern can fail: variants, literals, ranges, slices with a fixed length.
Refutable patterns are only allowed where failure has defined control flow:
| Context | On match | On failure |
|---|---|---|
| irrefutable binding | initialize names | not applicable |
Pattern := value else { } |
names stay in scope after the statement | run the diverging else |
if Pattern := value |
run the then-branch with the names | run the else-branch without them |
for Pattern := value |
run one iteration with the names | end the loop |
for Pattern in iterable |
bind the yielded element | element pattern must be irrefutable |
match arm |
evaluate the guard, then the body | try the next arm |
The same Some(value) shape therefore behaves differently in each of these:
fn lookup(id: int): int? {
if id < 0 { return None }
return Some(id)
}
fn four_ways(id: int, ids: []int): int {
mut total := 0
// 1. condition — bindings live in the then-branch only
if Some(value) := lookup(id) {
total += value
}
// 2. refutable binding — bindings live for the rest of the block
Some(base) := lookup(id) else {
return -1
}
total += base
// 3. loop head — failure ends the loop
mut cursor := 0
for Some(value) := lookup(cursor) {
total += value
cursor -= 1
}
// 4. match arm — failure tries the next arm
for candidate in ids {
total += match lookup(candidate) {
Some(value) => value
None => 0
}
}
return total
}Putting it together
enum Command {
Get { key: string }
Set { key: string, value: int }
Delete(string)
Batch([]string)
Ping
}
struct Reply { code: int, text: string }
fn execute(command: Command, store: Map[string, int]): Reply {
return match command {
Ping => Reply { code: 200, text: "pong" }
Get { key } => match store.get(key) {
Some(value) => Reply { code: 200, text: "${value}" }
None => Reply { code: 404, text: "missing ${key}" }
}
Set { key, value } if value < 0 =>
Reply { code: 400, text: "negative value for ${key}" }
Set { key, value } => Reply { code: 201, text: "${key}=${value}" }
Delete(key) => Reply { code: 204, text: key }
Batch([]) => Reply { code: 400, text: "empty batch" }
Batch([only]) => Reply { code: 200, text: "batch of one: ${only}" }
Batch(keys) => Reply { code: 200, text: "batch of ${keys.len()}" }
}
}Struct-payload variants, tuple payloads, guards, slice patterns, and a nested
Option match all coexist in one decision. Ordering matters: the guarded Set
arm has to precede the unguarded one, and the general Batch(keys) arm has to
come last.