Pattern Matching (planned)

Pattern Matching (planned)

Pattern matching gives you a clean way to branch on values, unwrap optionals, and dispatch on types — 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)
  • Type matching with traits and structs
  • Block bodies for multi-statement arms
  • Match as an expression
  • Future: 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()
    }
}

Type Matching

When working with trait types, you can 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

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}"
}

Type matching

match trait_value {
    x: ConcreteType -> use(x)
    _ -> fallback
}

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) Planned Phase 1
Variable binding Planned Phase 1
Optional unwrapping (nil / value) Planned Phase 1
Guard clauses (if) Planned Phase 1
Match as expression Planned Phase 1
Block bodies Planned 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
Last updated on