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.

Rule: Use 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)
}
Rule: A guarded branch does not fully cover its variant because the guard may evaluate to 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.

Rule: 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

  • match inspects 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.
  • Result errors 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 where and must produce bool.
  • 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.
  • match may 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.