# Chapter 8: Enums and Pattern Matching

**What you'll learn.** Plain enums, enums whose variants carry data, the built-in `Option[T]` and `Result[T, E]`, the patterns `match` accepts (literals, bindings, wildcards, guards, tuples, nested variants, union types), how exhaustiveness checking works, and how to write functions over enums.

**Why it matters.** A web app is full of values that are one of several shapes: a request that is idle, loading, loaded or failed; an order that is in the cart, placed, shipped or cancelled. A `status: string` field cannot tell you that you forgot a case. An enum can: when you add a variant, the compiler points at every `match` that does not handle it yet.

If you have used Rust enums, Swift enums or TypeScript discriminated unions, the idea is the same: a closed set of named variants, each with its own payload, and a `match` the compiler checks for completeness.

## Plain enums

The simplest enum is a closed set of names. Unit-only enums are usually written with one variant per line, each ending in `;`:

```loup
@mode(web.compiled)

enum Direction {
    North;
    East;
    South;
    West;
}

func arrow(d: Direction): string {
    return match d {
        North => "^",
        East => ">",
        South => "v",
        West => "<",
    };
}

func main(): void {
    let heading = Direction.North;
    println(heading);
    println(arrow(heading));
    println(arrow(Direction.West));
    println(Direction.East == Direction.West);
}
```

*Listing 8-1: A plain enum and an exhaustive `match`*

```text
North
^
<
false
```

A few things to notice:

- A variant is written `Direction.North` where a value is expected. Inside a `match` arm, the bare name `North` is enough: the pattern is resolved against the type of the value being matched (Language Reference §1-9.4). The qualified form `Direction.North =>` works too.
- `match` is an expression. Its arms are separated by commas, each is `pattern => result`, and there are no parentheses around the value being matched (unlike `if` and `while`).
- `println` renders an enum value as its variant name.

### Mistake: forgetting a variant

Delete the `West` arm and the program no longer compiles:

```loup,does_not_compile
#~ @mode(web.compiled)
enum Direction {
    North;
    East;
    South;
    West;
}

func arrow(d: Direction): string {
    return match d {
        North => "^",
        East => ">",
        South => "v",
    };
}

func main(): void {
    println(arrow(Direction.West));
}
```

```text
main.lpweb:10:12: error[E0001]: Non-exhaustive match: missing variants West
```

This is the check that makes enums worth using: the message names exactly the variants that are not handled. You can fix it by adding the `West` arm, or by adding a wildcard arm `_ => "?"` that matches anything left over. Prefer listing the variants when you can. With a wildcard, a variant added later is silently handled by `_`; without one, the compiler sends you to every `match` that needs a decision.

## Enums with payloads

A variant can carry data. Variants with payloads are separated by commas, and each payload field has a name and a type:

```loup
#~ @mode(web.compiled)
enum Shape {
    Circle(radius: float),
    Rect(width: float, height: float),
    Point,
}

func area(s: Shape): float {
    return match s {
        Circle(r) => 3.14159 * r * r,
        Rect(w, h) => w * h,
        Point => 0.0,
    };
}

func main(): void {
    let c = Shape.Circle(radius = 2.0);
    let r = Shape.Rect(width = 3.0, height = 4.0);
    println(c);
    println(r);
    println(area(c));
    println(area(r));
    println(area(Shape.Point));
    println(r == Shape.Rect(width = 3.0, height = 4.0));
}
```

*Listing 8-2: Variants with payloads, constructed by name and taken apart by `match`*

```text
Circle(2.0)
Rect(3.0, 4.0)
12.56636
12
0
true
```

You construct a payload variant with named arguments, like a struct (`Shape.Rect(width = 3.0, height = 4.0)`); positional arguments also work (`Shape.Rect(3.0, 4.0)`). In a pattern, the payload is bound by position: `Rect(w, h)` binds `w` to the width and `h` to the height. Use `_` for a payload you don't need, as in `Rect(w, _)`.

