Pattern Matching

Pattern Matching

Pattern matching gives you a clean way to branch on values, unwrap optionals, and use guard clauses — without long if/else chains. It uses the match keyword and produces a value, making it both a control flow tool and an expression.

This page covers:

  • Matching on values (strings, ints, bools)
  • Variable binding in match arms
  • Optional unwrapping (?T and nil)
  • Guard clauses (if conditions on arms)
  • Block bodies for multi-statement arms
  • Match as an expression
  • Future: type matching, enums, and exhaustiveness checking

Basic Value Matching

The simplest form matches a value against literal patterns:

match status {
    "active" -> print("user is active")
    "banned" -> print("user is banned")
    "pending" -> print("waiting for approval")
    _ -> print("unknown status")
}

Each arm is a pattern followed by -> and a body. The _ wildcard matches anything and acts as the default case.

This replaces the common if/else chain:

// Before: repetitive, verbose
if status == "active" {
    print("user is active")
} else if status == "banned" {
    print("user is banned")
} else if status == "pending" {
    print("waiting for approval")
} else {
    print("unknown status")
}

Match is shorter, the intent is clearer, and you never repeat the variable name.

Matching integers

match http_code {
    200 -> "OK"
    404 -> "Not Found"
    500 -> "Internal Server Error"
    _ -> "Unknown"
}

Matching booleans

match is_admin {
    true -> "full access"
    false -> "read only"
}

Variable Binding

When a pattern is an identifier (not a literal), it binds the matched value to that name for use in the arm body:

match error_code {
    0 -> print("success")
    code -> print("failed with code: ${code}")
}

The arm code -> ... matches any value and makes it available as code. This is useful as a catch-all that still gives you access to the value, unlike _ which discards it.

Match as Expression

Match produces a value. You can assign the result directly to a variable:

label: str = match status {
    "active" -> "Active User"
    "banned" -> "Banned"
    "pending" -> "Pending Review"
    _ -> "Unknown"
}
print(label)

This works because each arm’s body is an expression that produces a value. All arms must produce the same type.

You can also use match inline:

print(match mode {
    "dark" -> "Dark Mode"
    "light" -> "Light Mode"
    _ -> "System Default"
})

Optional Unwrapping

Matching on optional types (?T) is one of the most common uses. The nil pattern matches absence, and a variable pattern binds the unwrapped value:

user: ?Person = find_user(42)

match user {
    nil -> print("no user found")
    u -> print("found: ${u.name}")
}

In the u arm, the value is unwrapped — u is a Person, not ?Person. The match guarantees it’s not nil, so you get a clean, typed binding without manual nil checks.

Compare with today’s approach:

// Current: manual nil check, no unwrapping
if user != nil {
    print("found: ${user.name}")
} else {
    print("no user found")
}

Match makes the unwrapping explicit and safe.

Optional with guard

age: ?int = parse_age(input)

match age {
    nil -> print("no age provided")
    a if a < 0 -> print("invalid age")
    a if a < 18 -> print("minor")
    a -> print("adult, age ${a}")
}

Guard Clauses

Guards add conditions to a pattern using if. The arm only matches if both the pattern and the guard are satisfied:

match score {
    0 -> "no score"
    n if n >= 90 -> "excellent"
    n if n >= 70 -> "good"
    n if n >= 50 -> "pass"
    n -> "fail (${n})"
}

The variable n is bound first, then the if condition is checked. If the guard fails, matching continues to the next arm.

Guards keep patterns simple — the pattern does the structural matching, the guard handles the logic.

Guards with optionals

match find_temperature(city) {
    nil -> "no data"
    t if t > 35.0 -> "hot (${t})"
    t if t < 0.0 -> "freezing (${t})"
    t -> "normal (${t})"
}

Block Bodies

For arms that need more than a single expression, use a block body with { }:

match command {
    "quit" -> {
        save_state()
        print("goodbye")
    }
    "help" -> {
        print("Available commands:")
        print("  quit - exit the program")
        print("  help - show this message")
    }
    cmd -> print("unknown command: ${cmd}")
}

