Match
match performs exhaustive structural and variant-based branching.
It is the Sec construct for selecting among the semantic states of unions,
Result, Option, enums and related closed or explicitly
open variant domains.
match result {
Ok(value) => {
Process(value)
}
Err(error) => {
Handle(error)
}
}
Unlike switch, match can bind variant payloads and
participate directly in ownership and borrowing. Unlike if, it
does not require the subject itself to be a boolean condition.
match is exhaustive. Every reachable
semantic state of the subject must be covered by the arm set or by a valid
unguarded catch-all.
When to Use Match
Use match when the branching decision is structural:
- a union variant must be distinguished;
- a
Result[T, E]must handle both success and failure explicitly; - an
Option[T]must distinguishSomefromNone; - an enum domain should be exhaustively handled;
- a variant payload must be bound or borrowed;
- a struct-like union payload needs shallow named-field destructuring;
- a reachable union
emptyinitialization state must be handled; - branch selection itself should produce a value.
Use other constructs when the problem is not structural:
| Construct | Use it for |
|---|---|
if |
A boolean condition |
switch |
Literal, range, relational or ordinary value selection |
match |
Structural, variant and exhaustive branch selection |
Syntax
Statement form
match result {
Ok(value) => {
Process(value)
}
Err(error) => {
Handle(error)
}
}
Expression form
let message := match status {
Status.Ready => "ready"
Status.Waiting => "waiting"
Status.Failed => "failed"
}
Guarded arm
match option {
Some(value) where value > 100 => Large(value)
Some(value) => Normal(value)
None => Missing()
}
A match must contain at least one arm.
Each arm consists of a pattern, an optional where guard,
=>, and an arm body.
Subject Evaluation
The subject expression is evaluated exactly once.
match ReadResult() {
Ok(value) => Process(value)
Err(error) => Handle(error)
}
ReadResult() executes once. The resulting subject value or
subject Place is then used for all arm tests, guards, bindings, ownership
operations and final flow-state merging.
Backend lowering must not reevaluate the source expression separately for different arms.
Arm Order
Arms are considered in source order.
For each arm:
- the pattern is tested;
- candidate bindings become visible to the guard;
- the guard, when present, is evaluated;
- if the guard is false, matching continues with the next arm;
- if the guard succeeds, the arm is selected;
- ownership effects for the selected arm are committed;
- the arm body executes.
Exactly one arm executes. No later pattern or guard is evaluated after an arm has been selected.
Exhaustiveness
Every reachable semantic state of the subject must be handled.
match option {
Some(value) => Use(value)
None => Missing()
}
This is exhaustive for Option[T].
Coverage is based on the resolved subject type and proven control-flow state, not merely on the textual patterns that happen to appear in the source.
Guards do not provide complete coverage by themselves
match option {
Some(value) where value > 0 => Positive(value)
Some(value) => Other(value)
None => Missing()
}
This is exhaustive.
This is not:
match option {
Some(value) where value > 0 => Positive(value)
None => Missing()
}
A Some value for which the guard is false remains uncovered.
Catch-All Pattern
The catch-all pattern is:
_
It matches every remaining reachable state not handled by an earlier selected arm.
match direction {
Direction.North => North()
_ => Other()
}
An unguarded catch-all must be the final reachable arm.
Invalid:
match direction {
_ => Other()
Direction.North => North()
}
The second arm is unreachable.
Guarded catch-all
match value {
_ where condition => Handle()
_ => Fallback()
}
A guarded catch-all is not exhaustive by itself because its guard may fail.
Variant Patterns
A payload-less variant is matched by its resolved variant name.
match direction {
Direction.North => North()
Direction.South => South()
_ => Other()
}
Normal namespace and name-resolution rules apply.
Variant identity is semantic. The compiler does not infer that two variant spellings are related merely because their text or physical representation is similar.
Whole-Payload Binding
A variant carrying one payload may bind the complete payload:
match result {
Ok(value) => Process(value)
Err(error) => Handle(error)
}
Struct-like union variants may likewise bind their complete payload objects when the union defines that form:
match shape {
Circle(circle) => Process(circle.Radius)
Rectangle(rectangle) => Process(rectangle.Width, rectangle.Height)
}
The binding exists only inside the selected arm and its guard.
Shallow Named-Field Destructuring
Struct-like union payloads support shallow named-field destructuring.
match shape {
Rectangle { width, height } => Use(width, height)
Circle { radius } => UseRadius(radius)
}
Partial field binding requires no additional marker:
Rectangle { width } => Use(width)
Omitted fields:
- are not bound;
- do not constrain the match beyond variant selection;
- are not copied or moved merely because they were omitted.
Renaming
Rectangle { width: w, height: h } => Use(w, h)
Borrowing fields
Rectangle { width: ref w, height: ref h } => Inspect(w, h)
Rectangle { width: ref mut w } => Modify(w)
Field destructuring is shallow in Sec 0.1.
This nested recursive pattern is not part of the current pattern model:
Some(Ok(value)) => Use(value)
Use another match for the nested value.
The empty Union State
A mutable non-defaultable union binding may have the compiler-known
initialization state empty according to the union rules.
match state {
empty => Initialize()
Idle => Wait()
Running => Work()
}
empty is not:
- a union variant;
- a value of the union type;
None;Err;null;- a source-visible numeric tag.
If control-flow analysis proves that the subject is initialized,
empty is not part of exhaustiveness.
If empty remains reachable, exhaustive coverage must include it
or a valid unguarded catch-all.
A moved-from union is not the same state as empty.
Pattern Binding Scope
Pattern bindings exist only inside their own arm.
They are visible:
- in that arm's guard;
- in that arm's body.
They are not visible:
- in another arm;
- after the
match.
Normal Sec no-shadowing rules apply.
let value := 10
match result {
Ok(value) => Process(value)
Err(error) => Handle(error)
}
This is invalid because the pattern binding would shadow the existing
value.
Ownership in Whole-Payload Patterns
Pattern bindings follow the ordinary Sec ownership and borrowing model.
Plain binding
Some(value)
For payload type T:
- if
Tis implicitly copyable,valuereceives a copy; - if
Tis move-only and the subject is an owned reusable Place that may transfer ownership,valuereceives the payload by move; - if the subject is a fresh temporary, the payload may be forwarded directly into the binding;
- the compiler never silently clones a payload;
- the compiler never silently changes a by-value binding into a borrow.
Shared borrow
Some(ref value)
value has type ref T.
Mutable borrow
Some(ref mut value)
value has type ref mut T, and the subject must
provide mutable borrowing authority.
Pattern borrows are arm-scoped and follow normal overlap and escape rules.
Borrowed Subjects Cannot Transfer Ownership
A subject available only through ref UnionType does not own
the union and cannot transfer a move-only payload out of it.
A plain by-value binding from a shared borrowed subject is valid only when the payload can be copied.
For move-only payload access, borrow explicitly:
Some(ref value)
A ref mut UnionType subject may provide:
Some(ref value)
Some(ref mut value)
Mutable borrowing authority still does not grant ownership authority. A move-only payload cannot be moved out merely because the subject is mutably borrowed.
Ownership in Field Destructuring
Shallow named-field destructuring does not create hidden partial moves.
Packet { header, payload }
A plain field binding is by-value copy only. It is valid only when that field's type is implicitly copyable.
If a field is move-only, borrow it:
Packet { payload: ref p }
or:
Packet { payload: ref mut p }
If ownership of move-only content is required, bind the complete payload by value and perform ownership operations on that complete payload value.
Guards
An arm may add a boolean guard with where:
match option {
Some(value) where value > 100 => Large(value)
Some(value) => Normal(value)
None => Missing()
}
The guard:
- is evaluated only after the pattern matches;
- must have type
bool; - may use bindings introduced by the pattern;
- continues matching with the next arm when it evaluates to
false.
where is the guard syntax for match.
Guards and Move-Only Payloads
A move-only by-value payload is not considered moved merely because its pattern matched.
match option {
Some(value) where IsUsable(ref value) => Consume(value)
Some(value) => HandleOther(value)
None => Missing()
}
For the first arm, ownership transfer commits only after:
- the pattern matches;
- the guard evaluates to
true; - the arm is selected.
Before selection, the guard may inspect or borrow the prospective binding. It may not consume that prospective move-only value.
If the guard is false, no payload move has been committed by that arm. Later arms continue with the ownership state that actually exists after guard evaluation.
Candidate ref and ref mut borrows from a rejected
guarded arm end before the next arm is tested, subject to ordinary side effects.
Statement Match
A match may be used only for control flow and side effects:
match result {
Ok(value) => Process(value)
Err(error) => Handle(error)
}
Expression-shaped arm bodies in statement context are evaluated under normal statement/discard rules.
match does not create a special rule that allows must-use values
to disappear.
match option {
Some(value) => {
discard Calculate(value)
}
None => {}
}
Explicit discard remains subject to the ordinary discardability, ownership and destruction rules.
Expression Match
A match may produce a value.
let message := match status {
Status.Ready => "ready"
Status.Waiting => "waiting"
Status.Failed => "failed"
}
Every continuing arm must produce a value assignable to one common result type.
An arm that returns, propagates an error or otherwise terminates does not need to produce the match result:
let value := match result {
Ok(value) => value
Err(error) => return Err(error)
}
Ownership of the resulting expression follows the resolved result type and the ownership action that produced each continuing arm result.
Value-Producing Arm Blocks
Expression-match arms may use a contextual value-producing block.
let price := match product {
Book(book) => {
LogBook(book)
CalculateBookPrice(book)
}
Service(service) => service.Price
}
The final expression on every continuing path through the block is the arm's result.
This is contextual syntax specifically for an expression-match arm. It does not make arbitrary Sec blocks into general block expressions.
Terminating paths
let result := match value {
A(data) => {
if Invalid(data) {
return Err(Error.InvalidData)
}
Transform(data)
}
B => DefaultValue()
}
The return path terminates. The continuing path produces
Transform(data).
This is invalid when Log(data) returns void:
let result := match value {
A(data) => {
Log(data)
}
B => DefaultValue()
}
The continuing A arm does not produce the required result value.
Match Result Type Compatibility
All continuing value-producing arms must have compatible result types under the normal Sec type system.
match does not introduce its own implicit conversion rules.
let value := match option {
Some(number) => number
None => "missing"
}
This is invalid when number has type int.
Named type identity remains significant:
type CustomerID uint64
type ProductID uint64
A match expression cannot merge CustomerID and
ProductID results merely because their representation is compatible.
Matching Result
Result[T, E] uses compiler-known Ok and
Err variants.
match result {
Ok(value) => Success(value)
Err(error) => Failure(error)
}
General match does not propagate Err implicitly.
Both success and error states belong to the explicit branch structure.
Use try when the normal successful path should continue and
failure should be propagated or handled through try semantics.
Use match when both Ok and Err are
explicit branches of the program's logic.
Catch-all cannot hide an error
This is invalid when _ would absorb an unhandled Err:
match result {
Ok(value) => Process(value)
_ => Ignore()
}
Handle the error variant explicitly:
match result {
Ok(value) => Process(value)
Err(error) => Handle(error)
}
An explicit Err(_) handles the error variant while discarding
its payload:
match result {
Err(_) => Fallback()
Ok(value) => Process(value)
}
This is different from the general catch-all _.
Matching Option
Option[T] uses compiler-known Some and
None variants.
match option {
Some(value) => Use(value)
None => Missing()
}
None is a real Option[T] value.
It is not the union initialization state empty.
Whole-payload binding of Some follows the copy, move and
borrow rules described earlier in this chapter.
Open Error Narrowing
Result[T, error] preserves the concrete identity and payload
of widened Sec errors.
Its Err branch may therefore use error-specific concrete patterns:
match result {
Ok(value) => Use(value)
Err(IOError.NotFound) => UseDefault()
Err(errorValue) => Handle(errorValue)
}
The root error domain is open. Concrete error-specific arms
alone are not exhaustive.
An Err(errorValue) or Err(_) fallback is required
unless control-flow facts prove a narrower closed state.
The generic catch-all _ still may not silently hide an unhandled
Err.
Ordinary Enums
Ordinary enums are closed over their distinct declared underlying value classes.
match status {
Status.Ready => Ready()
Status.Waiting => Wait()
Status.Failed => Fail()
}
Exhaustiveness is based on semantic enum values, not merely on the number of member spellings.
If two enum members are aliases for the same underlying value, they represent one runtime value class for coverage and duplicate/unreachable analysis.
A match therefore does not need one arm for every alias spelling.
Open Bit-Backed Enums
A bit or bit[N] hardware enum is open over the
complete representable bit domain.
Declared members do not necessarily cover every representable encoding.
match status {
Status.Ready => Ready()
Status.Error => Error()
_ => UnknownEncoding()
}
A catch-all is required unless the declared members provably cover the complete bit domain.
Exhaustiveness is based on the actual bit width and normalized numeric value classes.
Pattern Forms Not Used by Match
Sec's match deliberately has a focused pattern model.
The following are not match forms in Sec 0.1:
- pattern alternatives such as
A | B; - general literal patterns;
- range patterns;
- direct
true/falsepatterns; - runtime type patterns;
- ordinary struct-subject destructuring;
- recursive nested patterns;
- arbitrary user-defined pattern protocols;
- regex patterns;
- guard syntax other than
where.
No direct boolean matching
This is not the Sec form for a plain boolean:
match ready {
true => Start()
false => Wait()
}
Use:
if ready {
Start()
} else {
Wait()
}
No literal or range matching
Do not use match for this:
match value {
1 => One()
2 => Two()
_ => Other()
}
Use switch.
Likewise, use switch ranges rather than range patterns:
switch value {
case 0..<10:
Small()
default:
Other()
}
Switch vs Match
switch and match solve different problems.
| Need | Use |
|---|---|
| Compare one value with literals or runtime expressions | switch |
| Test ranges or relational conditions | switch |
| Branch on a union or variant | match |
| Bind a variant payload | match |
| Require exhaustive handling | match |
| Produce a value from the branch construct | match |
| Inspect one boolean condition | if |
switch. If you are asking "which semantic variant or state?",
use match.
Flow and Ownership Analysis
match participates in ordinary path-sensitive flow analysis.
Return analysis
fn Convert(result: Result[int, IOError]) int {
match result {
Ok(value) => {
return value
}
Err(error) => {
discard error
return 0
}
}
}
An exhaustive match whose every arm terminates is itself non-continuing.
Definite assignment
fn Convert(option: Option[int]) int {
let mut result: int
match option {
Some(value) => {
result = value
}
None => {
result = 0
}
}
return result
}
Because every continuing arm assigns result, it is definitely
initialized after the match.
Ownership after match
match value {
Some(resource) => Consume(<-resource)
None => Nothing()
}
Use(value)
If resource is move-only and the Some path
continues after consuming it, the original union subject is not definitely
available after the match.
Post-match ownership state is merged conservatively across all continuing arms.
A terminating arm does not contribute a state to a later merge point because that path does not continue.
Duplicate and unreachable arms
Duplicate unguarded patterns are invalid:
match option {
None => A()
None => B()
Some(value) => C(value)
}
A pattern fully covered by an earlier unguarded arm is also unreachable:
match option {
Some(value) => A(value)
Some(value) where value > 10 => B(value)
None => C()
}
The guarded Some arm can never be reached.
The guarded-specific-first ordering is valid:
match option {
Some(value) where value > 10 => Large(value)
Some(value) => Other(value)
None => Missing()
}
No fallthrough
Match arms never fall through. There is no fallthrough statement
for match.
match is also not a break or continue target.
Those statements retain their enclosing-loop semantics when used legally inside an arm.
Diagnostics
Match diagnostics should describe the semantic problem rather than merely report that parsing or lowering failed.
Important diagnostic categories include:
- empty match;
- invalid subject type;
- unknown variant;
- invalid pattern for the subject type;
- duplicate pattern;
- unreachable arm;
- non-exhaustive match;
- uncovered reachable
emptystate; - uncovered open bit-enum encodings;
- non-boolean guard;
- shadowing pattern binding;
- catch-all hiding an unhandled
Err; - invalid
reforref mutbinding; - attempted move from a borrowed subject;
- move-only field bound by value through shallow destructuring;
- consuming use of a prospective move-only guard binding;
- post-match use of a moved or conditionally unavailable subject;
- incompatible expression-arm result types;
- an expression arm that fails to produce a value on a continuing path;
- a must-use arm result that would be implicitly discarded.
Ownership diagnostics should identify the source Place, the arm that moved or borrowed it, and the later invalid use when those locations are available.
Summary
matchperforms structural and variant-based branching.- The subject is evaluated exactly once.
- Arms and guards are considered in source order.
- Exactly one arm executes.
matchis exhaustive.- A guarded arm does not by itself exhaust its underlying pattern.
_is the catch-all pattern.- An unguarded catch-all must be the final reachable arm.
- A generic catch-all may not silently hide an unhandled
Err. Err(_)explicitly handles and discards an error payload.- Variant payloads may be bound as complete values.
- Struct-like union payloads support shallow named-field destructuring.
- Field bindings may be renamed or explicitly borrowed.
- Recursive nested patterns are not part of the current Sec 0.1 pattern model.
emptyis a compiler-known union initialization state, not a variant.- Pattern bindings are visible only in their arm guard and body.
- Plain whole-payload binding copies copyable payloads and may move owned move-only payloads.
refandref mutprovide explicit pattern borrowing.- Borrowed subjects cannot transfer ownership they do not possess.
- Shallow field destructuring never performs hidden move-only partial moves.
- Guards use
whereand must producebool. - Move-only transfer from a guarded arm commits only after the guard succeeds and the arm is selected.
- Statement matches obey ordinary discard and must-use rules.
- Expression matches require one common result type across continuing arms.
- An expression-match arm block may produce its value from its final expression.
- Ordinary enums are exhaustive over distinct declared semantic value classes.
- Open bit-backed enums may require a catch-all for undeclared encodings.
ResultandOptionuse the same match machinery with compiler-known variants.Result[T, error]supports error-specific narrowing while preserving the open error domain.- Plain booleans, literals and ranges belong to
iforswitch, notmatch. matchhas no fallthrough.- Duplicate and unreachable arms are compile-time errors.
- Ownership, borrowing, definite assignment and availability are merged path-sensitively after the match.