# Chapter 7: Enums and Pattern Matching

**What you'll learn:** how to define enums with and without payloads, take them apart with `match`, use guards, wildcards and nested patterns, and use the two built-in enums you will meet in almost every script, `Option[T]` and `Result[T, E]`.

An **enum** is a type whose values are one of a fixed set of *variants*, each of which may carry its own data. `match` inspects such a value, runs the arm for its variant, and binds the variant's data to names. Together they let the compiler check that every case is handled: add a variant, and every `match` that forgot it stops compiling. This chapter follows the Language Reference §1-9.

## Why Enums?

Without enums, a shape might be a string plus some loose numbers:

```loup
@mode(host.scripted)

let kind = "circle";
let radius = 5.0;
let width = 0.0;     # only meaningful when kind == "rect" — nothing checks that
println(kind);
```

Nothing stops a typo like `"circel"`, and nothing ties `radius` to circles. An enum makes the relationship part of the type:

```loup
@mode(host.scripted)

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

let s = Shape.Circle(radius = 5.5);
let r = Shape.Rect(width = 3.0, height = 4.5);
println(s);
println(r);
println(Shape.Point);
```

*Listing 7-1: An enum with payload variants and a unit variant*

```text
Circle(5.5)
Rect(3.0, 4.5)
Point
```

A variant with a payload is constructed like a struct, with named arguments: `Shape.Circle(radius = 5.5)`. A variant without one, such as `Point`, is a value on its own. Each variant has exactly the fields it declares — a `Point` has no radius to get wrong.

## Unit-Only Enums

When no variant carries data, an enum is a closed list of names. The idiomatic style ends each variant with `;`:

```loup
@mode(host.scripted)

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

func turnRight(d: Direction): Direction {
    return match d {
        North => Direction.East,
        East => Direction.South,
        South => Direction.West,
        West => Direction.North,
    };
}

let heading = Direction.North;
println(turnRight(heading));
println(turnRight(turnRight(heading)) == Direction.South);
```

*Listing 7-2: A unit-only enum and a function that matches on it*

```text
East
true
```

In a `match` arm, a variant can be written bare (`North`), because the compiler knows the type being matched. Elsewhere, qualify it with the enum name (`Direction.East`). Payload enums separate their variants with commas, unit-only enums with semicolons; the Language Reference makes mixing the two in one enum an error (`E_ENUM_MIXED_SEP`), which Loup 1.25 reports only as a warning.

Enum values compare by variant and payload: `Direction.North == Direction.North` is `true`, and so is `Shape.Circle(radius = 1.5) == Shape.Circle(radius = 1.5)`.

## Taking Enums Apart with `match`

`match value { pattern => result, … }` tries each arm in order and evaluates the first whose pattern fits. A variant pattern binds the payload to names, by position:

```loup
@mode(host.scripted)

enum Shape {
    Circle(radius: float),
    Rect(width: float, height: float),
    Triangle(base: float, height: float),
    Point,
}

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

let shapes = [
    Shape.Circle(radius = 2.0),
    Shape.Rect(width = 3.0, height = 4.5),
    Shape.Triangle(base = 5.0, height = 3.0),
];
for (s in shapes) {
    println(i"{$ s $}: {$ area(s) $}");
}
```

*Listing 7-3: Computing an area with one arm per variant*

```text
Circle(2.0): 12.56636
Rect(3.0, 4.5): 13.5
Triangle(5.0, 3.0): 7.5
```

- `Circle(r)` matches a circle and binds its radius to `r` for that arm only.
- `match` is an expression: here its value is the function's result. The arms are separated by commas.
- The value being matched is written without parentheses: `match shape { … }`, unlike the condition of an `if`.

### Mistake: forgetting a variant

The compiler checks that a `match` on an enum covers every variant (§1-9.4):

```loup,does_not_compile
@mode(host.scripted)

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

func name(shape: Shape): string {
    return match shape {
        Circle(_) => "circle",
        Rect(_, _) => "rectangle",
    };
}

println(name(Shape.Point));
```

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

