Match Expressions

Match Expressions

Match expressions provide pattern matching for clean, exhaustive branching.

Basic Syntax

match value {
    pattern1 => result1,
    pattern2 => result2,
    _ => default_result
}

The _ is a wildcard that matches any value not handled by previous arms.

Matching Integers

function describe(x: i32) i32 {
    return match x {
        1 => 100,
        2 => 200,
        3 => 300,
        _ => 0
    };
}
 
function main() i32 {
    return describe(2);  // Returns 200
}

Match as Expression

Match is an expression, so it returns a value that can be used directly:

function main() i32 {
    var x = 2;
    var result = match x {
        1 => 10,
        2 => 20,
        _ => 0
    };
    return result;  // 20
}

You can use match anywhere an expression is expected:

function main() i32 {
    var x = 1;
    var y = 10 + match x {
        1 => 5,
        _ => 0
    };
    return y;  // 15
}

Block Bodies

Use braces for multi-statement arm bodies:

function main() i32 {
    var x = 2;
    match x {
        1 => {
            var a = 10;
            return a + 5;
        },
        2 => {
            var b = 20;
            return b + 5;
        },
        _ => {
            return 0;
        }
    };
    return 0;
}

Matching Booleans

function main() i32 {
    var flag = true;
    return match flag {
        true => 1,
        false => 0,
        _ => 99
    };
}

Matching Enums

Match works naturally with enum types:

enum Color { Red, Green, Blue }
 
function to_code(c: Color) i32 {
    return match c {
        Color.Red => 1,
        Color.Green => 2,
        Color.Blue => 3,
        _ => 0
    };
}
 
function main() i32 {
    var c: Color = Color.Green;
    return to_code(c);  // 2
}

Match as Statement

Use match as a statement for side effects:

function main() i32 {
    var x = 2;
    var result = 0;
    match x {
        1 => { result = 10; },
        2 => { result = 20; },
        _ => { result = 99; }
    };
    return result;  // 20
}

Return from Match Arms

You can return from the enclosing function inside a match arm:

function process(x: i32) i32 {
    match x {
        1 => { return 10; },
        2 => { return 20; },
        _ => { return 99; }
    };
    return 0;  // Never reached if all cases return
}

Nested Match

Match expressions can be nested:

function main() i32 {
    var x = 1;
    var y = 2;
    return match x {
        1 => match y {
            1 => 11,
            2 => 12,
            _ => 10
        },
        2 => 20,
        _ => 0
    };  // Returns 12
}

No Match Fallthrough

If no arm matches and there's no wildcard _, execution continues after the match:

function main() i32 {
    var x = 99;
    match x {
        1 => { return 10; },
        2 => { return 20; }
    };
    return 0;  // Reached when x is neither 1 nor 2
}
⚠️

Without a wildcard arm, unmatched values will fall through silently. Consider always including a _ arm for safety.

Trailing Commas

Trailing commas are allowed:

match x {
    1 => 10,
    2 => 20,
    _ => 0,
}

Different Integer Sizes

Match handles different integer sizes (i32, i64, etc.):

function main() i32 {
    var x: i64 = 2;
    var result: i64 = match x {
        1 => 100,
        2 => 200,
        _ => 0
    };
    return result;  // 200
}

Destructuring Enum Payloads

Matching a payload-carrying enum destructures the payload into fresh bindings; _ skips a position:

enum Shape { Circle(f64), Rect(f64, f64), Empty }
 
function describe(s: ref Shape) f64 {
    return match s {
        Shape.Circle(r) => r,
        Shape.Rect(w, _) => w,
        Shape.Empty => 0.0
    };
}

A match borrows payloads in place when no reachable arm takes ownership of one, so inspection does not consume the enum or require an explicit reference. If any reachable arm moves a compound payload, the match consumes the owned enum: the selected arm owns its bindings and drops remaining payloads at arm exit. A match result stays alive until its receiving owner releases it. Matching a ref or const ref enum always borrows and rejects payload moves. See Ownership and Drops.

A non-enum match producing an owned value needs a wildcard, or both boolean patterns, so every input produces a valid result.

Only the first matching arm runs; there is no fallthrough. A wildcard handles values not covered by preceding arms.

Enum matches are exhaustive: cover every variant or add a _ arm. Duplicate arms and arms after _ produce unreachable-arm warnings.

Ordinary value patterns compare for equality. Guards, range patterns, and nested destructuring are planned for future versions.

Typed borrowed bindings

A typed pattern uses the same name: type spelling as a function parameter:

match error {
    (e: ref RetryableError) => { /* Handle a retryable payload. */ },
    (_: ref IError) => { /* Handle other error payloads. */ },
    _ => { /* Handle remaining variants. */ }
};

Use const ref for read-only access. _ in a typed binding keeps the type test without creating a variable; a plain _ matches unconditionally.

On an enum, a typed pattern examines each single-payload variant. A concrete class pattern requires that exact payload type; an interface pattern accepts payload classes implementing the interface or borrowed interfaces extending it. Matching a concrete class directly is also supported. Interface patterns do not perform runtime downcasts of an interface value.

Arms run in source order. Exhaustiveness checking includes every variant covered by a typed pattern. Multiple variants carrying the same concrete type are grouped by a typed pattern; an ordinary variant pattern can distinguish them.

Bindings borrow the active payload. They neither move nor copy it, and they cannot outlive its owner. Mutable bindings cannot borrow through a const value. Payload-free and multiple-payload variants use ordinary variant patterns or _. Nested enums require another match: matching _Result<T, ErrorEnum> against an interface does not recursively inspect the variants inside ErrorEnum.