`==` on enums is structural: two values are equal when they are the same variant with equal payloads. Like structs, enum values are copied on assignment, and their payloads follow the same pure-value rule as struct fields (Chapter 7).

The two variant separators are not mixed: a unit-only enum uses `;`, an enum with payloads uses `,`. An enum must declare at least one variant (`enum Empty {}` is `E_ENUM_NO_VARIANTS`); the type with no values is spelled `impossible`.

## `Option[T]`: a value that may be missing

Loup has no `null`. A value that may be absent has type `Option[T]`, a built-in enum with two variants, `Some(T)` and `None`. Both names are available without an import.

```loup
@mode(web.compiled)

struct User {
    id: int;
    name: string;
    email: Option[string];
}

func findUser(users: list[User], id: int): Option[User] {
    for (u in users) {
        if (u.id == id) {
            return Some(u);
        }
    }
    return None;
}

func main(): void {
    let users = [
        User(id = 1, name = "Ann", email = Some("ann@example.com")),
        User(id = 2, name = "Bo", email = None),
    ];

    let ann = findUser(users, 1);
    let nobody = findUser(users, 9);

    let greeting = match ann {
        Some(u) => "Hello, " + u.name,
        None => "Who are you?",
    };
    println(greeting);

    println(nobody?.name ?? "(no such user)");
    println(ann.is_some());
    println(nobody.is_none());
    println(ann.map(func(u: User): int => u.name.length()).unwrap_or(0));
    println(repr(nobody));
}
```

*Listing 8-3: Returning and consuming an `Option`*

```text
Hello, Ann
(no such user)
true
true
3
None
```

The ways to get at the value inside an `Option`:

- **`match`** handles both cases explicitly. This is the most common form.
- **`??`** supplies a fallback: `opt ?? default` is the value inside `Some`, or `default` for `None`.
- **`?.`** reads a field or calls a method only when the value is present: `nobody?.name` is an `Option[string]`, `None` here.
- **Methods**: `is_some()`, `is_none()`, `unwrap_or(default)`, `map(f)`, `and_then(f)`, `filter(pred)`, `unwrap()` and `expect(message)`.

`println` renders `Some(v)` as just `v` and `None` as an empty string; `repr` shows the variant, which is what you want in a log line.

### Mistake: the spelling of the methods

The Language Reference and older Loup material write the methods in camelCase (`isSome`, `unwrapOr`). The 1.25.1 toolchain implements them in snake_case:

```loup,does_not_compile
#~ @mode(web.compiled)
func main(): void {
    let port: Option[int] = Some(8080);
    if (port.isSome()) {
        println("configured");
    }
}
```

```text
main.lpweb:4:9: error[E0001]: type 'Option[int]' has no method 'isSome'
```

Write `port.is_some()`. The same goes for `is_none`, `unwrap_or`, and the `Result` methods below.

### Mistake: unwrapping `None`

`unwrap()` hands back the value inside `Some`. On `None` it throws:

```loup,throws
#~ @mode(web.compiled)
struct User {
    id: int;
    name: string;
}

func findUser(users: list[User], id: int): Option[User] {
    for (u in users) {
        if (u.id == id) {
            return Some(u);
        }
    }
    return None;
}

func main(): void {
    let users = [User(id = 1, name = "Ann")];
    println(findUser(users, 9).unwrap().name);
}
```

```text
Uncaught UnwrapError: called `unwrap()` on a None value
  at main (main.lpweb:18:13)
```

`unwrap()` says "I know this is present". When you don't know that, handle the `None` case:

```loup
#~ @mode(web.compiled)
struct User {
    id: int;
    name: string;
}

func findUser(users: list[User], id: int): Option[User] {
    for (u in users) {
        if (u.id == id) {
            return Some(u);
        }
    }
    return None;
}

func main(): void {
    let users = [User(id = 1, name = "Ann")];
    println(findUser(users, 9)?.name ?? "guest");
}
```

