# Chapter 6: Enums and Pattern Matching

Enums carry a lot of the weight in freestanding code. Device states are enums, error types are enums, and so are the legal values of most register fields. Because `match` must be exhaustive, the code that handles an enum cannot silently drop a case — which matters when there is no exception to catch the case you forgot.

This chapter covers plain and data-carrying enums, `Option[T]` and `Result[T, E]`, the patterns `match` understands, union types, and why errors in freestanding are values rather than exceptions.

## Plain enums

A plain enum is a set of named variants, separated by commas. Variants are written with the enum's name, `LinkState.Up`, both when you construct one and in a `match` pattern:

```loup
@mode(freestanding)

enum LinkState { Down, Training, Up }

func ledPattern(s: LinkState): uint8 {
    return match s {
        LinkState.Down => 0x00u8,
        LinkState.Training => 0x55u8,      # blink
        LinkState.Up => 0xFFu8,
    }
}

func isUsable(s: LinkState): bool {
    return s == LinkState.Up               # enum equality is structural
}
```

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

The variants need the commas. Writing them one per line without commas, as in a C or Swift `enum`, fails at the second variant:

```loup,does_not_compile
@mode(freestanding)

enum LinkState {
    Down
    Training
    Up
}
```

```text
error[E0002]: expected `}`
 --> src/main.lpfree:5:5
  |
5 |     Training
  |     ^^^^^^^^
  |

error[E0001]: expected expression
 --> src/main.lpfree:7:1
  |
7 | }
  | ^
  |
```

One variant per line is fine — with a comma after each (a trailing comma after the last one is allowed). An enum must have at least one variant; an empty `enum E {}` is `E_ENUM_NO_VARIANTS`, and the uninhabited type is spelled `impossible`.

A fieldless enum is stored as its tag, one pointer-sized word (Language Reference §3-3.4.1). It does not have a guaranteed numeric encoding, so when a hardware register field has defined values, keep the register as an integer and convert explicitly:

```loup
#~ @mode(freestanding)
#~
enum Parity { None, Odd, Even }

# 16550 line control register: bits 3..5 select parity.
func parityBits(p: Parity): uint8 {
    return match p {
        Parity.None => 0x00u8,
        Parity.Odd => 0x08u8,
        Parity.Even => 0x18u8,
    }
}

func parityFromBits(lcr: uint8): Option[Parity] {
    return match lcr & 0x38u8 {
        0x00u8 => Some(Parity.None),
        0x08u8 => Some(Parity.Odd),
        0x18u8 => Some(Parity.Even),
        _ => None,                         # a reserved encoding
    }
}
```

*Listing 6-2: Converting between an enum and a register encoding*

## Data-carrying enums

A variant can carry named fields. Construct it with named arguments; in a pattern, bind the fields by position:

```loup
#~ @mode(freestanding)
#~
enum DiskError {
    Timeout,
    BadSector(lba: uint64),
    Mismatch(expected: uint32, actual: uint32),
}

func errorCode(e: DiskError): uint32 {
    return match e {
        DiskError.Timeout => 1u32,
        DiskError.BadSector(lba) => 2u32,
        DiskError.Mismatch(expected, actual) => 3u32,
    }
}

func checkCrc(expected: uint32, actual: uint32): Option[DiskError] {
    if (expected != actual) {
        return Some(DiskError.Mismatch(expected = expected, actual = actual))
    }
    return None
}
```

*Listing 6-3: Variants with payloads*

## Exhaustiveness

Every variant must be handled, either by its own arm or by a `_` wildcard arm. Add a variant to an enum and the compiler finds every `match` that has to change:

```loup,does_not_compile
@mode(freestanding)

enum LinkState { Down, Training, Up, Error(code: uint8) }

func ledPattern(s: LinkState): uint8 {
    return match s {
        LinkState.Down => 0x00u8,
        LinkState.Training => 0x55u8,
        LinkState.Up => 0xFFu8,
    }
}
```

