Match Expressions and Statements
match selects one branch by inspecting the variant, shape or finite value represented by
its subject. It is primarily used with union types, Result[T, E], Option[T]
and enums.
Unlike switch, match does not merely compare a subject with values. A match
branch may identify a variant, bind the value carried by that variant and make the binding available
inside the selected branch.
A match must be exhaustive. Every possible value represented by the subject type must be handled by an explicit branch or by an allowed catch-all pattern.
match for variants, payload binding and exhaustive branching.
Use switch for values, ranges and relational comparisons. Use if for one
boolean condition.
When to Use match
match is the normal choice when code must inspect:
- a
Result[T, E]success or error variant, - an
Option[T]value or absence, - a tagged union variant,
- an enum value that must be handled exhaustively,
- or a variant payload that must be bound to a local name.
match ReadFile(path) {
Ok(data) => Process(data)
Err(error) => Handle(error)
}
In this example, match identifies whether the result contains Ok or
Err and binds the associated payload.
Basic Syntax
A match contains a subject expression followed by one or more branches:
match expression {
Pattern => branch
Pattern => branch
}
A branch may contain a single expression:
let message := match status {
Status.ready => "ready"
Status.waiting => "waiting"
Status.failed => "failed"
}
Or a statement block:
match result {
Ok(value) => {
LogSuccess(value)
Process(value)
}
Err(error) => {
LogError(error)
Handle(error)
}
}
The arrow => separates the pattern from the branch body. Switch-style
case and default clauses are not used in a match.
Subject Evaluation
The match subject is evaluated exactly once.
match ReadResult() {
Ok(value) => Process(value)
Err(error) => Handle(error)
}
ReadResult() is called once. The returned value is then inspected by the match.
This remains true even when the subject expression performs work or has visible side effects.
Branch Order
Patterns are considered from top to bottom. The first pattern whose variant and optional guard match is selected. Exactly one branch executes.
After a branch has been selected, no later pattern or guard is evaluated. Branch order therefore matters when several guarded branches refer to the same variant.
match option {
Some(value) where value > 100 => Large(value)
Some(value) => Normal(value)
None => Missing()
}
Variant Patterns
A variant without a payload is matched by its variant name. A variant that carries data uses parentheses to bind or discard that payload.
match direction {
Direction.north => MoveNorth()
Direction.east => MoveEast()
Direction.south => MoveSouth()
Direction.west => MoveWest()
}
match result {
Ok(value) => Process(value)
Err(error) => Handle(error)
}
The pattern must refer to a valid variant of the subject type. Unknown variants are compile-time errors.
Payload Binding
A variant carrying data may bind that data to a new immutable local symbol.
match result {
Ok(value) => Use(value)
Err(error) => Report(error)
}
For Result[int, IOError], value has type int and
error has type IOError. Their types are inferred from the matched variant.
Tagged union payloads are initially bound as one value rather than destructured field by field.
type Shape union {
Circle {
Radius: decimal,
}
Rectangle {
Width: decimal,
Height: decimal,
}
}
let area := match shape {
Circle(circle) => circle.Radius * circle.Radius
Rectangle(rectangle) => rectangle.Width * rectangle.Height
}
Discarded Payloads
Use _ inside a variant pattern when the variant matters but its payload does not.
match result {
Ok(_) => Success()
Err(error) => Handle(error)
}
A discarded payload does not declare a variable and cannot be referenced in the branch.
Catch-all Pattern
A standalone _ pattern matches every value not handled by an earlier branch.
match direction {
Direction.north => MoveNorth()
_ => MoveOtherDirection()
}
The catch-all pattern must be the final branch.
match direction {
_ => MoveOtherDirection()
Direction.north => MoveNorth()
} // Error: unreachable match branch after catch-all pattern.
A catch-all may not silently hide the Err variant of a Result.
match result {
Ok(value) => Process(value)
_ => Ignore()
} // Error: catch-all pattern may not hide Err.
Result errors must be handled explicitly. This prevents error paths from disappearing behind a general fallback branch.
Guards
A branch may add a boolean condition using where.
match option {
Some(value) where value > 100 => Large(value)
Some(value) => Normal(value)
None => Missing()
}
The pattern is checked first. The guard is evaluated only when the pattern matches. The branch is selected only when both the pattern and guard match.
The guard expression must have type bool.
match option {
Some(value) where value => Process(value)
None => Missing()
} // Error when value has type int: match guard must be bool, got int.
Pattern bindings are visible inside the guard:
match result {
Ok(value) where value > 0 => Positive(value)
Ok(value) => Other(value)
Err(error) => Handle(error)
}
false. An unguarded fallback for that variant is still required.
Binding Scope
Pattern bindings exist only inside the branch in which they are declared.
match result {
Ok(value) => {
Process(value)
}
Err(error) => {
Handle(error)
}
}
value is not visible in the Err branch or after the match. Likewise,
error is not visible in the Ok branch or after the match.
Normal Sec no-shadowing rules apply:
let value: int := 10
match result {
Ok(value) => Process(value)
Err(error) => Handle(error)
} // Error: variable "value" already declared.
Matching Result
match is the general explicit mechanism for inspecting Result[T, E].
Both Ok and Err must be handled.
match ReadFile(path) {
Ok(data) => Process(data)
Err(error) => Handle(error)
}
An incomplete Result match is invalid:
match ReadFile(path) {
Ok(data) => Process(data)
} // Error: non-exhaustive match: missing Err.
Use try when success should continue normally and errors should propagate. Use
match when both success and error paths should be handled explicitly at the current
location.
Matching Option
Option[T] distinguishes between a present value and absence.
let name := match optionalName {
Some(value) => value
None => "unknown"
}
Both Some and None must be handled unless a valid catch-all branch covers
the remaining variants.
match optionalName {
Some(value) => Print(value)
} // Error: non-exhaustive match: missing None.
Matching Enums
Enum matching is useful when every declared enum value must be handled explicitly.
match direction {
Direction.north => MoveNorth()
Direction.east => MoveEast()
Direction.south => MoveSouth()
Direction.west => MoveWest()
}
Omitting an enum value without a catch-all branch makes the match non-exhaustive.
Use switch instead when the intent is ordinary value comparison and exhaustive enum
handling is not required.
Matching Union Variants
A tagged union may represent several alternatives with different payloads. A match handles each variant and binds the selected payload.
type Shape union {
Circle {
Radius: decimal,
}
Rectangle {
Width: decimal,
Height: decimal,
}
}
let area := match shape {
Circle(circle) => circle.Radius * circle.Radius
Rectangle(rectangle) => rectangle.Width * rectangle.Height
}
Every union variant must be handled unless an allowed catch-all branch covers the remaining variants.
Exhaustiveness
A match must cover every possible value of its subject type. Exhaustiveness is checked at compile time.
match option {
Some(value) => Process(value)
None => Missing()
}
Guarded branches are tracked separately from unguarded coverage. The following is not exhaustive:
match option {
Some(value) where value > 0 => Positive(value)
None => Missing()
} // Error: Some values where value <= 0 are not handled.
Add an unguarded fallback for the same variant:
match option {
Some(value) where value > 0 => Positive(value)
Some(value) => Other(value)
None => Missing()
}
Exhaustiveness allows later compiler analysis to know that a valid subject always reaches one branch.
match as a Statement
A match may be used only to control execution. In that form, branch expression values are ignored.
match result {
Ok(value) => {
Process(value)
}
Err(error) => {
Handle(error)
}
}
Statement match is appropriate when each branch performs actions rather than producing one shared value.
match as an Expression
A match may produce a value. The selected branch becomes the value of the entire match expression.
let message := match status {
Status.ready => "ready"
Status.waiting => "waiting"
Status.failed => "failed"
}
Every branch that can continue must produce a compatible value. A branch that returns or otherwise terminates the surrounding control flow does not need to produce the match result.
Branch Result Types
When match is used as an expression, the compiler determines one common result type from all continuing branches.
let number := match option {
Some(value) => value
None => 0
}
Incompatible branch types are rejected:
let value := match option {
Some(number) => number
None => "missing"
} // Error: match branches have incompatible types int and string.
Named semantic types retain their identity. For example, Speed and Distance
do not become compatible merely because both use the same underlying numeric representation.
Terminating Branches
A branch that returns, propagates an error or otherwise terminates does not have to produce the value of an expression match.
let value := match result {
Ok(value) => value
Err(error) => return Handle(error)
}
Only the Ok branch contributes to the match result. The Err branch exits the
surrounding function.
Return Analysis
An exhaustive statement match participates in function return analysis.
fn Convert(result: Result[int, IOError]) int {
match result {
Ok(value) => {
return value
}
Err(error) => {
return 0
}
}
}
The function is valid because the match is exhaustive and every branch returns.
fn Convert(result: Result[int, IOError]) int {
match result {
Ok(value) => {
return value
}
Err(error) => {
Log(error)
}
}
} // Error: the function may reach its end without returning int.
Definite Assignment
An exhaustive match may establish that a mutable variable has been assigned before later use.
fn Convert(option: Option[int]) int {
let mut result: int
match option {
Some(value) => {
result = value
}
None => {
result = 0
}
}
return result
}
Every continuing branch assigns result, so it is definitely initialized after the
match.
Branches that terminate do not need to assign it:
fn Convert(result: Result[int, IOError]) int {
let mut value: int
match result {
Ok(resultValue) => {
value = resultValue
}
Err(error) => {
return 0
}
}
return value
}
Duplicate and Unreachable Branches
Duplicate unguarded patterns are invalid:
match option {
None => A()
None => B()
Some(value) => C(value)
} // Error: duplicate match pattern None.
A branch fully covered by an earlier unguarded branch is unreachable.
match option {
Some(value) => A(value)
Some(value) where value > 10 => B(value)
None => C()
} // Error: Some is already fully covered.
The correct order places guarded special cases first:
match option {
Some(value) where value > 10 => B(value)
Some(value) => A(value)
None => C()
}
No Fallthrough
Match branches never fall through into later branches. There is no fallthrough
statement for match.
match result {
Ok(value) => {
Process(value)
fallthrough
}
Err(error) => Handle(error)
} // Error: fallthrough is not valid in match.
A branch ends naturally when its expression or block completes. break does not end a
match branch; it remains loop control.
continue may appear inside a match branch when the match itself is inside a loop. It
affects the nearest enclosing loop.
for result in results {
match result {
Ok(value) => Process(value)
Err(error) => continue
}
}
Ownership and Borrowing
Pattern bindings follow the normal Sec ownership and borrowing rules. Matching must not silently clone, allocate or copy values that are not normally copyable.
Copyable enum values and copyable union payloads may be bound by value. Non-copyable payloads must eventually be rejected or bound through explicit borrowing rules once the ownership model for such patterns is finalized.
match never introduces hidden cloning or allocation.
match, switch and if
Use if for one boolean decision
if rawResult < 0 {
return Err(_decodeError(-rawResult))
}
Use switch for values, ranges and relations
switch statusCode {
case 200:
Success()
case 400..<500:
ClientError()
default:
Other()
}
Use match for variants and payloads
match result {
Ok(value) => Success(value)
Err(error) => Failure(error)
}
Literal and range matching belongs primarily to switch. Variant inspection and
exhaustive payload handling belongs to match.
Not Supported Initially
The initial match design does not include:
- pattern alternatives such as
A | B, - literal and range patterns as a replacement for
switch, - runtime type patterns,
- direct struct-field destructuring,
- nested patterns such as
Some(Ok(value)), - or hidden copying of non-copyable payloads.
Write separate branches instead of pattern alternatives:
match direction {
Direction.north => Vertical()
Direction.south => Vertical()
Direction.east => Horizontal()
Direction.west => Horizontal()
}
Use a nested match instead of a nested pattern:
match option {
Some(result) => {
match result {
Ok(value) => Process(value)
Err(error) => Handle(error)
}
}
None => Missing()
}
Common Errors
Missing a variant
match result {
Ok(value) => Process(value)
}
Result matches must explicitly handle Err.
Using a non-boolean guard
match option {
Some(value) where value => Process(value)
None => Missing()
}
A guard must produce bool.
Putting a branch after catch-all
match direction {
_ => Other()
Direction.north => North()
}
Every branch after an unguarded catch-all is unreachable.
Mixing incompatible expression types
let value := match option {
Some(number) => number
None => "missing"
}
All continuing branches of an expression match must produce compatible types.
Using switch syntax inside match
match result {
case Ok(value):
Process(value)
}
Match branches use patterns followed by =>, not case and a colon.
Summary of Rules
matchinspects variants, finite values and variant payloads.- The subject expression is evaluated exactly once.
- Branches are considered from top to bottom.
- The first matching pattern with a true guard executes.
- Exactly one branch executes.
- A match must be exhaustive.
Resulterrors may not be hidden by a catch-all pattern.- Payload bindings are immutable and local to their branch.
- Normal Sec no-shadowing rules apply.
- Guards use
whereand must producebool. - A guarded pattern does not fully cover its variant.
- Duplicate and unreachable branches are compile-time errors.
- There is no implicit or explicit fallthrough between match branches.
matchmay be used as a statement or as an expression.- Expression branches must produce compatible result types.
- Terminating branches do not need to produce an expression value.
- Literal and range comparisons belong primarily to
switch. - Matching follows normal ownership and borrowing rules.
- Matching does not introduce hidden copying, cloning or allocation.