```text
guest
```

`expect("why it must be there")` is `unwrap()` with your own message, for the cases where absence really is a bug.

> **Known gap (1.25.1).** The `Option` returned by `dict.get(key)` works with `match`, `??`, `is_some()`, `unwrap()` and `unwrap_or()`, but `map` on it fails at run time and `repr` prints the bare value instead of `Some(…)`. Use `match` or `??` on a `dict.get` result.

## `Result[T, E]`: success or a described failure

`Result[T, E]` is the other built-in enum: `Ok(T)` for success, `Err(E)` for failure. Use it when a failure is an expected outcome the caller should decide about, such as invalid input or an insufficient balance.

```loup
@mode(web.compiled)

struct Account {
    owner: string;
    balanceCents: int;
}

func withdraw(acct: Account, cents: int): Result[Account, string] {
    if (cents <= 0) {
        return Err("amount must be positive");
    }
    if (cents > acct.balanceCents) {
        return Err(acct.owner + ": insufficient funds");
    }
    return Ok(acct with (balanceCents = acct.balanceCents - cents));
}

func deposit(acct: Account, cents: int): Result[Account, string] {
    if (cents <= 0) {
        return Err("amount must be positive");
    }
    return Ok(acct with (balanceCents = acct.balanceCents + cents));
}

func transfer(source: Account, target: Account, cents: int): Result[(Account, Account), string] {
    let newSource = withdraw(source, cents)?;
    let newTarget = deposit(target, cents)?;
    return Ok((newSource, newTarget));
}

func main(): void {
    let ann = Account(owner = "ann", balanceCents = 5000);
    let bo = Account(owner = "bo", balanceCents = 1000);

    match transfer(ann, bo, 1500) {
        Ok((a, b)) => println(str(a.balanceCents) + " / " + str(b.balanceCents)),
        Err(msg) => println("failed: " + msg),
    }
    match transfer(bo, ann, 9999) {
        Ok((a, b)) => println(str(a.balanceCents) + " / " + str(b.balanceCents)),
        Err(msg) => println("failed: " + msg),
    }

    let r = withdraw(ann, 700);
    println(r.is_ok());
    println(r.map(func(a: Account): int => a.balanceCents).unwrap_or(0));
    println(withdraw(ann, -5).unwrap_err());
}
```

*Listing 8-4: Functions that return `Result`, and `?` to pass an `Err` up*

```text
3500 / 2500
failed: bo: insufficient funds
true
4300
amount must be positive
```

The postfix `?` in `transfer` is the propagation operator (Language Reference §1-5.7). `withdraw(source, cents)?` is the `Account` inside `Ok`; if the result is an `Err`, `transfer` returns that `Err` at once. `?` is allowed only in a function that itself returns a `Result`.

The `Result` methods implemented today are `is_ok()`, `is_err()`, `unwrap()`, `expect(message)`, `unwrap_or(default)`, `unwrap_err()`, `map(f)`, `map_err(f)` and `and_then(f)`. Chapter 12 compares `Result` with exceptions and covers when to use each.

The arm pattern `Ok((a, b))` takes the tuple apart inside the `Ok`; nested patterns are covered below.

### Mistake: `from` as a parameter name

A first draft of `transfer` would naturally say `transfer(from: Account, to: Account, …)`:

```loup,does_not_compile
#~ @mode(web.compiled)
struct Account {
    owner: string;
    balanceCents: int;
}

func transfer(from: Account, to: Account, cents: int): Result[int, string] {
    return Ok(cents);
}

func main(): void {
    println("never runs");
}
```

```text
main.lpweb:7:15: error[E0001]: `from` is a reserved word (§1-3.4) and cannot name a parameter
  note: rename the parameter — e.g. `from_value`
```

`from` is reserved because it starts an import (`from core.math import (sqrt);`). Rename the parameter, as Listing 8-4 does with `source` and `target`.