```text
error[E0001]: Non-exhaustive match: missing variants Error
 --> src/main.lpfree:6:12
  |
6 |     return match s {
  |            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  |
```

Handle the new case explicitly rather than reaching for `_` — the next variant someone adds should also be flagged:

```loup
#~ @mode(freestanding)
#~
enum LinkState { Down, Training, Up, Error(code: uint8) }

func ledPattern(s: LinkState): uint8 {
    return match s {
        LinkState.Down => 0x00u8,
        LinkState.Training => 0x55u8,
        LinkState.Up => 0xFFu8,
        LinkState.Error(code) => 0xF0u8 | (code & 0x0Fu8),
    }
}
```

*Listing 6-4: Handling the new variant*

## Patterns and guards

`match` arms can use:

- variant patterns, with bindings: `DiskError.BadSector(lba)`;
- literal patterns: `0u32`, `true`, `"on"`;
- a binding pattern that names the whole value: `n`;
- the wildcard `_`;
- a guard after any pattern: `n if (n < 100u32) => …` (the guard condition is parenthesized like every other condition).

Arms are tried in order, so a guarded arm must come before a more general one.

```loup
#~ @mode(freestanding)
#~
# Bucket an interrupt latency in microseconds for a histogram.
func latencyBucket(us: uint32): uint8 {
    return match us {
        0u32 => 0u8,
        n if (n < 10u32) => 1u8,
        n if (n < 100u32) => 2u8,
        n if (n < 1000u32) => 3u8,
        _ => 4u8,
    }
}
```

*Listing 6-5: Literal patterns, bindings and guards*

## `Option[T]` and `Result[T, E]`

Two generic enums are built in: `Option[T]` with the variants `Some(value)` and `None`, and `Result[T, E]` with `Ok(value)` and `Err(error)`. The constructors are available everywhere without an import.

Use `Option` for "might be absent" — a table lookup, a free slot, a reserved encoding as in Listing 6-2. Use `Result` for "might have failed", with an enum as the error type:

```loup
#~ @mode(freestanding)
#~
enum RegError { BadOffset(offset: uint32), Busy }

var regs: buffer[uint32, 64];

func readRegister(offset: uint32): Result[uint32, RegError] {
    if (offset >= 64u32) {
        return Err(RegError.BadOffset(offset = offset))
    }
    return Ok(regs[offset as int])
}

func readAndIncrement(offset: uint32): Result[uint32, RegError] {
    let current = readRegister(offset)?      # an Err returns from this function
    return Ok(current + 1u32)
}

func firstFreeSlot(used: uint64): Option[int] {
    var i = 0
    while (i < 64) {
        if ((used & (1u64 << (i as uint64))) == 0u64) {
            return Some(i)
        }
        i = i + 1
    }
    return None
}

func slotOrDefault(used: uint64): int {
    let slot = firstFreeSlot(used)
    if (slot.is_some()) {
        return slot.unwrap()
    }
    return firstFreeSlot(0u64).unwrap_or(0)
}
```

*Listing 6-6: `Result` with an error enum, `?`, and `Option` helpers*

The helper methods use snake_case names: `is_some()`, `is_none()`, `is_ok()`, `is_err()`, `unwrap()`, `unwrap_or(default)`, `unwrap_err()`. (The Language Reference spells some of them in camelCase, such as `unwrapOr`; use the snake_case names the toolchain implements.) `x ?? default` is shorthand for unwrapping an `Option` with a fallback.

`unwrap()` on `None` is a panic. In freestanding a panic is a stop handled by the package's panic policy — halt the core by default, or call your `@panic_handler` under `panic = "custom"` (chapter 17). Use `unwrap()` only where `None` really is impossible, and prefer `match`, `??` or `unwrap_or` everywhere else.

Chapter 9 covers `Result`, `?` and error conversion in depth.

## Union types