This is the property that makes enums worth using: when you add a variant to an enum, every `match` that does not handle it fails to compile, and the error names the variant. Add the arm:

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

func name(shape: Shape): string {
    return match shape {
        Circle(_) => "circle",
        Rect(_, _) => "rectangle",
        Point => "point",
    };
}

println(name(Shape.Point));
```

```text
point
```

`_` in a payload position ignores that field. As a whole pattern, `_` matches anything, so a final `_ => …` arm makes any `match` exhaustive — at the price that the compiler can no longer tell you about a new variant. Prefer listing the variants.

## Guards

An arm can add a condition after its pattern with `if (…)`. The arm is chosen only when the pattern fits *and* the guard is true:

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

func describe(shape: Shape): string {
    return match shape {
        Circle(r) if (r > 100.0) => "huge circle",
        Circle(r) if (r > 10.0) => "medium circle",
        Circle(_) => "small circle",
        Rect(w, h) if (w == h) => i"square {$ w $}",
        Rect(w, h) => i"rectangle {$ w $} x {$ h $}",
        Point => "point",
    };
}

println(describe(Shape.Circle(radius = 150.0)));
println(describe(Shape.Circle(radius = 2.5)));
println(describe(Shape.Rect(width = 2.5, height = 2.5)));
println(describe(Shape.Rect(width = 2.5, height = 1.5)));
```

*Listing 7-4: Guards refine an arm*

```text
huge circle
small circle
square 2.5
rectangle 2.5 x 1.5
```

Arms are tried top to bottom, so put the more specific guarded arms first.

### Mistake: an or-pattern

Rust and F# let one arm list several patterns, `A | B => …`. Loup does not (§1-9.4: each arm has exactly one pattern):

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

func isVertical(d: Direction): bool {
    return match d {
        North | South => true,
        _ => false,
    };
}

println(isVertical(Direction.South));
```

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

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

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

func isVertical(d: Direction): bool {
    return match d {
        North => true,
        South => true,
        _ => false,
    };
}

println(isVertical(Direction.South));
println(isVertical(Direction.East));
```

```text
true
false
```

## Matching Other Values

`match` also works on strings, numbers and booleans, with literal patterns:

```loup
#~ @mode(host.scripted)
func reason(status: int): string {
    return match status {
        200 => "OK",
        404 => "Not Found",
        500 => "Internal Server Error",
        _ => "Unknown",
    };
}

println(reason(404));
println(reason(418));
```

```text
Not Found
Unknown
```

### Mistake: no catch-all arm on a literal match

The exhaustiveness check only knows the variants of enums (and the members of union types). A `match` on an `int` or a `string` without `_` compiles, and fails when no arm fits:

```loup,throws
@mode(host.scripted)

func reason(status: int): string {
    return match status {
        200 => "OK",
        404 => "Not Found",
        500 => "Internal Server Error",
    };
}

println(reason(404));
println(reason(418));
```

```text
Not Found
Uncaught MatchError: No matching arm for value in match expression
  at reason (main.lpscr:4:12)
  at <main> (main.lpscr:12:9)
```

Always end a match on literals with a `_` arm, as in the version above.

### `match` as a statement

When the arms do work instead of producing a value, use `match` as a statement and give the arms blocks. An empty block `{}` does nothing:

```loup
#~ @mode(host.scripted)
enum Level {
    Debug;
    Info;
    Warn;
}

func log(level: Level, msg: string): void {
    match level {
        Debug => {},
        Info => println(msg),
        Warn => {
            println(i"WARNING: {$ msg $}");
        },
    }
}

log(Level.Debug, "cache lookup");
log(Level.Info, "started");
log(Level.Warn, "disk 91% full");
```

```text
started
WARNING: disk 91% full
```

A block arm used for its value yields its last expression, as an `if` branch does.

## Nested Patterns

Patterns nest, so one arm can look through several layers:

```loup
#~ @mode(host.scripted)
func classify(r: Result[Option[int], string]): string {
    return match r {
        Ok(Some(n)) if (n > 0) => i"positive: {$ n $}",
        Ok(Some(n)) => i"non-positive: {$ n $}",
        Ok(None) => "no value",
        Err(msg) => i"error: {$ msg $}",
    };
}

println(classify(Ok(Some(5))));
println(classify(Ok(Some(-2))));
println(classify(Ok(None)));
println(classify(Err("timeout")));
```