### Mistake: `unwrapOr` compiles, then fails

On `Result`, the camelCase spelling is not caught by the compiler. It fails when the call runs:

```loup,throws
#~ @mode(web.compiled)
func withdraw(balance: int, amount: int): Result[int, string] {
    if (amount > balance) {
        return Err("insufficient funds");
    }
    return Ok(balance - amount);
}

func main(): void {
    let left = withdraw(100, 250).unwrapOr(100);
    println(left);
}
```

```text
Uncaught RuntimeError: internal interpreter limitation: unsupported instruction: CallVirtual unwrapOr on user class Result — method not found
```

The message says the interpreter could not find a method called `unwrapOr` on `Result`. The fix is the snake_case name, `unwrap_or(100)`. Because this mistake gets past the compiler, search your code for `unwrapOr`, `isOk` and `isErr` when you port examples from older material.

### Mistake: `?` on an `Option`

`?` works on `Result`, not on `Option`:

```loup,does_not_compile
#~ @mode(web.compiled)
func firstEven(xs: list[int]): Option[int] {
    for (x in xs) {
        if (x % 2 == 0) {
            return Some(x);
        }
    }
    return None;
}

func doubledFirstEven(xs: list[int]): Option[int] {
    let n = firstEven(xs)?;
    return Some(n * 2);
}

func main(): void {
    println(doubledFirstEven([1, 4, 5]));
}
```

```text
main.lpweb:12:13: error[E_PROPAGATE_WRONG_RETURN]: E_PROPAGATE_WRONG_RETURN: the '?' operator can only be used inside a function returning Result (§1-5.7)
```

