Match Expressions

match is a cleaner alternative to long if/elif chains. It compares a single value against a list of patterns and runs the matching arm.

Basic Match

Each arm is pattern: body:

fn main() {
    var day:i32 = 3

    match (day) {
        1: { print("Monday\n") }
        2: { print("Tuesday\n") }
        3: { print("Wednesday\n") }
        4: { print("Thursday\n") }
        5: { print("Friday\n") }
        default: { print("Weekend\n") }
    }
}

The default: arm runs if no other pattern matches — think of it as the else of match.

Multi-Value Patterns

One arm can match several values with comma separation:

fn main() {
    var ch:i32 = 101  // 'e'

    match (ch) {
        97, 101, 105, 111, 117: {
            print("vowel\n")
        }
        default: {
            print("consonant\n")
        }
    }
}

Range Patterns

Use .. to match a range (inclusive on both ends):

fn main() {
    var score:i32 = 87

    match (score) {
        90..100: { print("A\n") }
        80..89:  { print("B\n") }
        70..79:  { print("C\n") }
        60..69:  { print("D\n") }
        0..59:   { print("F\n") }
        default: { print("invalid\n") }
    }
}

Range patterns only work with integer literals — the compiler expands them into individual case labels in the generated C code.

Match with Enums

Match pairs naturally with enums:

enum Direction {
    North
    South
    East
    West
}

fn main() {
    var dir = Direction.East

    match (dir) {
        Direction.North: { print("heading up\n") }
        Direction.South: { print("heading down\n") }
        Direction.East:  { print("heading right\n") }
        Direction.West:  { print("heading left\n") }
    }
}

Enum matches are exhaustive by convention — handle every variant, or use default: for “anything else.”

Returning from Match Arms

Arms can contain any statements, including return:

fn grade:str(score:i32) {
    match (score) {
        90..100: { return "A" }
        80..89:  { return "B" }
        70..79:  { return "C" }
        60..69:  { return "D" }
        default: { return "F" }
    }
    return "F"  // unreachable, but required for type checker
}

fn main() {
    print("92 -> {grade(92)}\n")
    print("75 -> {grade(75)}\n")
    print("40 -> {grade(40)}\n")
}

Practical Example: HTTP Status Codes

fn status_category:str(code:i32) {
    match (code) {
        200..299: { return "success" }
        300..399: { return "redirect" }
        400..499: { return "client error" }
        500..599: { return "server error" }
        default:  { return "unknown" }
    }
    return "unknown"
}

fn main() {
    var codes:i32[5] = {200, 301, 404, 500, 999}
    for (var code in codes) {
        print("{code} -> {status_category(code)}\n")
    }
}

Try it — Build a match that categorizes a character code as “digit” (48-57), “uppercase” (65-90), “lowercase” (97-122), or “other” in the Playground.


Expert Corner

What match compiles to: GX’s match generates a C switch statement. Multi-value patterns become multiple case labels, and range patterns are expanded by the compiler into one case per value. For small ranges this is fine; for huge ranges (0..10000), the compiler would emit thousands of case labels — use if/elif instead.

Why no “pattern matching with destructuring”: GX’s match is an enhanced switch, not Rust/Haskell-style algebraic data type matching. You can’t pattern-match a struct’s fields or bind variables inside patterns. The goal is readable control flow over integer-like values, not a functional-programming feature. Unions exist for tagged data but are matched manually with if.

Match vs if/elif: Use match when you’re comparing one value against many constants. Use if/elif when conditions involve comparisons between different variables, computed expressions, or complex boolean logic. Match is optimized by the C compiler into jump tables for dense integer ranges, which is usually faster than chained if.

Fallthrough is NOT supported: Unlike C switch, GX match arms don’t fall through. Each arm is self-contained — when it’s done, execution continues after the match. This eliminates the classic C bug of forgetting break between cases.

The exhaustiveness question: GX doesn’t enforce exhaustive matching the way Rust does with enums. If you match an enum but only handle half the variants, there’s no warning. For critical code, add default: { panic("unhandled variant") } to catch missing cases at runtime.