*Listing 7-5: Nested `Result` and `Option` patterns*

```text
positive: 5
non-positive: -2
no value
error: timeout
```

An arm is chosen only when its whole pattern matches, nested parts included (§1-9.4, ruling MATCH-2).

## Generic and Recursive Enums

An enum can take type parameters and can refer to itself, which makes it a natural fit for trees:

```loup
@mode(host.scripted)

enum Tree[T] {
    Leaf(value: T),
    Node(left: Tree[T], right: Tree[T]),
}

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

let tree: Tree[int] = Tree.Node(
    left = Tree.Leaf(value = 1),
    right = Tree.Node(
        left = Tree.Leaf(value = 2),
        right = Tree.Leaf(value = 3),
    ),
);
println(tree);
println(sum(tree));
```

*Listing 7-6: A generic binary tree*

```text
Node(Leaf(1), Node(Leaf(2), Leaf(3)))
6
```

## `Option[T]`: a Value That May Be Absent

Loup has no `null`. A value that may be missing has the type `Option[T]` (also written `T?`), whose variants are `Some(value)` and `None`. Both are available everywhere without an import:

```loup
@mode(host.scripted)

struct User {
    id: int;
    name: string;
}

let users = [
    User(id = 1, name = "Ada"),
    User(id = 2, name = "Grace"),
];

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

let name = match findUser(2) {
    Some(u) => u.name,
    None => "Unknown",
};
println(name);

println(findUser(9)?.name ?? "Guest");

let fallback = User(id = 0, name = "nobody");
println(findUser(7).unwrap_or(fallback).name);
println(findUser(1).is_some());
```

*Listing 7-7: Producing and consuming an `Option`*

```text
Grace
Guest
nobody
true
```

- `match` with `Some(u)` and `None` arms handles both cases explicitly.
- `opt?.field` reads a field when the option is `Some`, and gives `None` otherwise; `opt ?? default` unwraps an option or supplies a default.
- `unwrap_or(default)`, `is_some()` and `is_none()` are methods on `Option`. `unwrap()` returns the value or throws `UnwrapError` on `None`; use it only when absence is a bug.

### Mistake: the spec's camelCase method names

The Language Reference writes the `Option` and `Result` helpers in camelCase (`unwrapOr`, `isSome`). Loup 1.25 implements them in snake_case:

```loup,does_not_compile
@mode(host.scripted)

let ports = {"http": 80, "https": 443};
let ssh = ports.get("ssh");
if (ssh.isSome()) {
    println("ssh is configured");
}
println(ssh.unwrapOr(22));
```

```text
main.lpscr:5:5: error[E0001]: type 'Option[int]' has no method 'isSome'
main.lpscr:8:9: error[E0001]: type 'Option[int]' has no method 'unwrapOr'
```

Use the snake_case names — or, more often, a `match` or `??`:

```loup
@mode(host.scripted)

let ports = {"http": 80, "https": 443};
let ssh = ports.get("ssh");
if (ssh.is_some()) {
    println("ssh is configured");
}
println(ssh.unwrap_or(22));
println(ports.get("https") ?? 443);
```

```text
22
443
```

Be careful with `Result`: there, `r.unwrapOr(0)` compiles in Loup 1.25 and then fails at run time with `Uncaught RuntimeError: internal interpreter limitation: … unwrapOr … method not found`. Write `unwrap_or` everywhere.

## `Result[T, E]`: Success or Failure

`Result[T, E]` has the variants `Ok(value)` and `Err(error)`. A function returns it when failure is an expected outcome the caller should handle:

```loup
#~ @mode(host.scripted)
func parsePort(text: string): Result[int, string] {
    if (text.length() == 0) {
        return Err("empty port");
    }
    for (ch in text.chars()) {
        if (not "0123456789".contains(ch)) {
            return Err(i"'{$ text $}' is not a number");
        }
    }
    return Ok(text.toInt());
}

for (input in ["8080", "80a", ""]) {
    let message = match parsePort(input) {
        Ok(port) => i"port {$ port $}",
        Err(problem) => i"error: {$ problem $}",
    };
    println(message);
}
```