For an `Option`, use `map`, `and_then`, `??` or a `match`. (Inside a function that returns `Result`, the 1.25.1 compiler does accept `?` on an `Option` and turns a `None` into `Err(None)`. The Language Reference does not allow this, so don't rely on it.)

```loup
#~ @mode(web.compiled)
func firstEven(xs: list[int]): Option[int] {
    for (x in xs) {
        if (x % 2 == 0) {
            return Some(x);
        }
    }
    return None;
}

func doubledFirstEven(xs: list[int]): Option[int] {
    return firstEven(xs).map(func(n: int): int => n * 2);
}

func main(): void {
    println(repr(doubledFirstEven([1, 4, 5])));
    println(repr(doubledFirstEven([1, 3])));
    println(firstEven([1, 3]) ?? -1);
}
```

```text
Some(8)
None
-1
```

## `match` in depth

### Literal patterns and wildcards

A literal pattern matches an equal value. `_` matches anything and binds nothing:

```loup
#~ @mode(web.compiled)
func command(word: string): string {
    return match word {
        "start" => "starting",
        "stop" => "stopping",
        _ => "unknown command: " + word,
    };
}

func main(): void {
    println(command("stop"));
    println(command("pause"));
}
```

```text
stopping
unknown command: pause
```

A `match` over an `int` or a `string` has no fixed set of variants, so it needs a `_` arm (or a binding arm) to be exhaustive.

### Binding patterns and guards

A plain name in a pattern binds the value. A guard, `if (condition)` after the pattern, adds a condition the arm must also satisfy:

```loup
#~ @mode(web.compiled)
func sizeLabel(bytes: Option[int]): string {
    return match bytes {
        Some(n) if (n >= 1000000) => i"{$ n // 1000000 $} MB",
        Some(n) if (n >= 1000) => i"{$ n // 1000 $} KB",
        Some(n) => i"{$ n $} bytes",
        None => "size unknown",
    };
}

func main(): void {
    println(sizeLabel(Some(2500000)));
    println(sizeLabel(Some(4096)));
    println(sizeLabel(Some(12)));
    println(sizeLabel(None));
}
```

*Listing 8-5: Guards choose between arms that match the same variant*

```text
2 MB
4 KB
12 bytes
size unknown
```

Arms are tried from top to bottom, and the first arm whose pattern and guard both match wins. For exhaustiveness the compiler treats a guarded arm as one that might not match, which is why `Some(n) =>` without a guard is still needed.

### Known gap: a guarded binding next to literal arms

Mapping HTTP status codes to text mixes literal arms with a guarded binding. In 1.25.1 that combination misbehaves:

```loup,not_desired_behavior
#~ @mode(web.compiled)
func statusText(code: int): string {
    return match code {
        200 => "OK",
        404 => "Not Found",
        n if (n >= 500) => i"server error {$ n $}",
        n => i"status {$ n $}",
    };
}

func main(): void {
    println(statusText(503));
    println(statusText(418));
}
```

```text
server error 
status 418
```

The guard selects the right arm, but in a `match` that also has literal arms, the name bound by a guarded arm renders empty in the arm's body. This is an interpreter bug, not something your code did wrong. Until it is fixed, guard on the value you are matching instead of on a new binding:

```loup
#~ @mode(web.compiled)
func statusText(code: int): string {
    return match code {
        200 => "OK",
        404 => "Not Found",
        _ if (code >= 500) => i"server error {$ code $}",
        n => i"status {$ n $}",
    };
}

func main(): void {
    println(statusText(200));
    println(statusText(404));
    println(statusText(503));
    println(statusText(418));
}
```

*Listing 8-6: Literal arms, a guarded wildcard and a binding arm*

```text
OK
Not Found
server error 503
status 418
```

### Tuple patterns

Matching a tuple lets one `match` look at several values at once. Tuple patterns may contain literals, bindings, `_` and nested tuples:

```loup
#~ @mode(web.compiled)
func quadrant(p: (int, int)): string {
    return match p {
        (0, 0) => "origin",
        (0, _) => "on the y axis",
        (_, 0) => "on the x axis",
        _ if (p[0] > 0 and p[1] > 0) => "first quadrant",
        _ => "elsewhere",
    };
}

func main(): void {
    println(quadrant((0, 0)));
    println(quadrant((0, 5)));
    println(quadrant((3, 4)));
    println(quadrant((-1, 4)));
}
```

```text
origin
on the y axis
first quadrant
elsewhere
```

Enum variant patterns are not allowed inside a tuple pattern in 1.25.1; the worked example at the end of the chapter shows what to do instead.

### Nested patterns

A pattern can look inside a variant's payload. Arms are tried in order and an arm matches only if its whole pattern matches, so `Ok(Some(n))` and `Ok(None)` are different cases:

```loup
#~ @mode(web.compiled)
func describe(r: Result[Option[int], string]): string {
    return match r {
        Ok(Some(n)) => i"value {$ n $}",
        Ok(None) => "no value",
        Err(e) => "error: " + e,
    };
}

func main(): void {
    println(describe(Ok(Some(5))));
    println(describe(Ok(None)));
    println(describe(Err("timeout")));
}
```

```text
value 5
no value
error: timeout
```

### Mistake: one arm for two variants

Rust and TypeScript let one arm cover several cases with `|`. Loup does not (Language Reference §1-9.4: each arm has exactly one pattern):

```loup,does_not_compile
#~ @mode(web.compiled)
enum Direction {
    North;
    East;
    South;
    West;
}

func axis(d: Direction): string {
    return match d {
        North | South => "vertical",
        East | West => "horizontal",
    };
}

func main(): void {
    println(axis(Direction.West));
}
```

```text
main.lpweb:11:15: error[E0001]: expected FatArrow, found Pipe
main.lpweb:11:15: error[E0001]: expected expression
main.lpweb:12:14: error[E0001]: expected FatArrow, found Pipe
main.lpweb:12:14: error[E0001]: expected expression
```

The parser expected the `=>` after the first pattern and found `|`. Write one arm per variant:

```loup
#~ @mode(web.compiled)
enum Direction {
    North;
    East;
    South;
    West;
}

func axis(d: Direction): string {
    return match d {
        North => "vertical",
        South => "vertical",
        East => "horizontal",
        West => "horizontal",
    };
}

func main(): void {
    println(axis(Direction.West));
}
```

```text
horizontal
```

When many variants share a result, a guard over a helper function (`_ if (isVertical(d)) => …`) also works.

### Block arms and `match` as a statement

An arm's result can be a block. The block's last expression is the arm's value. When you only want side effects, use `match` as a statement:

```loup
#~ @mode(web.compiled)
enum Shape {
    Circle(radius: float),
    Rect(width: float, height: float),
    Point,
}

func perimeter(s: Shape): float {
    return match s {
        Circle(r) => {
            let pi = 3.14159;
            2.0 * pi * r
        },
        Rect(w, h) => 2.0 * (w + h),
        Point => 0.0,
    };
}

func main(): void {
    println(perimeter(Shape.Circle(radius = 1.0)));
    let shapes = [Shape.Circle(radius = 2.0), Shape.Point, Shape.Rect(width = 1.0, height = 1.0)];
    for (s in shapes) {
        match s {
            Circle(r) => println("circle " + str(r)),
            Rect(w, h) => println("rect " + str(w) + "x" + str(h)),
            Point => println("point"),
        }
    }
}
```

```text
6.28318
circle 2
point
rect 1x1
```

### Matching union types

A value of a union type such as `int | string | bool` (Chapter 3) is matched by type. The pattern `int(n)` matches an `int` and binds it:

```loup
#~ @mode(web.compiled)
func describe(v: int | string | bool): string {
    return match v {
        int(n) => i"int {$ n $}",
        string(s) => "string " + s,
        bool(b) => i"bool {$ b $}",
    };
}

func label(v: string | int): string {
    if (v is string) {
        return "text: " + v.toUpperCase();
    }
    return "number";
}

func main(): void {
    println(describe(42));
    println(describe("hi"));
    println(describe(true));
    println(label("ok"));
    println(label(3));
}
```

```text
int 42
string hi
bool true
text: OK
number
```

The match must cover every member of the union; leaving out `bool` is `Non-exhaustive match: missing types bool`. For a single check, `is` narrows the type inside the `if` branch, so `v.toUpperCase()` is allowed there.

## Generic enums

An enum can take type parameters, and a variant can refer to the enum itself. That makes trees easy to express:

```loup
#~ @mode(web.compiled)
enum Tree[T] {
    Leaf(value: T),
    Node(left: Tree[T], right: Tree[T]),
}

func countLeaves[T](t: Tree[T]): int {
    return match t {
        Leaf(_) => 1,
        Node(l, r) => countLeaves(l) + countLeaves(r),
    };
}

func sumTree(t: Tree[int]): int {
    return match t {
        Leaf(v) => v,
        Node(l, r) => sumTree(l) + sumTree(r),
    };
}

func main(): void {
    let t = Tree.Node(
        left = Tree.Leaf(value = 1),
        right = Tree.Node(left = Tree.Leaf(value = 2), right = Tree.Leaf(value = 3)),
    );
    println(countLeaves(t));
    println(sumTree(t));
}
```

*Listing 8-7: A recursive generic enum*

```text
3
6
```

`Option[T]` and `Result[T, E]` are generic enums of exactly this kind. Chapter 11 covers generics in general.

## Functions over enums

In Rust or Swift you would put a `typeName()` method inside the enum. In Loup, an enum body holds only variants (the grammar in Language Reference §1-19 has no other member):

```loup,does_not_compile
#~ @mode(web.compiled)
enum Json {
    Null,
    Number(value: float),
    Text(value: string),

    func typeName(): string {
        return "?";
    }
}

func main(): void {
    println("never runs");
}
```

```text
main.lpweb:7:5: error[E0001]: `func` is a reserved word (§1-3.4) and cannot name an enum variant
  note: rename the enum variant — e.g. `func_value`
main.lpweb:7:10: error[E0002]: expected `}`
main.lpweb:7:20: error[E0003]: expected `;`
main.lpweb:9:5: error[E0001]: expected expression
main.lpweb:10:1: error[E0001]: expected expression
```