Block bodies follow the same rules as lambda block bodies — use return to produce a value when match is used as an expression:

result: str = match input {
    "" -> "empty"
    s -> {
        trimmed: str = s.trim()
        return trimmed.upper()
    }
}

Future: Type Matching

Type matching is planned for Phase 2. The syntax below shows the intended design.

When working with trait types, you will be able to match on the concrete type using the name: Type syntax — the same syntax Luma uses for declarations:

trait Shape {}

struct Circle {
    radius: float
}

struct Rect {
    width: float
    height: float
}

fn area(shape: Shape) -> float {
    return match shape {
        c: Circle -> 3.14 * c.radius * c.radius
        r: Rect -> r.width * r.height
        _ -> 0.0
    }
}

The pattern c: Circle means “if the value is a Circle, bind it to c.” Inside the arm, c has the full Circle type with access to its fields.

This replaces type-checking chains:

// Before: manual type dispatch
fn area(shape: Shape) -> float {
    if shape.is_type("Circle") {
        return 3.14 * shape.radius * shape.radius
    } else if shape.is_type("Rect") {
        return shape.width * shape.height
    }
    return 0.0
}

Type matching with guards (planned)

fn describe(shape: Shape) -> str {
    return match shape {
        c: Circle if c.radius > 100.0 -> "large circle"
        c: Circle -> "circle (r=${c.radius})"
        r: Rect if r.width == r.height -> "square (${r.width})"
        r: Rect -> "rectangle (${r.width} x ${r.height})"
        _ -> "unknown shape"
    }
}

Rules

A few rules that keep match predictable:

  • Arms are checked top to bottom. The first matching arm wins. Put specific patterns before general ones.
  • _ matches anything and discards the value. Use it as a catch-all at the end.
  • Variable patterns match anything and bind the value. n -> ... always matches.
  • Guards are optional. n if n > 0 -> ... only matches if the guard is true.
  • All arms must produce the same type when match is used as an expression.
  • A _ or variable catch-all is required for non-exhaustive patterns. The compiler warns if a match could fall through with no matching arm.

Arm ordering matters

Because arms are checked in order, put specific cases first:

// Correct: specific before general
match n {
    0 -> "zero"
    n if n < 0 -> "negative"
    n -> "positive"
}

// Wrong: variable arm catches everything, other arms never run
match n {
    n -> "always this"
    0 -> "never reached"
}

Common Patterns

Replacing if/else chains

// Before
greeting: str = "hey"
if hour < 12 {
    greeting = "good morning"
} else if hour < 18 {
    greeting = "good afternoon"
} else {
    greeting = "good evening"
}

// After
greeting: str = match hour {
    h if h < 12 -> "good morning"
    h if h < 18 -> "good afternoon"
    _ -> "good evening"
}

Safe optional access

fn user_label(id: int) -> str {
    return match find_user(id) {
        nil -> "guest"
        user -> "${user.name} (${user.role})"
    }
}

Command dispatch

match args[0] {
    "run" -> run_command(args)
    "build" -> build_project(args)
    "test" -> run_tests(args)
    "help" -> print_help()
    cmd -> print("unknown command: ${cmd}")
}

Error code handling

message: str = match response.status {
    200 -> "success"
    201 -> "created"
    400 -> "bad request"
    401 -> "unauthorized"
    404 -> "not found"
    code if code >= 500 -> "server error (${code})"
    code -> "unexpected status: ${code}"
}

Quick Reference

Value matching

match value {
    literal -> expr           // match exact value
    name -> expr              // bind to variable
    _ -> expr                 // wildcard (catch-all)
}

With guards

match value {
    n if n > 0 -> "positive"
    n if n < 0 -> "negative"
    _ -> "zero"
}

Optional unwrapping

match optional_value {
    nil -> "absent"
    val -> "present: ${val}"
}

As expression

result: str = match value {
    pattern -> expr
    _ -> default_expr
}

Block bodies