```text
port 8080
error: '80a' is not a number
error: empty port
```

(`text.toInt()` alone returns `0` for text that is not a number, which is why the function checks the characters first.) Inside a function that itself returns a `Result`, the `?` operator unwraps an `Ok` or returns the `Err` to the caller at once; the evaluator at the end of this chapter uses it, and Chapter 10 covers it with the rest of error handling.

## Union Types

A union type `string | int | bool` holds a value of any of its member types. `match` takes it apart with type patterns, and checks that every member is covered:

```loup
#~ @mode(host.scripted)
func describe(value: string | int | bool): string {
    return match value {
        string(s) => i"text: '{$ s $}'",
        int(n) => i"number: {$ n $}",
        bool(b) => i"flag: {$ b $}",
    };
}

println(describe("hello"));
println(describe(42));
println(describe(true));
```

```text
text: 'hello'
number: 42
flag: true
```

For a single test, `is` narrows the type inside an `if` (§1-9.5):

```loup
#~ @mode(host.scripted)
func normalize(value: string | int): string {
    if (value is string) {
        return value.toUpperCase();    # value is a string here
    } else {
        return str(value + 1);         # and an int here
    }
}

println(normalize("abc"));
println(normalize(41));
```

```text
ABC
42
```

Use an enum when the cases have names and meanings of their own; use a union for an ad-hoc "one of these types".

## A Worked Example: A Command Parser

Enums are a good model for the commands of a small interactive tool. Parsing produces a `Result[Command, string]`, and running a command is a `match`:

```loup
@mode(host.scripted)

from core.string_utils import (isDigits);

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

enum Command {
    Quit,
    Echo(message: string),
    Move(direction: Direction, steps: int),
    Help(topic: Option[string]),
}

func parseDirection(word: string): Option[Direction] {
    return match word {
        "north" => Some(Direction.North),
        "south" => Some(Direction.South),
        "east" => Some(Direction.East),
        "west" => Some(Direction.West),
        _ => None,
    };
}

func parseCommand(input: string): Result[Command, string] {
    let words = input.trim().split(" ");
    let verb = words[0];
    return match verb {
        "quit" => Ok(Command.Quit),
        "echo" => Ok(Command.Echo(message = words[1:].join(" "))),
        "move" if (words.length() == 3) => {
            match parseDirection(words[1]) {
                None => Err(i"unknown direction '{$ words[1] $}'"),
                Some(d) if (isDigits(words[2])) => Ok(Command.Move(direction = d, steps = words[2].toInt())),
                Some(_) => Err(i"'{$ words[2] $}' is not a number of steps"),
            }
        },
        "move" => Err("usage: move <direction> <steps>"),
        "help" => Ok(Command.Help(topic = if (words.length() > 1) { Some(words[1]) } else { None })),
        _ => Err(i"unknown command '{$ verb $}'"),
    };
}

func run(cmd: Command): string {
    return match cmd {
        Quit => "Goodbye!",
        Echo(msg) => msg,
        Move(dir, steps) => i"moving {$ dir $} {$ steps $} steps",
        Help(Some(topic)) => i"help on {$ topic $}",
        Help(None) => "commands: quit, echo, move, help",
    };
}

for (line in ["echo hello there", "move north 3", "move up 3", "move west x", "help", "help move", "dance", "quit"]) {
    let out = match parseCommand(line) {
        Ok(cmd) => run(cmd),
        Err(problem) => i"error: {$ problem $}",
    };
    println(i"> {$ line $}");
    println(out);
}
```

*Listing 7-8: Parsing and running commands*

```text
> echo hello there
hello there
> move north 3
moving North 3 steps
> move up 3
error: unknown direction 'up'
> move west x
error: 'x' is not a number of steps
> help
commands: quit, echo, move, help
> help move
help on move
> dance
error: unknown command 'dance'
> quit
Goodbye!
```