A union type lists alternatives inline: `uint32 | bool` holds either a `uint32` or a `bool`. `T | None` is the same type as `Option[T]`. You narrow a union with type patterns in a `match`, or with `is` in an `if`:

```loup
#~ @mode(freestanding)
#~
# A boot option is either a number or an on/off switch.
func parseOption(s: string): uint32 | bool {
    if (s == "on") { return true }
    if (s == "off") { return false }
    return 0u32
}

func optionWeight(v: uint32 | bool): uint32 {
    return match v {
        uint32(n) => n,
        bool(b) => if (b) { 1u32 } else { 0u32 },
    }
}

func isNumeric(v: uint32 | bool): bool {
    if (v is uint32) {
        return true
    }
    return false
}
```

*Listing 6-7: A union type narrowed by type patterns and `is`*

Type patterns are exhaustive too: leave out the `bool(b)` arm and the compiler reports `Non-exhaustive match: missing types bool`. In freestanding every member of a union must be a concrete type known at compile time; `Any` is not allowed, because naming its type at run time would need reflection metadata. Prefer an enum when the alternatives mean something — a named variant documents itself; a union is for genuinely ad-hoc "this or that type" values.

## Errors are values in freestanding

In the hosted modes a function can `throw` and a caller further up can `catch`. A freestanding image has no unwinder and no `.eh_frame`, so exceptions do not exist there. A habit from hosted code is caught at once:

```loup,does_not_compile
@mode(freestanding)

enum UartError { Timeout, Framing, Overrun }

func readByte(ready: bool, data: uint8): uint8 {
    if (not ready) {
        throw UartError.Timeout
    }
    return data
}
```

```text
error[E_FREESTANDING_EXCEPTIONS]: E_FREESTANDING_EXCEPTIONS: `throw` is not available in `@mode(freestanding)` — the mode has no unwinder and no `.eh_frame`. Use `Result[T, E]` with `?` or `attempt` (§1-5.10).
 --> src/main.lpfree:7:9
  |
7 |         throw UartError.Timeout
  |         ^^^^^^^^^^^^^^^^^^^^^^^
  |

error[E_THROW_NON_ERROR]: cannot throw value of type 'UartError': thrown values must extend Error
 --> src/main.lpfree:7:9
  |
7 |         throw UartError.Timeout
  |         ^^^^^^^^^^^^^^^^^^^^^^^
  |
```

The first error is the freestanding rule. The second would apply in any mode: only exception classes can be thrown, not enum values. The freestanding version returns the error as a value:

```loup
#~ @mode(freestanding)
#~
enum UartError { Timeout, Framing, Overrun }

func readByte(ready: bool, data: uint8): Result[uint8, UartError] {
    if (not ready) {
        return Err(UartError.Timeout)
    }
    return Ok(data)
}

func readPair(ready: bool, a: uint8, b: uint8): Result[uint16, UartError] {
    let high = readByte(ready, a)?
    let low = readByte(ready, b)?
    return Ok(((high as uint16) << 8) | (low as uint16))
}
```

*Listing 6-8: Returning an error enum instead of throwing*

Every enum you define for a subsystem is likely to end up as somebody's error type, and its variants are the list of things that can go wrong.

## Summary

- Enum variants are comma-separated and written `Enum.Variant`; variants can carry named fields.
- `match` is exhaustive — missing variants (or union member types) are compile errors; guards are `pattern if (condition)`.
- A fieldless enum has no guaranteed encoding; map register values explicitly.
- `Option[T]` and `Result[T, E]` are built in; their helpers are snake_case (`is_some`, `unwrap_or`); `unwrap()` on `None` is a panic under the package's panic policy.
- Union types such as `uint32 | bool` are narrowed with type patterns or `is`; `Any` is not allowed in freestanding unions.
- `throw` is `E_FREESTANDING_EXCEPTIONS`; return `Err(...)` and propagate with `?`.

Next: classes and allocators.
