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.

Core rule: 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 distinguish Some from None;
  • 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 empty initialization 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:

  1. the pattern is tested;
  2. candidate bindings become visible to the guard;
  3. the guard, when present, is evaluated;
  4. if the guard is false, matching continues with the next arm;
  5. if the guard succeeds, the arm is selected;
  6. ownership effects for the selected arm are committed;
  7. the arm body executes.

Exactly one arm executes. No later pattern or guard is evaluated after an arm has been selected.

Rule: Guarded matching is ordered. A guard that rejects one candidate arm does not prevent a later arm from matching the same variant.

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 T is implicitly copyable, value receives a copy;
  • if T is move-only and the subject is an owned reusable Place that may transfer ownership, value receives 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.

Rule: Shallow field destructuring never leaves a reusable union subject in a hidden partially moved state.

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:

  1. the pattern matches;
  2. the guard evaluates to true;
  3. 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.

Boundary: Error-specific narrowing is part of the Sec error model. It is not a general runtime type-pattern facility.

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 / false patterns;
  • 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
Rule of thumb: If you are asking "which value or interval?", use 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 empty state;
  • uncovered open bit-enum encodings;
  • non-boolean guard;
  • shadowing pattern binding;
  • catch-all hiding an unhandled Err;
  • invalid ref or ref mut binding;
  • 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

  • match performs structural and variant-based branching.
  • The subject is evaluated exactly once.
  • Arms and guards are considered in source order.
  • Exactly one arm executes.
  • match is 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.
  • empty is 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.
  • ref and ref mut provide explicit pattern borrowing.
  • Borrowed subjects cannot transfer ownership they do not possess.
  • Shallow field destructuring never performs hidden move-only partial moves.
  • Guards use where and must produce bool.
  • 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.
  • Result and Option use 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 if or switch, not match.
  • match has no fallthrough.
  • Duplicate and unreachable arms are compile-time errors.
  • Ownership, borrowing, definite assignment and availability are merged path-sensitively after the match.