Notice how `Help(Some(topic))` and `Help(None)` peel two layers in one pattern, and how the guard on the first `"move"` arm sends a command with the wrong number of words to the usage message. `isDigits` comes from the standard library module `core.string_utils`.

## A Worked Example: An Expression Evaluator

A recursive enum represents a tree of arithmetic, and two recursive `match` functions evaluate and print it. Evaluation returns a `Result`, so division by zero is an ordinary value rather than a crash:

```loup
@mode(host.scripted)

enum Expr {
    Num(value: float),
    Add(left: Expr, right: Expr),
    Mul(left: Expr, right: Expr),
    Div(left: Expr, right: Expr),
    Neg(inner: Expr),
}

func divide(a: float, b: float): Result[float, string] {
    if (b == 0.0) {
        return Err("division by zero");
    }
    return Ok(a / b);
}

func eval(expr: Expr): Result[float, string] {
    return match expr {
        Num(v) => Ok(v),
        Add(l, r) => Ok(eval(l)? + eval(r)?),
        Mul(l, r) => Ok(eval(l)? * eval(r)?),
        Div(l, r) => divide(eval(l)?, eval(r)?),
        Neg(inner) => Ok(-eval(inner)?),
    };
}

func show(expr: Expr): string {
    return match expr {
        Num(v) => str(v),
        Add(l, r) => i"({$ show(l) $} + {$ show(r) $})",
        Mul(l, r) => i"({$ show(l) $} * {$ show(r) $})",
        Div(l, r) => i"({$ show(l) $} / {$ show(r) $})",
        Neg(inner) => i"-{$ show(inner) $}",
    };
}

let three = Expr.Num(value = 3.0);
let two = Expr.Num(value = 2.0);

# 3 + 2 * -4.5
let good = Expr.Add(left = three, right = Expr.Mul(left = two, right = Expr.Neg(inner = Expr.Num(value = 4.5))));
# 3 / (2 + -2)
let bad = Expr.Div(left = three, right = Expr.Add(left = two, right = Expr.Neg(inner = two)));

for (e in [good, bad]) {
    let text = match eval(e) {
        Ok(result) => i"{$ show(e) $} = {$ result $}",
        Err(msg) => i"{$ show(e) $}: {$ msg $}",
    };
    println(text);
}
```

*Listing 7-9: Evaluating an expression tree with `Result` and `?`*

```text
(3 + (2 * -4.5)) = -6
(3 / (2 + -2)): division by zero
```

`eval(l)?` evaluates the left side and, if that produced an `Err`, returns it from `eval` immediately. (Whole-number floats print without `.0` — `3`, `-6` — when printed on their own.)

## Summary

- `enum Name { Variant(field: Type), Other, }` declares a closed set of variants; payload variants are constructed with named arguments (`Shape.Circle(radius = 2.0)`).
- `match value { Pattern => result, … }` is an expression; variant patterns bind payloads by position, `_` ignores, and `if (…)` guards refine an arm.
- A `match` on an enum must cover every variant; a `match` on literals needs a `_` arm or it throws `MatchError` at run time.
- One pattern per arm: there are no `A | B` or-patterns.
- `Option[T]` (`Some`/`None`) replaces null; use `match`, `?.`, `??`, `unwrap_or`, `is_some`. The 1.25 method names are snake_case.
- `Result[T, E]` (`Ok`/`Err`) reports expected failures; `?` propagates them (Chapter 10).
- Union types are matched with type patterns (`string(s)`) or narrowed with `is`.

## Try It Yourself

1. Define `enum Json { Null, Bool(b: bool), Num(n: float), Str(s: string), Arr(items: list[Json]) }` and write `render(j: Json): string` with `match`.
2. Add a `Teleport(x: int, y: int)` variant to `Command` in Listing 7-8 and run the script before handling it. Read the error, then add the arm.
3. Write `func firstWord(line: string): Option[string]` that returns `None` for a blank line, and print `firstWord(line) ?? "(blank)"` for a few lines.
4. Extend the evaluator with `Sub(left: Expr, right: Expr)` and make `show` print it.