The parser read `func` as the name of a fourth variant, which is why the first message talks about a reserved word. Write the operation as a module-level function that takes the enum:

```loup
#~ @mode(web.compiled)
enum Json {
    Null,
    Bool(value: bool),
    Number(value: float),
    Text(value: string),
    Array(items: list[Json]),
}

func typeName(v: Json): string {
    return match v {
        Null => "null",
        Bool(_) => "bool",
        Number(_) => "number",
        Text(_) => "string",
        Array(_) => "array",
    };
}

func countScalars(v: Json): int {
    return match v {
        Array(items) => {
            var n = 0;
            for (item in items) {
                n = n + countScalars(item);
            }
            n
        },
        _ => 1,
    };
}

func main(): void {
    let doc = Json.Array(items = [
        Json.Number(value = 1.5),
        Json.Text(value = "hi"),
        Json.Array(items = [Json.Bool(value = true), Json.Null]),
    ]);
    println(typeName(doc));
    println(countScalars(doc));
    println(doc);
}
```

*Listing 8-8: A recursive `Json` enum with functions that match over it*

```text
array
4
Array([Number(1.5), Text("hi"), Array([Bool(true), Null])])
```

Adding a variant to `Json` makes `typeName` fail to compile until it handles the new case. `countScalars` has a `_` arm, so it would accept the new variant silently; that is the trade-off from the start of the chapter.