match value {
    pattern -> {
        statement1
        statement2
        return expr
    }
}

Future: Enums

Enums define a fixed set of named variants. Combined with pattern matching, they let you model choices, states, and alternatives as types — and the compiler ensures you handle every case.

Enums are not yet implemented. The design below shows the planned direction.

Simple enums

A basic enum lists its variants:

enum Color {
    Red
    Green
    Blue
}

color: Color = Color.Red

label: str = match color {
    Color.Red -> "red"
    Color.Green -> "green"
    Color.Blue -> "blue"
}

With simple enums, every variant is a plain value with no additional data. The compiler knows all possible variants, so it can verify your match is exhaustive — if you forget a case, the compiler tells you:

// Compiler warning: missing case Color.Blue
match color {
    Color.Red -> "red"
    Color.Green -> "green"
}

Enums with associated data

Each variant can carry its own data, making enums a powerful modeling tool:

enum Shape {
    Circle { radius: float }
    Rect { width: float, height: float }
    Point
}

Pattern matching can destructure the associated data:

fn area(s: Shape) -> float {
    return match s {
        Shape.Circle { radius } -> 3.14 * radius * radius
        Shape.Rect { width, height } -> width * height
        Shape.Point -> 0.0
    }
}

The destructuring Shape.Circle { radius } extracts the radius field and makes it available in the arm body. This combines type checking, field access, and variable binding in a single pattern.

Modeling optional results

Enums naturally model “one of several outcomes”:

enum Result {
    Ok { value: int }
    Err { message: str }
}

fn parse(input: str) -> Result {
    if input == "" {
        return Result.Err { message: "empty input" }
    }
    return Result.Ok { value: input.to_int().or(0) }
}

match parse(user_input) {
    Result.Ok { value } -> print("parsed: ${value}")
    Result.Err { message } -> print("error: ${message}")
}

Modeling states

Enums are ideal for state machines and workflows:

enum OrderStatus {
    Pending
    Shipped { tracking: str }
    Delivered { date: str }
    Cancelled { reason: str }
}

fn status_label(status: OrderStatus) -> str {
    return match status {
        OrderStatus.Pending -> "Waiting to ship"
        OrderStatus.Shipped { tracking } -> "Shipped (${tracking})"
        OrderStatus.Delivered { date } -> "Delivered on ${date}"
        OrderStatus.Cancelled { reason } -> "Cancelled: ${reason}"
    }
}

Exhaustiveness

When matching on enums, the compiler verifies that every variant is covered. This is one of the biggest benefits of enums — if you add a new variant later, the compiler points you to every match that needs updating.

No _ needed (or allowed) when all variants are handled:

// All variants covered — no wildcard required
match color {
    Color.Red -> "red"
    Color.Green -> "green"
    Color.Blue -> "blue"
}

If you intentionally want to ignore some variants, use _:

match color {
    Color.Red -> "primary"
    _ -> "not red"
}

Future: Struct Field Matching

Matching on specific field values of structs, without enums:

match person {
    Person { name: "Alice" } -> "found Alice"
    Person { age } if age >= 18 -> "adult: ${age}"
    _ -> "someone else"
}

This is a convenience for matching struct shapes directly. The pattern Person { name: "Alice" } matches any Person whose name field equals "Alice".

Future: Multiple Patterns Per Arm

Matching several values in a single arm using |:

match day {
    "Saturday" | "Sunday" -> "weekend"
    _ -> "weekday"
}

This avoids duplicating arm bodies for related patterns.

Roadmap

Feature Status Phase
Value matching (literals, wildcards) Done Phase 1
Variable binding Done Phase 1
Optional unwrapping (nil / value) Done Phase 1
Guard clauses (if) Done Phase 1
Match as expression Done Phase 1
Block bodies Done Phase 1
Type matching (traits) Planned Phase 2
Simple enums Planned Phase 3
Enums with associated data Planned Phase 3
Exhaustiveness checking Planned Phase 3
Struct field matching Planned Future
Multiple patterns per arm (|) Planned Future
Last updated on