## A worked example: an order state machine

An online order moves through states in response to events. Some transitions are allowed (a placed order can be shipped) and some are not (a shipped order cannot be cancelled). Two enums and a function that returns a `Result` describe the whole thing.

The first attempt matches on the pair `(state, event)`:

```loup,does_not_compile
#~ @mode(web.compiled)
enum OrderState {
    Cart,
    Placed(orderId: string),
}

enum OrderEvent {
    Checkout(orderId: string),
    Cancel(reason: string),
}

func next(state: OrderState, event: OrderEvent): Result[OrderState, string] {
    return match (state, event) {
        (Cart, Checkout(id)) => Ok(OrderState.Placed(orderId = id)),
        _ => Err("not allowed"),
    };
}

func main(): void {
    println("never runs");
}
```

```text
main.lpweb:14:10: error[E0001]: unsupported pattern inside a tuple pattern (only literals, bindings, `_` and nested tuples are supported)
main.lpweb:14:16: error[E0001]: unsupported pattern inside a tuple pattern (only literals, bindings, `_` and nested tuples are supported)
```

In 1.25.1 a tuple pattern cannot contain variant patterns. Match on the state first, then on the event inside each arm:

```loup
@mode(web.compiled)

enum OrderState {
    Cart,
    Placed(orderId: string),
    Shipped(orderId: string, tracking: string),
    Delivered(orderId: string),
    Cancelled(reason: string),
}

enum OrderEvent {
    Checkout(orderId: string),
    Ship(tracking: string),
    Deliver,
    Cancel(reason: string),
}

func rejected(state: OrderState, event: OrderEvent): Result[OrderState, string] {
    return Err("cannot apply " + str(event) + " to " + str(state));
}

func next(state: OrderState, event: OrderEvent): Result[OrderState, string] {
    return match state {
        Cart => match event {
            Checkout(id) => Ok(OrderState.Placed(orderId = id)),
            Cancel(why) => Ok(OrderState.Cancelled(reason = why)),
            _ => rejected(state, event),
        },
        Placed(id) => match event {
            Ship(tracking) => Ok(OrderState.Shipped(orderId = id, tracking = tracking)),
            Cancel(why) => Ok(OrderState.Cancelled(reason = why)),
            _ => rejected(state, event),
        },
        Shipped(id, _) => match event {
            Deliver => Ok(OrderState.Delivered(orderId = id)),
            _ => rejected(state, event),
        },
        _ => rejected(state, event),
    };
}

func run(events: list[OrderEvent]): Result[OrderState, string] {
    var state = OrderState.Cart;
    for (e in events) {
        state = next(state, e)?;
    }
    return Ok(state);
}

func describe(result: Result[OrderState, string]): string {
    return match result {
        Ok(Delivered(id)) => "order " + id + " delivered",
        Ok(other) => "order is now " + str(other),
        Err(msg) => "rejected: " + msg,
    };
}

func main(): void {
    let happy = [
        OrderEvent.Checkout(orderId = "A-17"),
        OrderEvent.Ship(tracking = "1Z999"),
        OrderEvent.Deliver,
    ];
    let late = [
        OrderEvent.Checkout(orderId = "A-18"),
        OrderEvent.Ship(tracking = "1Z123"),
        OrderEvent.Cancel(reason = "changed mind"),
    ];
    let early = [
        OrderEvent.Checkout(orderId = "A-19"),
        OrderEvent.Cancel(reason = "duplicate"),
    ];
    println(describe(run(happy)));
    println(describe(run(late)));
    println(describe(run(early)));
}
```

*Listing 8-9: An order state machine: enums for states and events, `Result` for rejected transitions*

```text
order A-17 delivered
rejected: cannot apply Cancel("changed mind") to Shipped("A-18", "1Z123")
order is now Cancelled("duplicate")
```

The pieces from this chapter fit together here:

- `next` is a `match` inside a `match`. Each state lists the events it accepts; everything else goes to `rejected`.
- `run` applies events in order and uses `?` to stop at the first rejected transition.
- `describe` uses a nested pattern, `Ok(Delivered(id))`, before the more general `Ok(other)`.
- `str(event)` and `str(state)` render enum values the same way `println` does, which gives readable error messages for free.

The same shape (an enum of states, an enum of events, a function from one state to the next) is how you would model a form wizard or a fetch lifecycle in a web UI.

## Try it yourself

1. Add a `Returned(orderId: string)` state that a `Delivered` order can move to on a new `Return` event. Compile before you update `next` and `describe`; which `match` expressions does the compiler point at, and which ones does it not, because of a `_` arm?
2. Model a fetch lifecycle: `enum Fetch[T] { Idle, Loading, Loaded(value: T), Failed(message: string) }`, and write `func label(f: Fetch[int]): string`.
3. Write `func depth(v: Json): int` for the `Json` enum in Listing 8-8.

## Summary

- An enum is a closed set of variants. Unit-only enums use `;` between variants; enums with payloads use `,`.
- Construct a variant as `Shape.Rect(width = 3.0, height = 4.0)`; take it apart with `Rect(w, h) =>` in a `match`. Enum `==` is structural.
- `match` must be exhaustive. The compiler names the missing variants or union members; a `_` arm covers the rest, at the cost of not being told about new variants.
- Patterns: literals, bindings, `_`, guards (`if (…)`), tuples, nested variants such as `Ok(Some(n))`, and type patterns such as `int(n)` for unions. There are no `|` alternatives in an arm.
- `Option[T]` replaces `null`; use `match`, `??`, `?.` and the snake_case methods (`is_some`, `unwrap_or`, `map`, …). `unwrap()` on `None` throws `UnwrapError`.
- `Result[T, E]` carries a success or a failure; `?` passes an `Err` up from a function that returns `Result`, and does not apply to `Option`.
- Enum bodies hold only variants; write operations as functions that take the enum.
- Known gaps in 1.25.1: camelCase `Result` methods such as `unwrapOr` compile and then fail at run time; a guarded binding in a `match` that also has literal arms renders empty; variant patterns cannot appear inside tuple patterns; `map` and `repr` misbehave on a `dict.get` result.
