# general todos

- sum-type ABI/layout polish
  - consider dynamic tag-width shrinking after the fixed-`u16` ABI has real pressure
  - consider all-void channel collapse after fallible channels are otherwise stable
  - define cross-module/global-id ABI determinism before multi-module builds depend on it
  - keep backed/C enum composition and must-consume fallible linting as later policy work

# milestones

1. interop type foundation (implemented)
  - unsigned integers, floats, and target-dependent c scalar types
  - atomic `c_*` primitive types remain distinct until target-aware lowering
  - `c_func`, complete `c_struct`, and pointer-only `opaque`; `c` remains an ordinary identifier
  - keep binding mutability (`::` / `=`) separate from element or pointee mutability (`mut`)
  - arrays and indexing
    - `[N]T`: array with `N` logical elements
    - `[N;S]T`: array with `N` logical elements followed by sentinel `S`
  - pointers
    - `@T` / `@mut T`: non-null single-item pointer without arithmetic
    - `*T` / `*mut T`: non-null many-item pointer with arithmetic
    - optional pointers represent nullable pointers (i.e. `?@T` / `?@mut T`, `?*T` / `?*mut T`)
  - slices and slicing
    - `[]T`: pointer and length
    - `[;S]T`: pointer and length with a sentinel invariant
    - ordinary slices do not guarantee null termination
  - string literals as immutable sentinel slices backed by static arrays (superseded by milestone 3.5)
  - character literals
  - optionals with trapping unwrap and fallback operations
  - native structs with compiler-controlled layout
  - complete `c_struct` support with target c layout and pointer-only `opaque` records
    - `Some :: c_struct { ... }`: defined c-layout struct
    - `Some :: opaque`: incomplete nominal record
    - passing c structs by value was deferred until milestone 4.1

2. restricted c header imports (implemented)
  - treat an imported header as a synthetic, file-local package namespace
    - `native :: import "relative/path/to/header.h"`
  - import functions, typedefs, scalar types, and pointers to opaque records
  - keep implementation linking separate from header imports
  - cache imports by canonical header path and target/include/define configuration
  - diagnose unsupported declarations when referenced
  - dynamically load libclang behind a replaceable c importer boundary

3. c variadic calls (implemented)
  - represent c variadics as a fixed parameter count plus a variadic flag
  - apply c default argument promotions at call sites
  - emit LLVM c-variadic declarations and calls
  - keep native brolang variadics and tuple design separate

3.5. sentinel pointers and c strings (implemented)
  - add sentinel many-item pointers: `[*;S]T`
  - represent string literals as immutable pointers to statically stored sentinel arrays: `@[N;0]u8`
  - arrays expose `.len` but no `.ptr`; slices and pointers-to-arrays expose sentinel-preserving `.ptr`
  - allow pointer-to-array `.len`, indexing, slicing, pointer decay, and slice construction without explicit dereference
  - preserve or forget sentinel information through compatible pointer and slice coercions without copying arrays
  - allow zero-terminated immutable byte pointer views to convert to immutable `*c_char` and `[*;0]c_char`
  - keep `u8` and `c_char` distinct to preserve target-dependent scalar c semantics
  - reject general `u8`/`c_char` interchange, slice-to-pointer coercion, and conversion to mutable c character pointers

4. advanced c interop
  - by-value records and unions (implemented)
    - complete plain imported structs/unions and manual `c_struct` values
    - fixed C arrays inside imported records
    - keyed struct literals and exactly-one-field union literals
    - field reads/writes, storage, and fixed-signature calls/returns
    - aarch64-macos small aggregate, homogeneous float aggregate, and indirect ABI lowering
    - keep incomplete, bitfield, packed, flexible-array, qualified-field, and otherwise non-plain records pointer-only
    - keep C variadic record arguments unsupported
  - function pointers and callbacks (implemented)
    - imported C function pointer typedefs lower to nullable pointer types
    - manual `?*c_func(...) T` callback type spelling
    - concrete `c_func` declarations/definitions can be passed as callback values
    - postfix calls through non-null function pointers, including `callback?(...)`
    - fixed and C-variadic callback ABI emission through LLVM indirect calls
  - external variables (implemented)
    - imported external C object variables lower to direct LLVM external global references
    - top-level `const` object variables are read-only from brolang
    - mutable external scalars/records can be assigned through qualified package globals
    - unsupported variable types remain lazy diagnostics when referenced
  - object-like macro constants (implemented)
    - scalar integer/float literal macros import as immutable globals
    - `CLITERAL(Type){ ... }` / `(Type){ ... }` record literal macros import as immutable globals
    - function-like macros and non-literal macro expressions remain unsupported
  - static inline functions (implemented)

5. control flow (implemented)
  - boolean expressions (implemented)
    - `bool` type with `true` / `false` literals
    - comparison operators: `==`, `!=`, `<`, `<=`, `>`, `>=` (numeric operands widen; `bool` supports only `==` / `!=`)
    - operators: `and`, `or`, `!`
    - lazy evaluation / short-circuit evaluation
  - if statements (implemented). example: `if condition { ... } else if { ... } else { ... }`
    - conditions must be `bool`; block-scoped locals do not escape their blocks
    - lowered through new `Label` / `Br` / `Cond_Br` IR opcodes (alloca-backed locals, no phi nodes)
  - conditional unwrapping for optionals (`?T`) (implemented): `if val |v| { ... } else { ... }` - unwrap `val` into `v` if it is not `null`
    - single immutable binding scoped to the then-block; `v` not visible in `else` or after the `if`
    - `|` lexes as a new `Pipe` token; the `.If` reuses AST `name` / HIR `local` to carry the binding (no new statement kind)
    - new `Optional_Is_Some` / `Optional_Value` IR opcodes (the `Unwrap` presence-test + extract, minus the trap)
  - conditional unwrapping with guard clause (implemented):  `if val |v : v >= 10| { ... } else { ... }` - enter the then-block when `val` is not `null` and the guard is true
  - multi-unwrap (implemented; see section below)
  - while loops (implemented; operates on boolean conditions). examples:
    - `while condition { ... }` - iterate while the condition is true
    - `while condition : i = i + 1 { ... }` - execute the update after each completed iteration
    - the condition and update may be parenthesized independently for visual clarity
    - update targets must already be declared and mutable; loops do not introduce implicit induction variables
    - update clauses support ordinary and compound assignment
  - ranges (implemented; see section below)
  - for loops (implemented; operates on ranges, arrays, slices, and pointers-to-arrays). examples:
    - `for items |item| { ... }` - capture just the `item` value in the array/slice (uses copy semantics, i.e. gets a `T`)
    - `for (&items) |@item| { ... }` - capture a pointer to each array element; its `@T` / `@mut T` mutability follows the iterable
    - `for items_slice |@item| { ... }` - slices already refer to backing storage and support pointer capture directly
    - `for items |item, idx| { ... }` - capture `item` and its index index in the array/slice
    - `for 0..10 |i| { ... }` - iterate over the range `0..10` (exclusive)
    - `for 0..=10 |i| { ... }` - iterate over the range `0..10` (inclusive)
    - `for 0..(len) |i| { ... }` or equivalently `for 0..=(len - 1) |i| { ... }` - calculating range bounds, expressions must be parenthesized
  - for all conditionals/guards, parentheses are optional but allowed for visual clarity

6. compound assignment: `+=`, `-=`, `*=`, `/=` (implemented; division semantics superseded by milestone 32)
  - added the binary arithmetic operators `-`, `*`, `/` (previously only `+` existed); `*`/`/`
    bind tighter than `+`/`-`, and prefix `-` (negation) is unchanged
  - compound assignments preserve the target, operator, and right-hand side explicitly through
    parsing and checking; lowering computes the target address once, then loads, applies the
    operation, and stores through that address
  - side-effecting index, field-base, and dereference expressions are evaluated once in
    left-to-right order
  - integer `+`, `-`, and `*` trap on overflow; milestone 32 later restricted `/` and `/=` to
    floats and introduced the explicit integer/float division family
  - constant folding (global initializers) covers the arithmetic family

7. enums (native and c interop) (implemented; see below)
  - native enums are nominal value types with integer runtime representations
  - unbacked enums are non-empty, dense, zero-based, and use the smallest fitting unsigned backing
  - explicitly backed enums require an integer type and strictly increasing literal values
  - enum members support `Type.member`, `package.Type.member`, and contextual `.member`
  - enum values support storage, calls/returns, and same-type equality/inequality
  - explicitly backed native enums use their backing ABI in `c_func` signatures and variadic promotion
  - imported C enum types alias libclang's target-selected integer backing and enumerators import as package constants

8. distinct types (implemented; see below)
  - nominal declarations preserve identity across packages and reuse the backing runtime representation
  - construction uses `Type(value)` with exactly one value of the exact backing type
  - no implicit conversion to or from the backing type
  - backing-type operators and reverse explicit conversions remain deferred
  - concrete runtime backing types are supported; unresolved, `int`, `void`, function, and opaque backings are rejected

9. allow pointer field access pass-through (implemented)
  - having a pointer (`ptr`) to a struct, we should allow access through `ptr.field` as opposed to mandating `ptr^.field`

10. make slice expressions on array variables implicitly address-taking (implemented)
  - zig's slice expression on an array variable handles the address-taking implicitly (nice ergonomics)
  - `arr[a..b]` on an array variable now slices without the explicit `&`; the
    explicit `(&arr)[a..b]` pointer-to-array form keeps working unchanged
  - array rvalues (e.g. a by-value array return) are materialized into a
    temporary before slicing, matching the for-loop iterable lowering

11. c header imports and automatic native brolang bindings (implemented)
  - `brolang translate-c <header.h> [--target ...] [--c-include-path ...] [--c-define ...]`
    prints native `.bro` bindings for a C header to stdout (the offline counterpart of the
    in-memory `native :: import "x.h"`); reuses the libclang `cimport.Result`
  - emitter lives in `compiler/translatec`; `render_type` mirrors `loader.translate_c_type`
    one-to-one so emitted source re-parses to identical types (guarded by a round-trip test)
  - added a native type-alias declaration `Name :: alias T` (parser/lexer/token surface; the
    `types.define_alias` / `.Alias` machinery already existed) so C typedefs and callback
    typedefs round-trip
  - emits functions, complete structs and opaque records (collapsing `typedef struct {...} Foo`),
    typedef aliases, and scalar/aggregate/enum-member constants
  - C unions, external variables, static-inline functions, and unsupported declarations have
    no hand-writable spelling and are emitted as `# unsupported in bindings:` comments
    (functions that reference an un-spellable union therefore keep a dangling reference)

11.1. opaque, anyopaque, and pointer casts (implemented; v1)
  - `Name :: opaque` is the incomplete nominal record spelling; bodyless `c_struct` is invalid
  - `anyopaque` is the erased object type used behind pointers for C `void*` and allocator contexts
  - C `void` function results remain `void`; C `void*` / `const void*` import and render as `?*mut anyopaque` / `?*anyopaque`
  - `ptrcast!(T, ptr)` preserves pointer shape and only changes the child type in v1
  - future direction: generalize toward Zig-style arbitrary pointer-result casts once casts have a broader result-type story

12. `undefined` as inspired by zig (implemented):
  - allow mutable local declarations with `undefined`
    - undefined values are assigned a poison value (0xaa...)
    - allows for something like:
      ```
      a int = undefined
      if (condition) {
          a = 42
      } else {
          a = -2
      }
      ```
  - disallow: `b :: undefined` since assigning undefined to something that can't change defeats the purpose
  - disallow assigning `undefined` after declaration; use optionals and `null` for values that intentionally move back to an empty state

13. introduce `float` and `range` type constraints (the `int` family generalized) (implemented)
  - `float` resolves a local binding to any float scalar (`f32`/`f64`) via static analysis;
    widens `f32` -> `f64` across assignments, mirroring how `int` picks the smallest integer
  - on a local declaration, integer literals satisfy `float` and default to `f64`
    (`pi float = 3` is `3.0`); a runtime integer (`x float = some_i32`) stays a
    `cannot implicitly convert` error
  - `range` is now a spellable type/constraint: `r range :: 0..10` resolves to the inferred
    range (element type preserved), and `func(start, end int) range { return start..end }`
    monomorphizes the result per call. this replaces the prior `int`-as-passthrough hack that
    was the only way to forward a range through a function
  - `int`/`float`/`range` constraints now gate by family in every position (previously
    params/results were unchecked generic passthroughs):
    - a local initializer out of family errors instead of silently taking the natural type
    - a param rejects an out-of-family argument (`cannot pass f64 to 'int' parameter 'x'`);
      a `float` param accepts an integer-literal argument as f64 (e.g. `f(3)`)
    - a function result is narrowed to the constraint's family

14. broaden type inference to surrounding context (implemented)
  - a slot's concrete type is the join of demands reachable from its declaration,
    flowing backward as well as forward to a fixpoint (the existing global/spec
    fixpoint in `infer_all`), generalizing milestone 13's forward-only resolution
  - an "open constant" (a global or local with no concrete annotation plus a compile-time
    integer initializer) is sign-agnostic until used: a backward demand from any reachable
    use picks its family/width as long as the value fits, so `A :: 10` followed by
    `B u16 :: A` resolves both to u16 — the literal's smallest-signed default no longer
    blocks an unsigned demand; absent any demand it defaults to the smallest signed type
  - a concrete declared type flows backward through a chain of bare-name references:
    `X :: 1000; Y int :: X; Z i32 :: Y` resolves X and Y to i32 (previously they stayed
    at the literal's i16)
  - locals resolve identically to globals (no scope asymmetry): demands flow through
    bare-name typed declarations, call arguments (a concrete parameter type demands its
    argument, e.g. `take_u16(a)`), and returns — including from inside a function body
    back onto a referenced global
  - at this milestone, demands flow only through bare names; they do not cross arithmetic
    or other operators, nor back across a call's result (arithmetic is milestone 15;
    result-to-argument direction is milestone 14.5)
  - a non-fitting or family-conflicting demand is not applied (first demand wins); the
    genuine mismatch then surfaces as the usual boundary coercion error at the use
    (e.g. `C u8 :: BIG` where `BIG :: 100000`)

14.5. backward type-demand propagation through call boundaries (DEFERRED)
  - a callee's result/return demand flows back through the function body to constrain
    the caller's arguments, so `R u32 :: echo(A)` (with `echo func(p int) int`)
    resolves A to u32 instead of erroring at the call's result coercion
  - requires reversing the per-call data flow: a specialization's argument types
    (`spec.args`) become outputs to solve, not just inputs — a new back-edge threaded
    through every call site and the specialization fixpoint
  - only meaningful on top of milestone 14's open constants

15. broaden type inference to infer type of declaration based on arithmetic expressions too (implemented)
  - backward contextual demands now flow through numeric arithmetic (`+`, `-`, `*`, `/`, unary `-`)
    for integer and float open constants
  - integer literals can adopt integer or float arithmetic context; float literals can adopt `f32`/`f64`
  - unannotated declarations initialized by arithmetic expressions adopt the concrete numeric operand type
  - e.g.
    ```
    a :: 1
    b i32 :: a + 2 # a is constrained to `i32`
    c :: b + 3     # c is constrained to `i32`
    ```

16. allow brace-less single-statement `if` and `for` bodies (implemented)
  - brace-less bodies must wrap the condition or iterable in parentheses UNLESS it's a function call
  - brace-less single-statement bodies apply to the then-body, the `else`-body, and the
    unwrap/guard forms (`if (v) |x| stmt`); each branch is independent, so braced and
    brace-less branches mix freely
  - the parenthesize-or-call rule constrains `if` then-branch conditions (including unwraps)
    and `for` iterables; `else` bodies have no preceding expression to constrain
  - the brace-less statement may sit on the following line
  - parser-only change (`parse_control_body` in `compiler/parser/parser.odin`): a brace-less
    body is just a 1-element statement slice, so the checker and codegen are unchanged

17. multi-line strings (implemented; see below)
  - a multi-line string is an ordinary string literal under the hood: it lowers to the
    same `.String` expr / `@[N;0]u8` type, so the checker, lowering, and codegen are
    unchanged. only the lexer and parser change.
  - the lexer (`compiler/lexer/lexer.odin`, `` case '`' ``) collapses consecutive
    backtick-marked lines into one `Multiline_String` token; the trailing newline after
    the last line stays a `.Newline` so it terminates the statement normally
  - the parser (`decode_multiline_string` in `compiler/parser/parser.odin`) strips each
    line's leading indentation and `` ` ``, takes the rest of the line raw (no escapes),
    and joins lines with an implicit `\n` (no leading/trailing newline); an empty
    `` ` `` yields a blank line
  - the value may sit on the line after `=`/`::` (the existing post-operator
    `skip_newlines` already allows this)
  - `++` concatenation (the spec's "mixing" examples) is a separate, unimplemented
    operator and is out of scope here

18. add `break` and `continue` statements (implemented)
  - `break` exits the innermost enclosing loop; `continue` skips to that loop's next
    iteration (running the `while` update / `for` index increment first). Both target
    the innermost loop only (no labeled break) and carry no value
  - new `Keyword_Break`/`Keyword_Continue` tokens; `Break`/`Continue` AST and HIR
    statement kinds (no fields beyond kind/span); parsed by `parse_loop_control`
  - the checker tracks loop nesting (`Build_Ctx.loop_depth`, bumped around loop-body
    builds) and rejects `break`/`continue` outside a loop; `all_paths_return` no longer
    treats a `while true` whose body can `break` as non-terminating (so a non-void
    function that breaks out without returning is correctly diagnosed)
  - lowering keeps an innermost-last loop-target stack (`State.loops`): `break` branches
    to the loop's exit label, `continue` to its update/latch label. The range-for routes
    `continue` through the end-of-iteration bounds/overflow guard, so
    `for 0..=255 |b: u8|` exits cleanly instead of overflowing the increment
  - the LLVM emitter opens a fresh recovery block after any terminator (not just `ret`),
    so dead code following a `break`/`continue` branch stays well-formed

19. add `defer` statement (inspired by zig) (implemented; also adds bare block statements)
  - `defer <stmt>` runs the statement when the enclosing block scope exits, in reverse
    (LIFO) order, on every exit path: fall-through, `return`, `break`, `continue`. The
    deferred statement may be a block (`defer { ... }`)
  - `errdefer [|error|] <stmt>` is the fallible-function counterpart: it runs only when
    an explicit or `try`-propagated error exits its active block scope, can capture the
    widened enclosing error, and stays interleaved with ordinary defers in LIFO order
  - bare block statements `{ ... }` were added as the enabling feature: a `{ ... }`
    introduces a nested scope (locals are name-scoped to it; defers inside it fire at the
    closing brace). A leading `{` is unambiguous since struct literals are postfix only
  - the return value is captured *before* defers run (a `defer` that mutates the returned
    local can't change what is returned) — the checker spills the return value into a temp
    local, then flushes, matching Zig
  - `return` flushes all active defers; `break`/`continue` flush only down to and including
    the innermost loop body; fall-through flushes the current block's own defers. Deferring
    a `return`/`break`/`continue`/`defer`, a `return` inside a `defer`, or a `break`/
    `continue` that would escape a `defer` are all rejected
  - cleanup is statically expanded with no runtime registration stack; `try` carries its
    active error-exit cleanup into lowering so propagation cannot bypass either defer form

20. add `yield` statement (implemented; first pass — value blocks only; see below)
  - a `{ ... }` on the right of a declaration or assignment is a *value block*: its final
    statement must be `yield <expr>`, which supplies the block's value (the block analogue
    of `return`). Supported: `x :: { ...; yield v }` (untyped — the local takes the yield's
    natural type), `x T = { ... }` (coerces to `T`), and `target = { ... }` (coerces to the
    target's type, including complex targets like `a[i] = { ... }`)
  - the yielded value is captured *before* the block's defers run (a defer that mutates a
    block local can't change what is yielded), reusing the `return` spill-to-temp pattern
  - `yield` is valid *only* as the final statement of a value block. A `yield` nested in an
    `if`/loop/inner block, or in a non-value block, is rejected ("'yield' is only valid as
    the final statement of a value block"); a value block not ending in `yield` is rejected
    too. This no-early-exit restriction keeps it a lexer/parser/checker-only change with no
    HIR/lowering touch (like milestones 18/19)
  - new `Keyword_Yield` token + `.Yield` AST stmt (reuses `expr`); a block-initialized
    `Declaration`/`Assignment` reuses the existing `body` field with `expr` invalid. The
    checker's `build_value_block` builds the leading statements (via `build_block` with a
    new `close=false` flag that keeps the scope open), evaluates the final yield, spills and
    flushes the block's defers, then feeds the value into an ordinary `Declaration`/
    `Assignment`. HIR never holds a `.Yield` (final yield → `Declaration`/`Assignment`,
    misplaced yield → `Trap`), so lowering/codegen are unchanged
  - deferred to a later milestone (needs labeled blocks + rules for whether an `if`/loop
    always produces a value, e.g. optionals): yield from inside `if`/loops, labeled blocks
    (`blk: { yield :blk v }`), implicit trailing-expression yield, and yield in match arms /
    `catch` handlers (milestones 21–22)

20.5 `yield` from if-statements and loops (implemented; see below)
  - an `if`/`for`/`while` on the right of a declaration or assignment is now a *value
    source*, governed by the rule **if one path yields, all paths must yield** (no
    optionals-as-a-crutch, so the value is always present and never needs unwrapping):
    - value-if: `result :: if a { yield 1 } else if b { yield 2 } else { yield 3 }` — a
      mandatory `else`, every branch ends in `yield`, all branches share a type (the first
      branch fixes it when untyped; later branches coerce). Typed `T =` and reassignment
      `target = if …` are supported too
    - value-loop: a labeled body `for/while … blk: { … }` whose early exits are
      `yield :blk x` and whose body ends in an unlabeled fall-through `yield` (the value
      when the loop completes). The `{T, null}` yields resolve the result to `?T`
      (a pure-AST `null`-scan picks optionality; the first concrete yield fixes the element
      type). E.g. `active_ent_idx :: for 0..10 |i| blk: { if (cond) yield :blk i; yield null }`
      resolves to `?usize`
  - new `blk:` / `yield :blk` label surface adds one `label` field to the AST `Stmt`; no new
    token (`blk:` is `Identifier Colon`, `:blk` is `Colon Identifier`). The parser carries a
    value `if`/`for`/`while` as a one-element block-init `body` (the same `expr`-invalid
    signal a value block uses)
  - **no HIR/lowering change** (like 18/19/20): a value-if/loop desugars in the checker to a
    mutable result *slot* (a poison-/fall-through-initialized local) that branches/iterations
    assign and that is read after the construct — the existing alloca-backed local flow. A
    `yield :blk x` desugars to `slot = x; break`, reusing the milestone-18 `Break` lowering.
    Each value-if branch is a `build_value_block` call; the value-loop reuses the ordinary
    `.For`/`.While` build via a peeled-body copy. HIR never holds a `.Yield`
  - errors: an `if` value without `else`; a branch/value-block not ending in `yield`; a value
    loop body without a trailing fall-through `yield`; a `yield :blk` with no matching value
    loop. The TODO "BAD" loops (unlabeled yield from inside an `if`, an unbound labeled loop)
    fall out of these naturally
  - follow-ups: a branch that early-`return`s instead of yielding, unwrap-`if` as a value
    source, and `null`-before-concrete typing in untyped loops are done in 20.6; labeled value
    blocks and `yield`/`break` to an outer loop are done in 20.7

20.6 value if/loop follow-ups (implemented; checker-only)
  - a value-if branch may end in `yield` **or** exit on every path (`return`/`break`/
    `continue`) — `r :: if (ok) { yield x } else { return -1 }`. A non-terminating, non-yielding
    branch is rejected ("a value branch must end with 'yield' or exit on every path"). Checked
    via `all_paths_exit` on the built branch in `emit_value_branch`
  - unwrap-`if` as a value source: `name :: if opt |v| { yield v * 2 } else { yield d }`.
    `emit_value_if` gained an unwrap path mirroring the build-pass `.If` unwrap arm (captures +
    guard), each branch assigning the slot; the HIR `.If` carries the unwraps, which the existing
    lowering already handles. (The simple "unwrap or fallback" case is just `orelse` —
    `name :: opt orelse d` — already a plain expression.)
  - untyped value loops pre-type their element from the first concrete (non-`null`) yield
    regardless of source order (a capture-scoped probe build, `value_loop_element_type`), so a
    `null` yielded before any concrete value still resolves the result to `?T`
  - still checker-only; no HIR/lowering change

20.7 labels — value blocks + yield/break to an outer loop (implemented; first lowering change)
  - `x :: blk: { …; yield :blk v }` — a labeled value *block* (the disambiguated form of "an
    if/loop at the end of a block"; an unlabeled trailing if/loop stays ambiguous and is not a
    value source). `yield :blk v` exits the block with a value; every path must yield. Carries
    the same `{T, null}` → `?T` typing, defer-capture, and reassignment forms as value loops
  - `yield :outer v` to an enclosing (non-innermost) value loop/block, plus plain `break :L` /
    `continue :L` to an enclosing labeled loop
  - a label now names a first-class exit target: `label` added to the HIR `Stmt` (on
    `.While`/`.For`/`.Break`/`.Continue`) and a new HIR `.Block` kind (lowers to its body + an
    exit label). The lowering's `Loop_Ctx`/`State.loops` became a label-keyed exit-target stack
    (`is_loop` distinguishes loops from value blocks; plain `break`/`continue` take the innermost
    loop, a labeled one searches by label). The checker tracks a `loop_labels` stack and a
    `Yield_Target.defer_floor`; a `yield :L v` desugars to `slot = v; flush defers to L's body;
    break :L`, reusing the milestone-18 break lowering — **no new IR opcode, no emitter change**
  - a labeled bare block as a *plain statement* is also exitable with `break :blk` (a HIR
    `.Block` break target; not a loop, so unlabeled `break`/`continue` and `continue :blk` skip
    it). The checker tracks a parallel `loop_is_loop` stack so labeled `break` reaches a loop or
    block while `continue` and unlabeled `break`/`continue` reach only the innermost loop
  - untyped block `null`-before-concrete typing now builds the block's leading (yield-free)
    statements first (a throwaway probe), so a first concrete `yield :blk` that references a
    block local still resolves the result to `?T`
  - deferred (`// ponytail:`): the same `null`-before-concrete typing in an untyped block (or
    loop) whose concrete yield references a local declared *past* the first yield (annotate)

21. unions and tagged unions (implemented; first pass — native untagged unions only; see below)
  - inspired by zig
  - ```
    # unions (untagged; named fields like a struct — Zig union members are always named)
    SomeStuff :: union {
        f float
        i int
        a Animal
    }

    # tagged unions (constrained to the backing enum) — see 21.5
    AnimalNameOrHeight :: union(Animal) {  # use just `enum` instead of `Animal` for unconstrained tagged union
        dog []u8
        cat []u8
        bird int
        lizard int
    }
    ```
  - this first pass ships **native untagged unions with named fields**. an untagged union is a
    carrier sized to its largest/most-aligned member with no runtime tag (a C union); reading a
    non-active field reinterprets the bytes (unsafe, like a Zig `union {}` in ReleaseFast) and an
    untagged union is not matchable. the TODO's original bare type-only spelling
    (`union { float, int, Animal }`) was dropped in favor of named fields to match Zig and to
    reuse the existing field-access path
  - the entire untagged-union backend already existed from the C-interop work (milestone 4): the
    `.Union` type kind, carrier-based LLVM layout (`emit_types`), `size`/`alignment`, keyed-literal
    construction `Val{ field = value }` (exactly one initializer; the active-field index rides in
    `hir.Expr.integer`), and field access (GEP-at-offset-0 reinterpret). none of these are gated on
    `c_layout`, so a *native* union flows through them unchanged. record-declaration validation
    (`is_runtime_value` per field) already covers native unions too
  - the change is front-end only: a new `union` keyword (token/lexer), and `parse_union` is just
    `parse_struct` parametrized with `is_union=true` routing through `types.define_record`
    (native-only, so `c_layout=false` and the no-body case errors). no `types`/`checker`/`hir`/
    `lower`/`llvm` change (mirrors the minimal-footprint slices of 18/19/20)
  - deferred to **21.5**: tagged unions (`union(Enum)` constrained + `union(enum)` inferred) with a
    runtime `{tag, payload}` layout, tag init at construction, and tag extraction — the real codegen
    work and the foundation for milestone 22 (`match` with payload unwrapping)

21.5. tagged unions with a runtime tag (implemented; see below)
  - `union(Enum)` (variant names constrained to an existing enum's members) and `union(enum)`
    (compiler-synthesized anonymous tag enum, one dense 0-based member per variant); both store
    the discriminant beside the payload as `{tag, payload}`
  - a tagged union is a first-class `.Union` type node whose `child` holds the tag enum (untagged
    unions and structs leave `child` INVALID). its fields are the variants (payload types), still
    looked up by name. `union(enum)` synthesizes its tag enum up front (`types.enum_anonymous`,
    variant names as members), so both forms converge on one representation
  - **key reuse:** the per-variant tag value is *derivable* from `(union type, active variant
    index)` — the variant's field name matches a member of the tag enum, whose value is the tag.
    so codegen computes the tag itself and the existing `.Struct`/`.Field` HIR (carrying `type` +
    active/field `integer`) is sufficient. **no HIR/IR/lowering change** (like 18/19/20); the delta
    is parser + types layout + one checker validation + three LLVM emit sites
  - runtime layout `{tag, payload-carrier}`: tag at offset 0, payload carrier at
    `payload_offset = round_up(sizeof!(tag), payload_align)` (`types.union_payload_offset`, shared by
    `size` and the emitter). field access uses byte-offset GEPs, so offsets stay self-consistent
  - construction `T{ variant = value }` reuses the union-literal path and additionally stores the
    derived tag; payload read `x.variant` reuses field access, reading at the payload offset
    (unchecked reinterpret, Zig-style). safe tag dispatch + payload capture is milestone 22 (`match`)
  - changes: `parse_struct` parses `union(...)` (`enum` → synthesize; else an existing enum);
    `types.define_record` takes a `tag` param stored in `child`, plus `is_tagged_union` /
    `union_tag_enum` / `union_payload_offset` helpers and tagged `size`/`alignment`; the checker
    record-decl pass requires the tag to be an enum and each variant to name a member of it; the
    LLVM emitter lays out `{tag, [pad], carrier, [pad]}`, stores the tag in construction, and offsets
    payload field access
  - deferred to milestone 22 (`match`): safe tag dispatch + payload capture (`match x { .bird |v| … }`),
    exhaustiveness checking, and any first-class tag-read accessor

22. match statements with tagged unions payload unwrapping (implemented; see below)
  - `match <subject> { <arm>* }` dispatches on a tagged union, a plain enum, or a scalar
    value. Arms are `.variant [|cap|]: <body>`, `<value>: <body>`, or `else: <body>`; a body
    is a braced block or a single brace-less statement. Works as a **statement** and (on a
    declaration/assignment RHS) as a **value source** (`x :: match … { … }`), where a
    single-expression arm yields implicitly and a `{ … }` arm ends in `yield` (or exits on
    every path), reusing the value-if branch rule from 20.6
  - **tagged unions:** `.variant |cap|:` binds the payload (`cap := subject.variant`, the
    unchecked field reinterpret from 21.5); `.variant:` ignores it. Exhaustive over the
    union's *variants* (its fields), not the whole tag enum
  - **enums:** `.member:` arms, exhaustive over the enum's members; no capture
  - **scalars (int/float/bool/char):** arbitrary-expression patterns compared with `==`; an
    `else` is mandatory (the domain can't be enumerated)
  - exhaustiveness is a **compile error**: a non-exhaustive enum/union match with no `else`
    lists the missing variants, and a redundant `else` on an already-exhaustive match is
    rejected. Other diagnostics: unknown variant/member, duplicate arm, a non-`.variant`
    pattern for an enum/union subject, a capture on a non-union/`else` arm, an untagged-union
    subject, and arms after `else`
  - **the only new runtime capability is reading the discriminant** (the 21.5-deferred tag
    accessor): a new `hir.Union_Tag` expr + `ir.Union_Tag` op load the tag enum at the
    union's offset 0 (the union address *is* the tag address — no GEP). Everything else is a
    checker-only desugar to existing HIR (mirrors 18/19/20): the subject is spilled to one
    temp, the tag read once into another, and the arms become an `if tag == .a { … } else if
    … else { … }` chain (the last covered arm is promoted to the unconditional `else` so the
    chain stays exhaustive and `all_paths_return` flows through). Value-match reuses the
    value-if result-slot pattern (`new_value_slot`/`emit_value_branch`); captures reuse the
    unwrap-if capture scoping. No new HIR/IR statement kinds; lowering/codegen add only the
    one `Union_Tag` load
  - parser/AST add a `match` keyword and `Match`/`Match_Arm` statement kinds (arms stored in
    the `Match`'s `body`, pattern in `expr` with `INVALID` marking `else`, capture in
    `captures`); a `union(enum)`/`union(Enum)` match resolves variant tags via the same
    name→tag-enum-member lookup construction uses
  - deferred (`// ponytail:` follow-ups): `void`-payload variants (`pending void`) — `void`
    is not a runtime field type yet, so 21.5 can't declare them, though the no-capture arm
    form is already wired; and multi-pattern arms (`.a, .b:`) / range patterns

22.5. `void`-payloads and multi-pattern arms / range patterns in match statements (implemented; see below)
  - **void-payload variants**: a tagged union may declare a `void`-payload variant
    (`quit void`). As in Zig, a void field carries no runtime value — it is allowed only on a
    tagged union (the record-decl check skips the runtime-value requirement for it), contributes
    nothing to the layout (`size` 0 / `align` 1, so it is never the carrier), and is constructed
    with the **bare-key** literal `T{ variant }` (no `= value`). Construction stores only the tag;
    codegen skips the payload store. Matched with a plain no-capture arm (`.quit:`); a capture on a
    void variant, a `= value` on a void variant, a bare key on a non-void field, and a direct
    `x.quit` payload read are all diagnosed
  - **multi-pattern arms**: an arm may list several patterns (`.a, .b:` / `0, 1, 2:`); the AST
    `Match_Arm` now carries a `patterns` list and the checker ORs their dispatch conditions. A
    capturing multi-pattern arm over a tagged union is allowed when every listed variant has the
    same payload type (Zig parity — payloads share the carrier offset, so it is one read);
    mismatched payload types are a "capture group with incompatible types" error
  - **range patterns**: a scalar arm may be a range (`0..10:` / `0..=10:`), desugared to
    `key >= lo and key <(=) hi` (existing `.Ge`/`.Le`/`.Lt`/`.And` HIR); a range pattern on an
    enum/union subject is rejected
  - **pointer captures**: `|@cap|` binds a pointer into the subject's payload (mutate in place),
    reusing the for-loop `@`-capture and `pointer_capture` flag; mutability follows the subject. It
    requires an addressable subject — verified to need **no lowering/codegen change**: the subject
    is spilled as `&subject` and captures route through a `Deref` (`lower_location(Deref)` is the
    pointee address), so `Address(Field(Deref(ptr)))` aliases the original storage
  - the only new codegen is the void construction skip (one `llvm` site) plus a one-line `lower`
    guard so a void variant's absent payload operand is not lowered into a trapping recovery value;
    everything else is parser + checker desugar
  - deferred (`// ponytail:` follow-ups): contextual void construction (`e Event = .quit`) needs
    enum-literal→union coercion (milestone 23); Zig `inline .a, .b => |v|` per-tag comptime
    captures need monomorphization. Separately, a **call expression directly as a match subject**
    (`match get()`) is a pre-existing gap (assign to a variable first, as the spec examples do);
    the rvalue pointer-capture guard is defensive for when that lands

22.6. contextual void construction + call-as-match-subject (implemented; see below)
  - **contextual void construction**: a bare enum literal in a tagged-union context constructs a
    void-payload variant — `e Event = .quit` (and any expected-union position: `=`, return, call
    argument) coerces `.quit` to the union, equivalent to `Event{ quit }`. Build-pass only: the
    `.Enum_Literal` case, given a tagged-union `expected`, looks the variant up and emits the
    tag-only union `.Struct` HIR for a void variant. A payload variant via a bare `.variant`
    ("needs a payload") and an unknown variant are diagnosed; the payload-carrying contextual form
    `.variant{...}` stays deferred to milestone 23 (error channel)
  - **call expression directly as a match subject**: `match get() { … }` now specializes the call.
    Root cause was that the inference/spec-request walker `infer_statements` had no `.Match` case,
    so a match's subject and arm bodies were never visited and their calls never got a
    specialization (`find_spec` → "could not resolve specialization"). Added a `.Match` case that
    infers the subject and recurses into arm bodies (with the capture local typed from the variant
    payload, mirroring the `.For`/unwrap-`.If` handling). This also covers value-`match` and calls
    inside arm bodies. As a side effect the rvalue pointer-capture guard from 22.5 is now reachable
    (`match make_box() { .v |@p|: … }` correctly errors "requires an addressable subject")
  - checker-only; no parser/AST/HIR/IR/lowering/codegen change
  - deferred: payload-carrying contextual construction (`.variant{…}`) remains outside milestone
    23 v1. The separate `yield call()` inference gap is fixed in milestone 23.

23. sum-type composition, fallible channels, and yield inference (implemented; v1)
  - `.Yield` is now visited by specialization-demand inference, so `yield call()` inside value
    blocks, value-if/value-match branches, and match arms requests the needed call specialization
  - native unbacked enums and native `union(enum)` / `union(SomeEnum)` variants are registered in a
    program-global variant table keyed by `(name, payload-type)`. Runtime tags are fixed `u16`
    global ids; `0` is reserved for fallible success/no-error, and overflow is diagnosed
  - tagged-union construction, tag reads, matching, and LLVM layout use those global ids rather
    than per-type dense tags. Component-to-composite widening preserves the tag and copies only the
    active payload carrier bytes from the source carrier offset to the destination carrier offset
  - `A | B` composes native unbacked enums and native tagged unions: matching `(name, payload-type)`
    variants merge, same-name/different-payload conflicts are diagnosed, and backed enums, C enums,
    and untagged unions are rejected for v1
  - `T ! E` is represented as a real synthetic fallible channel type in `types.Store`. A fallible
    function returns success with code `0` or an error variant's global id; `return expr` dispatches
    based on whether `expr` coerces to the success type `T` or the error type `E`
  - `try expr` unwraps success and propagates the enclosing function's exact error channel;
    `expr catch fallback` unwraps success or evaluates the fallback value
  - runtime coverage lives in `examples/programs/errors`; compiler coverage includes global-id sum
    merge/conflict/widening/rejections and the `.Yield` inference regression
  - deferred follow-ups are split below; 23.5 keeps the next user-visible slice small

23.5. fallible ergonomics: catch blocks + composable try widening (implemented)
  - `expr catch |e| { ... }` binds `e : E` in the handler and uses the existing value-block
    `yield` rules to produce the fallback success value
  - `try` still requires the same success type `T`, but now propagates either the exact error
    channel or a sum-widenable `E1` into an enclosing `E1 | E2`
  - focused coverage lives in `examples/programs/errors` and the fallible ergonomics compiler test
  - leave ABI/layout/lint/design polish for later milestones

23.6. contextual payload construction + inline error types (implemented)
  - contextual payload `.variant{expr}` construction now works in tagged-union contexts
    (assignment, return, calls, and fallible error dispatch); bare `.variant` remains the
    spelling for void-payload variants
  - named fallible signatures can use inline unbacked enum and `union(enum)` error types
    after `!`

23.7. anonymous struct payloads and keyed payload sugar (implemented)
  - tagged-union variants can use anonymous `struct { ... }` payloads, scoped to variant payload
    declarations rather than general anonymous type syntax
  - contextual `.variant{field = value, ...}` constructs struct payloads by key, reusing ordinary
    struct-literal validation for unknown, duplicate, missing, and mismatched fields
  - structurally identical anonymous struct payloads share type identity, so sum composition merges
    matching variants and still rejects same-name variants with different payload shapes

24. bug fixes & interop/indexing oversights (implemented)
  - `return match ...` and `yield match ...` are accepted as direct value-control-flow
    operands, matching declaration/assignment value sources
  - index and slice-bound expressions are contextually coerced to `usize`; unsigned narrower
    integer indices work, while signed runtime indices diagnose instead of reaching LLVM
  - concrete native scalars coerce to same-family C scalar types at call, return, assignment,
    aggregate, and optional boundaries when the target C type can represent the source width
  - scalar keyword casts (`i32(x)`, `usize(x)`, `c_float(x)`, etc.) provide explicit numeric
    conversions for cases that should not be implicit
  - C scalar comparisons accept numeric literals by typing the literal from the concrete C
    operand, so aliases like `ZF` are no longer needed
  - array counts accept compile-time integer expressions such as `[CAP]T` and `[N + 1]T`;
    runtime variables remain rejected because arrays are fixed-size values
  - string literals in value-`match` / value-`if` peers resolve to a common zero-terminated
    byte slice when possible, and `[;0]u8` slices can decay to immutable `*c_char`/`?*c_char`
    parameters

25. dynamic heap allocation (implemented; v1)
  - `std/mem` exposes a plain-data `Allocator` contract with `?@mut anyopaque` context and `alloc`, `realloc`, and `free` `@func` pointers
  - `mem.c_allocator` is the libc-backed allocator; `mem.alloc(mem.c_allocator, size, alignment)` returns nullable mutable byte memory
  - `mem.free(mem.c_allocator, ptr, size, alignment)` frees with the same allocator; `malloc` handles default-aligned requests and `posix_memalign` handles larger power-of-two alignments
  - `mem.realloc` preserves alignment and the original allocation on failure; zero size frees, and over-aligned blocks use allocate/copy/free
  - typed allocation helpers, arenas/pools, build-mode heap policy, and escaping-allocation diagnostics remain deferred

26. import from project "root" (implemented)
  - imports beginning with `@` resolve from the project root
  - `brolang build [root]` uses `root` as the project root; without `root`, it searches cwd and parents for `build.bro`
  - direct `brolang <package-dir> -o <out>` defaults the project root to the package dir; `--root <dir>` overrides it

27. comptime integer value parameters (implemented; v1)
  - `$N` marks an integer comptime parameter in a normal `func` signature:
    `make_array func($N usize) [N]u8`
  - callers pass a compile-time integer expression; the value specializes the function
    and is omitted from the runtime ABI
  - inside the specialization, `N` is visible as an immutable compile-time integer in
    array counts, types, and body expressions
  - v1 intentionally supports integer values only; no comptime branch pruning or
    user-function execution

27.5 comptime type parameters (implemented; v1)
  - `$T type` marks an explicit comptime type parameter in a normal `func` signature:
    `max func($T type, a, b T) T`
  - callers pass the type explicitly as an ordinary comptime argument (`max(i32, a, b)`);
    the type argument specializes the function and is omitted from the runtime ABI
  - inside the specialization, `T` is visible in parameter, result, local, array, pointer,
    slice, and fallible type syntax
  - v1 intentionally keeps `type` contextual to comptime parameter declarations; no
    inferred type parameters, first-class type values, or comptime execution

27.6 comptime-evaluable constants/functions (implemented)
  - `$expr` forces comptime evaluation of an expression:
    `x :: $32`, `n :: $sum(1, 2)`, and `res :: ${ ... }`
  - constant contexts such as array counts and comptime value arguments implicitly
    require comptime evaluation; ordinary immutable bindings remain ordinary bindings
  - ordinary `func` calls are comptime-evaluable when reached from a comptime context;
    do not add a separate `$sum func(...)` declaration form
  - v1 evaluator supported integer literals/arithmetic, boolean conditions, immutable
    locals, `return`, `if`/`else`, comptime blocks, and direct calls to other evaluable
    brolang functions
  - broader typed execution is milestone 27.7

27.7 broader Zig-style comptime execution (implemented; v1)
  - `compiler/checker/comptime.odin` owns checker-local evaluator state, typed
    comptime values, execution, and HIR materialization; `checker.odin` keeps type
    checking, inference, specialization, and build orchestration
  - typed `$` values cover bools, integers, floats, strings, arrays, structs, tagged
    unions, enums, optionals, and fallibles
  - supports mutable comptime locals/assignment, `if`, `while`, `for`,
    `break`/`continue`, `defer`, `match`, value blocks/`yield`, direct calls to
    bodyful Brolang functions, and `try`/`catch`
  - successful `$` results materialize back into ordinary HIR expressions so lowering
    and LLVM stay unchanged
  - evaluation uses a fixed `100_000` step quota
  - immutable locals/globals with comptime-known initializers may feed comptime
    evaluation; runtime-dependent values remain invalid in comptime contexts
  - runtime-only behavior is rejected in comptime: external/bodyless `c_func`,
    writable globals, and materializing comptime storage pointers/slices as runtime memory
  - milestone 39 extends specialization keys from integer/type/string values to
    recursively stable values while keeping runtime ABI erasure unchanged

27.8 source-defined mutable runtime globals (implemented)
  - allow mutable global declarations in Brolang source for process-global runtime
    state, matching the writable-global support already needed for imported C globals
  - require source type syntax and an initializer; constraints (`int`/`float`/`range`)
    and inferred array counts may resolve through the existing inference fixpoint, but
    the final type must be concrete runtime storage
  - emit source-defined mutable globals as writable globals, not constants
  - allow assignment, address-taking, field/index mutation, and pointer passing under
    the same mutability rules as other writable locations
  - keep mutable globals invalid in comptime evaluation; `$global_var` and writes from
    comptime execution must remain runtime-dependent errors
  - define initialization order and cycle behavior by reusing the existing global
    initializer dependency/cycle system where possible
  - reject user-visible name shadowing across imports, named types, globals, functions,
    params, locals, comptime params, captures, and labels; `_` remains reusable

27.9 comptime storage and function values (implemented; practical v1)
  - comptime locals, params, and immutable globals can own evaluator storage cells
    addressable through places instead of compiler-owned memory
  - comptime supports address/deref, mutable pointer and slice mutation, field/index
    places, slicing, `.len`, `.ptr`, pointer captures, and pointer-param aliasing
  - comptime storage pointers/slices cannot materialize as runtime memory; escaped
    dead storage is rejected
  - bare concrete non-comptime function names are comptime-only declaration identities;
    native function pointer types use `@func(...) R` and fallible `@func(...) R ! E`
  - bare identities implicitly materialize compatible runtime pointers, never the reverse;
    aggregates containing bare identities remain comptime-only
  - comptime-known native/bodyful `c_func` values can be called; bodyless/imported
    callbacks remain runtime-only
  - native function pointers are non-variadic v1; C variadic function pointers stay
    under `*c_func(...) R`

28. brolang build system (implemented; v0 shipped)
  - `brolang build [root]` reads a declarative `config` constant from
    `root/build.bro` (importing `@std/build`'s `BuildConfig`) and compiles the
    program package it names. The config is read from the checked HIR — build.bro
    is never lowered — so it is literal-only.
  - `brolang new <project>` and `brolang init` create the default project layout
    with local `std`, `ffi`, `vendor`, `source`, and `build.bro`
  - enabled `&<array literal>` (Zig's `&.{...}`): the literal is promoted to an
    anonymous global whose address decays to a slice, so list fields like
    `libraries = &["raylib"]` work; empty lists are `&[]`
  - deferred: build graph / steps / caching, multiple artifacts, and computed paths
    (needs string building); milestone 44 later removed explicit `&[]` build-config fields

29. fix bugs (implemented)
  - bare `return` is the empty return for void functions; `yield` always requires a
    same-line, non-void value because `break` handles valueless scope exits
  - catch value blocks may end by returning from the function instead of yielding when
    every path exits
  - implicit-conversion diagnostics render source-level composite and named types instead
    of internal `<type N>` ids
  - aliases resolve transparently in value contexts, including composed enum/union sums
  - final open-constant defaults feed one last inference fixpoint before stale
    specializations are pruned

30. Zig-style type factories and basic `std/arraylist` (implemented; v1)
  - comptime-only functions may return `type`; anonymous `struct { ... }` expressions and
    factory calls such as `ArrayList(i32)` resolve to cached nominal concrete types
  - factory parameters use the existing explicit `$T type` / integer comptime parameters;
    normal comptime control flow and helper factory calls are supported
  - type-factory calls work in signatures, nested types, struct literals, and type builtins;
    runtime materialization and recursive specializations are diagnosed
  - `std/mem` adds typed `empty` and failure-preserving `realloc`, including overflow,
    zero-count, zero-sized-type, and alignment handling
  - `std/arraylist.ArrayList(T)` exposes `items`, `capacity`, and `allocator`, with fallible
    reserve/append, roughly 1.5x growth from 8, clear-without-free, and reusable deinit
  - deferred: recursive factories, reflection, type-producing
    unions/enums, pop/insert/remove/shrink/clone container operations

31. generic container ergonomics (spike completed; no language change)
  - type factories already provide the important half of generic structs: `ArrayList(T)` is a
    cached, concrete nominal type whose layout contains `T`; no type information is carried at
    runtime
  - the verbosity comes from free functions repeating explicit comptime type arguments
    (`deinit(i32, &values)`, `append(i32, &values, value)`), not from a missing generic data model
  - do not add functions inside structs, implicit `Self`, associated lookup, or per-value runtime
    type metadata for this; those features would add a second namespace/member model without
    improving layout or specialization
  - the smallest fitting feature is call-local inference of omitted comptime type arguments:
    ```
    values ArrayList(i32) = arraylist.init(mem.c_allocator)
    defer arraylist.deinit(&values)
    try arraylist.append(&values, 42)
    ```
    `T` comes from the expected result for `init` and from the concrete receiver argument for the
    other calls
  - keep functions package-scoped and keep the explicit form valid; this preserves simple name
    resolution and gives ambiguous calls an escape hatch

31.5. inferred comptime parameters (implemented)
  - comptime parameters may appear anywhere and are erased while preserving runtime parameter order
  - a native call may omit `$T type`, integer, or immutable byte-string comptime arguments when every
    value is uniquely recoverable from runtime argument types and/or the immediate expected result
  - inference structurally matches direct type parameters, pointers/slices/arrays/optionals/
    fallibles/functions, direct array counts, and canonical generated type-factory provenance;
    forwarding/non-invertible factories keep the explicit spelling
  - concrete evidence is exact; contextual numeric constants are weak evidence and are rebuilt with
    the resolved parameter type before ordinary coercion
  - `_` explicitly leaves one comptime argument to inference; exactly one complete argument mapping
    must succeed, with missing and ambiguous mappings diagnosed
  - the existing specialization/HIR/LLVM ABI is unchanged; `std/mem` and `std/arraylist` now use the
    inferred form where their arguments or result provide enough information

32. explicit division family (implemented)
  - `/` and `/=` are float-only; every integer use is rejected with guidance toward explicit
    division, including literals, comptime execution, array counts, and compound assignment
  - direct bang calls use `divtrunc!`, `divfloor!`, `divexact!`, `divceil!`, `rem!`, and `mod!`;
    bare and qualified names remain ordinary functions
  - the builtins accept compatible concrete integer or float scalars, reuse existing literal and
    widening rules, and return the common operand type (integral-valued floats for quotients)
  - all builtins diagnose zero denominators at comptime and trap at runtime; quotient operations
    also trap on signed `minval!(T) / -1`, while `rem!` and `mod!` return zero for that pair
  - `divexact!` checks the reconstructed dividend in the operand type; `rem!` pairs with truncation
    and follows the numerator sign, while `mod!` pairs with floor and follows the denominator sign
  - HIR/IR use compact semantic enum tags; integer floor, ceil, and exact lowering reconstructs the
    remainder from one quotient so each produces only one hardware-division candidate
  - float lowering uses the typed LLVM trunc/floor/ceil intrinsics, `frem`, and ordered equality;
    ordinary float `/` remains the unchecked IEEE infinity/NaN escape hatch
  - migrated `std/mem`, `std/arraylist`, and the compound-assignment example to `divtrunc!`

33. explicit I/O provider (implemented)
  - `main` may take one canonical `@std/process Init`; parameterless entry points remain valid
  - the compiler supplies a `hide system` macOS provider and constructs `Init` in the external C wrapper
  - readers and writers bind provider context, a generalized handle, and a direct callback
  - standard-stream helpers and existing-file operations use the injected provider; files retain it
  - `read` and `write` validate provider counts; `write_all` handles partial writes and no progress
  - the system provider uses unbuffered POSIX file descriptors, retries interrupted open/read/write,
    and allocates nothing

34. package declaration aliases and root `std.ArrayList` (implemented)
  - bare qualified aliases use `Name :: alias package.Member` without adding a keyword
  - functions/type factories, named types, and globals transparently retain the target identity;
    mutable global aliases therefore share the original storage
  - aliases resolve transitively at load time, consume their file-local import, preserve explicit
    `hide` visibility, and diagnose missing, hidden, unavailable, ambiguous, cyclic, or
    conflicting targets
  - root `std` re-exports only `ArrayList(T)` for now; operations remain under `std/arraylist`

35. syntax highlighting (tree-sitter) updates (implemented)
  - pointer sigils are highlighted as operators
  - type-factory calls in type positions and struct literals are highlighted as functions

36. transitive package namespaces (spike completed; no language change)
  - imports remain file-local implementation details, including explicitly named imports such as
    `rl :: import "@vendor/raylib"`; naming an import only chooses its local qualifier
  - packages expose declarations, not their imports, so imported namespaces never become public or
    transitively reachable package members
  - callers import each package they use directly; subdirectory layout does not create namespaces
  - this keeps package lookup shallow and deterministic and avoids overloading import aliases with
    declaration visibility

36.5. explicit `hide` file-local declarations (implemented)
  - `hide name ...` gives any named top-level function, global, native/C record, union, enum,
    opaque/distinct type, or declaration/type alias the existing file-local semantics
  - declarations remain public by default; a leading underscore is an ordinary identifier and `_`
    remains the write-only sink
  - hidden native and C declarations with the same name may coexist in separate files, while public
    collisions are still diagnosed
  - `hide` is reserved for named top-level declarations and is rejected on imports, locals,
    parameters, fields, and anonymous declarations

37. tuples, reflection, process init, and printing (implemented)
  - tuples are unnamed-field structs with structural anonymous values, nominal named declarations,
    brace literals, numeric fields, and no runtime metadata
  - `@std/meta`, `typeinfo!`, `field!`, `compile_error!`, specialization-time branches, and semantic
    `expand for` use checker-owned persistent compile-time values for aggregate-first reflection and
    heterogeneous static expansion; expand-loop control is recursively resolved at comptime
  - interleaved comptime parameters use semantic candidate resolution, immutable byte values specialize
    by contents, and all comptime parameters remain erased from the runtime ABI
  - `io.print(writer, format, args)` validates and expands `{s}` / `{d}` formatting at comptime,
    is implemented in ordinary `@std/io` code, performs no allocation or runtime parsing, and
    propagates the first write error
  - `debug.print` reuses formatting through an independent stderr backend, ignores failures, and adds
    no newline
  - entrypoint validation accepts only parameterless `main` or canonical `main(process.Init)`, validates
    the hidden `@std/io.system` provider, and injects the provider through the generated C entrypoint;
    native variadics and additional startup data remain deferred

38. place every intrinsic behind direct unqualified `name!(...)` syntax, freeing the bare names for
    user functions (implemented)

39. stable comptime values and richer formatting (implemented)
  - comptime parameters accept booleans, integers, floats, types, immutable bytes,
    enums, fixed arrays, records/tuples, optionals, and tagged unions recursively
  - canonical specialization keys include deterministic type identity, exact float bits,
    byte contents, ordered aggregate children, optional state, and active union variants;
    FNV-1a fingerprints accelerate lookup while exact key comparison handles collisions
  - equal structural values reuse specializations and stable emitted names, distinct values
    specialize separately, aggregate inference uses exact type-factory provenance, and all
    comptime parameters remain erased from the runtime ABI
  - undefined values, pointers, general slices, fallibles, ranges, and untagged
    unions diagnose that they have no stable comptime identity
  - `@std/meta.TypeInfo.enum` carries declaration-ordered `EnumInfo.fields`, enabling enum
    formatting through `field!` without runtime reflection metadata
  - `io.print` and `debug.print` retain their APIs and expand `{}`, `{s}`, `{d}`, `{b}`,
    `{o}`, `{x}`, `{X}`, `{c}`, and `{e}` at comptime; `{{` and `}}` remain escapes
  - integer output uses one base-aware 65-byte stack buffer; float output uses fixed-buffer
    libc `snprintf` with 32-bit and 64-bit general/scientific precision and propagates failure

40. compile-time `expand` (implemented)
  - expansion-oriented `inline for` is strictly renamed to `expand for`; `inline` remains available
    for future function-inlining syntax
  - final expanded enum and tagged-union match arms generate checker-local specialized arms only
    for variants not covered by preceding explicit arms
  - generated enum values and union tags are static bindings, heterogeneous payloads retain their
    concrete types, and `void` payloads support value and pointer captures without runtime storage
  - `tag!` reads or folds a tagged union discriminant, while `tagname!` turns a comptime-known enum
    value into its immutable field-name string

41. native test framework (implemented; v1)
  - `name test { ... }` declares an implicit `void ! testing.Error` test omitted from executable builds
  - anonymous `test import` edges discover dependency tests transitively; ordinary imports remain
    separate namespaces and never discover tests
  - direct `@std/testing` `expect` and expected-first `expect_equal` calls inject their source locations
  - `brolang test` reuses `build.bro`, runs tests sequentially, continues after assertion failures,
    prints a summary, and returns failure when a test fails or traps
  - filtering, skipping, fixtures, snapshots, parallelism, isolation, allocators, and more assertion
    families remain deferred

42. unsigned integer constraint (implemented)
  - `uint` is the unsigned subset constraint while `int` remains the whole integer family
  - literal-only values choose the smallest fitting `u8`, `u16`, `u32`, or `u64`
  - native unsigned scalars and target-classified unsigned C scalars satisfy the constraint
  - `isize` and `usize` remain concrete pointer-sized types

43. comptime function parameters (implemented)
  - bare native and C function identities, function literals, and comptime-only aggregates
    containing them specialize by declaration identity rather than runtime address
  - repeated declarations reuse specializations, distinct declarations specialize separately, and
    function-valued parameters remain erased from the runtime ABI
  - statically known callback invocations lower to direct calls; runtime-selected function pointers
    remain indirect, and bodyless C declarations remain runtime-only during comptime execution
  - runtime pointer contexts implicitly materialize a bare identity; pointer-to-identity conversion is
    rejected, and bare-containing aggregates cannot enter runtime storage or ABI/C layouts

44. struct field defaults on declaration (implemented)
  - named native structs accept `field T = expression`; keyed construction evaluates defaults
    for omitted fields while explicit initializers override them
  - defaults resolve names in the declaration file, participate in record constraint inference,
    and work during runtime and comptime construction
  - `@std/build.BuildConfig` uses defaults for optional list fields
  - fields without defaults remain required; C-layout records, unions, tuples, and anonymous
    generated structs do not accept defaults

45. typed memory intrinsics (implemented)
  - `memcopy!` copies equal-length, non-overlapping slices or pointers-to-arrays with identical
    element types; comptime diagnoses invalid regions and runtime guards length, size, and overlap
  - `memset!` fills a mutable region with a value coerced to its element type; bytes lower to LLVM
    memset and wider values use typed stores
  - both operations evaluate operands once, preserve undefined state without observing it, and work
    identically during comptime evaluation; zeroing is `memset!(destination, 0)`
  - `std/mem` aligned reallocation uses `memcopy!`

46. fix `EnumFieldStruct` in `std/meta` (implemented)
  - comptime local declarations resolve type syntax in the active interpreter state, preserving match captures
  - `EnumFieldStruct` sizes its working arrays directly from the comptime `.enum |info|` capture

47. rename optional `none` to `null` (implemented)

48. add `noreturn` and `unreachable` (implemented)
  - `noreturn` is the native bottom type, permitted as a function result but rejected for storage,
    parameters, record fields, and the C ABI
  - `noreturn` expressions coerce to any expected value type and terminate path analysis
  - `unreachable` always traps at runtime and reports an error during comptime evaluation

49. unify catch fallback value sources (implemented)
  - `expr catch [|error|] value_source` uses the same ordinary expression, value block, and
    value-producing control-flow forms with or without an error capture
  - void fallthrough and diverging `noreturn` fallbacks remain valid

50. add `constcast!` and immutable deallocation (implemented)
  - `constcast!` restores mutability only for pointers, optional pointers, and slices while
    preserving child type, pointer kind, optionality, length, and sentinel shape
  - `std/mem.free` accepts immutable slices and restores mutability only at the allocator boundary

## A word on unchecked casts

For casts that bypass safety checks, Honey provides builtin functions:

| Builtin | Purpose | Traps when... |
| -- | -- | -- |
| `truncate(x, T)` | Keep low bits, discard rest | Never |
| `bitcast(x, T)` | Reinterpret bits, no cast | Sizes don't match (compile error) |
| `ptrcast!(T, p)` | Change a pointer's child type while preserving its shape | Invalid child or non-pointer operand (compile error) |
| `constcast!(p)` | Restore pointer or slice mutability | Non-pointer/slice operand (compile error) |

```honey
# truncation
a: u32 = 0xDEADBEEF
b := truncate(a, u8)  # b == 0xEF (low byte)

# bit reinterpretation
n: i32 = -1
m := bitcast(n, u32)  # m == 0xFFFFFFFF (same bits)
f: f32 = 3.14
bits := bitcast(f, u32)   # IEEE 754 representation

# pointer casts
buf *u8 = get_buffer()
ints *u32 = ptrcast!(u32, buf)       # element type change, same pointer shape
writable *mut u8 = constcast!(buf)   # explicit unsafe mutability restoration
```

## A word on multi-unwrap

Unwrap multiple optionals with `and`. This **short-circuits**: if the first optional is null, subsequent expressions are not evaluated.

```
name: ?[]u8 = get_name()
age: ?u8 = get_age()
if name and age |n, a| {
    # both n and a are guaranteed non-null here
    print("{s} is {d} years old", n, a)
}
```

**With guard clause on multiple values:**

```
if name and hat |n, h : n == "Huginn" and h.brand == .gucci| {
    print("{s}'s got that drip\n", n)
}
```

Parentheses around the expression are optional, but can aid readability when combined with guards:

```
# without parentheses
if name and hat |n, h : guard| { ... }

# with parentheses for clarity
if (name and hat) |n, h : guard| { ... }
```

## A word on lazy / short-circuit evaluation

The `and` in multi-unwrap short-circuits left-to-right:

```
if get_name() and get_hat() |n, h| {
    # get_hat() is only called if get_name() returned non-null
}
```

This is important for avoiding unnecessary computation or side effects.

## A word on ranges

Ranges represent a sequence of values, commonly used in for loops, and is itself a value type:

```
0..10      # exclusive: 0, 1, 2, ..., 9
0..=10     # inclusive: 0, 1, 2, ..., 10
```

**Parenthesization rule:** Each side of `..` must be either a simple term (literal or identifier) or a parenthesized expression. This eliminates precedence ambiguity:

```
0..10           # OK: both sides are literals
0..n            # OK: both sides are simple
0..(n + 1)      # OK: complex expression is parenthesized
(a + 1)..(b - 1)  # OK: both sides parenthesized
# 0..n + 1      # ERROR: must parenthesize complex expressions
```

This rule keeps the grammar simple and forces clarity at the call site — no precedence rules to remember. Also, being a value type, ranges can be assigned to variables and passed around like any other value. Range bounds are evaluated once, must have compatible concrete integer types, and descending ranges are empty.

For-loop captures are immutable and scoped to the loop body. Sequence index captures are `usize`. Pointer capture uses `|@item|`; arrays must be passed by pointer (for example `&items`), while slices can be used directly. Sentinel elements are not included in iteration.

## A word on distinct types

Distinct types are considered distinct from their backing type. They do not implicitly coerce to their backing type.

```
# distinct type
UserID :: distinct u32

# instantiate distinct type
my_id UserID :: UserID(42) # value must have the exact backing type
```

## A word on enums

```
# standard enums
Animal :: enum {
    dog
    cat
    bird
    lizard
}

# enums with backing type
Nat :: enum(u8) { # in this case, a maximum of 256 values are possible
    one # default: implicitly starts from value 0
    two
    three
    four
    five
}

# enums with backing type with explicit associated values
# note: must not be jumbled (i.e. `first_val = 1` must come before `other_val = 2`), but is allowed to be discontiguous (i.e. `one = 1` can be followed by `three = 3` without `two = 2` in between)
Nat :: enum(u8) {
    one = 1
    two = 2
    three = 3
    # no four
    five = 5
}

# enums with backing type with semi-implicit associated values
Nat :: enum(u8) {
    one = 1 # starts from value 1
    two     # implicitly gets value 2
    three   # etc...
    four
    five
}

# using enums
dog_tag1 Animal :: Animal.dog
dog_tag2 Animal :: .dog # type inferred
```

Unbacked enums cannot assign explicit values. Backed enum values must be decimal integer
literals, fit the backing type, and increase strictly; gaps are allowed.

Native enum types remain distinct from integers and from other enum types. They support
`==` and `!=`, but not arithmetic, ordering, casts, or backing-value extraction.

C enums follow C/Zig import semantics rather than native enum semantics:

```
native :: import "native.h"
value native.Imported_Enum :: native.IMPORTED_ENUM_VALUE
```

The imported enum type is an alias of its target-selected C integer backing, and imported
enumerators are package-level constants.

## A word on multi-line strings

Multi-line strings use the `` ` `` character to mark each line. Content starts immediately after the backtick. Newlines between lines are implicit.

```
config =
    `# Database configuration
    `host = localhost
    `port = 5432
    `
    `[server]
    `address = 0.0.0.0
```

Key properties:

* Content begins immediately after `` ` `` 
* Newlines are automatically inserted between lines
* Empty `` ` `` produces a blank line
* No escape sequence processing (raw content)
* No trailing newline after the last line

Only the leading `` ` `` is special; the rest is treated as raw content.

If you need a trailing newline, add an empty line at the end:

```
# No trailing newline
msg =
    `hello
    `world

# With trailing newline
msg =
    `hello
    `world
    `
```

Mixing multi-line strings with inline strings (using concatenation):

```
message = 
    "Header:\t" ++
    `more content here
    `even more content
    `
    ++ "Footer"
```

Formatting alternative (purely aesthetics/preference, no effect on program):

```
message =  "Header:\t" ++
           `more content here
           `even more content
           `
           ++ "Footer"
```

## A word on `yield`

The `yield` keyword provides a value from a block to its enclosing expression and **exits the block immediately** — just as `return` exits a function, `yield` exits the enclosing scope. Code after a `yield` is unreachable, and the compiler flags it. This makes `yield` part of a consistent set of scope-exiting control flow: `return` exits a function, `yield` exits a block, `break` exits a loop, and `continue` skips to the next iteration.

It is used in scoped blocks and match arms today; catch-handler blocks are planned.

**General rule:** When a block needs to produce a value, single expressions yield implicitly while multi-statement blocks require explicit `yield`. This rule applies uniformly across the language:

```
# scoped block
data :: {
    result := compute()
    yield result
}

# match arms
label []u8 = match p {
    .high: "HIGH",          # single expression: implicit yield
    .low: {
        log("low priority")
        yield "LOW"         # block: explicit yield
    },
}

# catch handlers (planned; block form deferred in milestone 23 v1)
data []u8 = read(path) catch |e| {
    log(e)
    yield fallback_data                       # block: explicit yield
}
```

### Yielding from if-statements and loops

Yielding from if-statements is possible with the constraint that all branches must resolve to the same yield type.

```
# yielding to a constant
result :: if a {
    yield 1
} else if b {
    yield 2
} else {
    yield 3
}

# yielding to a variable
result int = if a {
    yield 1
} else if b {
    yield 2
} else {
    yield 3
}

# ILLEGAL: branches with different yield types
result :: if a {
    yield 1
} else {
    yield Color{ r = 255, g = 0, b = 0 }
}
```

Yielding is also possible from loops with the same constraint.

```
# get active entity
active_ent_idx :: for 0..10 |i| blk: {
    if is_active(some_entity, i) yield :blk i
    yield null # fall-through: no active ent was found (this should imply a return type matching both the index value and `null`, meaning it should resolve to an optional in this case)

    # note that in this case, we have to use the `blk` label to yield from the correct scope.
    # otherwise, the yield should return directly from the if-statement's scope (which would be incorrect in this case).
}

# BAD: yield returned from if-statement, but no name binds it: should miscompile similar to unused return values from functions.
active_ent_idx :: for 0..10 |i| {
    if is_active(some_entity, i) yield i # bad
    yield null
}

# BAD: likewise for loops
for 0..10 |i| blk: { # bad, no name binds returned value
    if is_active(some_entity, i) yield :blk i
    yield null
}
```

## A word on match statements

```
# matching on enums
match status {
    .ok: print("success")
    .error: print("failure")
    .pending: {
        log("still waiting")
        retry()
    }
}

# matching on integers and other values
match code {
    0: print("zero")
    1: print("one")
    2: print("two")
    else: print("other")  # needed - missing variants
}

Status :: enum { ok, error, pending }

status :: get_status() # returns a `Status`
match status {
    .ok: handle_ok()
    .error: handle_error()
    .pending: handle_pending()
    # no else needed - all variants covered
}

# matching on tagged unions
Result :: union(enum) {
    success Data
    failure struct {
        msg []u8
        code i32
    }
    pending void
}

match result {
    .success |data|: {  # use `|name|` to capture the variant's payload
        process(data)
    }
    .failure |info|: {
        print("error {d}: {s}", info.code, info.msg)
    }
    .pending: {
        # void payload - no capture needed
        wait()
    }
}

# when a union variant has a `void` payload, omit the capture
Event :: union(enum) {
    click struct { x i32, y i32 }
    keypress KeyCode
    quit void
}

event :: get_event() # returns an `Event`
match event {
    .click |pos|: handle_click(pos.x, pos.y)
    .keypress |key|: handle_key(key)
    .quit: should_exit = true
}

# single-expression arms yield value implicitly
label :: match priority {
    .critical: "CRIT"
    .high:     "HIGH"
    .normal:   "NORM"
    .low:      " LOW"
}

# multi-statement arms use `yield`
message :: match code {
    0: "success"
    1: {
        log("warning encountered")
        yield "warning"
    }
    else: "unknown"
}
```

## A word on error handling

Brolang handles errors as values. There is no hidden control flow — a function that can fail declares this in its signature, and the caller must explicitly handle the possibility of failure.

Milestone 23 v1 implements named error channels, native sum composition, `return`-based error dispatch, exact-channel `try`, and fallback `catch`. Milestone 23.5 adds `catch |e|` blocks and `try` widening across composable error channels. Milestone 23.6 adds contextual payload construction and inline error types. Milestone 23.7 adds anonymous struct payloads and keyed payload sugar. It still defers the `error` keyword shorthand and match-on-error shorthand.

### Fallible Functions

Functions that can fail declare their error type after `!`:

```
read_file func(path []u8) []u8 ! IoError { ... }
```

This reads as: "returns `[]u8` or fails with `IoError`." The space around `!` is idiomatic but not required.

### Error Types

Errors can be enums (when you only need to identify what went wrong) or tagged unions (when errors need to carry additional context).

**Simple errors (enum):**

```
SimpleError :: enum {
    OutOfMemory,
    InvalidSize,
    Timeout,
}
```

**Rich errors (tagged union):**

```
IoError :: union(enum) {
    NotFound struct { path: []u8 },
    PermissionDenied struct { path []u8, operation []u8 },
    Timeout struct { after_ms u64 },
    ConnectionReset void,  # no additional data needed
}
```

**Constrained tagged union:**

When you have a predefined set of error kinds, you can constrain the union:

```
IoErrorKind :: enum {
    not_found,
    permission_denied,
    timeout,
}

IoError :: union(IoErrorKind) {
    not_found struct { path []u8 },
    permission_denied struct { path []u8 },
    timeout struct { after_ms u64 },
}
```

### Error Composition

Functions that can fail with multiple error types use `|` to compose a named error type:

```
ProcessError :: alias IoError | ParseError
process func(path []u8) Ast ! ProcessError { ... }
```

Parentheses are optional in the composed type and can aid readability:

```
ProcessError :: alias (IoError | ParseError)
```

### Inline Error Types

Fallible signatures can define small private error channels inline:

```
read_count func(path []u8) i32 ! union(enum) {
    not_found PathErrorInfo
    timeout_ms u64
} { ... }
```

Inline enums are also supported:

```
parse_flag func(text []u8) bool ! enum {
    empty
    invalid
} { ... }
```

Use a named error type when the channel is shared or needs stable public identity.

### Returning Errors

Fallible functions use ordinary `return` for both channels. If the returned expression coerces to the success type `T`, the function returns success with channel code `0`. If it coerces to the error type `E`, the function returns the error with that variant's global tag id:

```
parse_section func(p: @mut Parser) void ! ParseError {
    start_line Line = p.line
    p.advance()
    
    # ... parsing logic ...
    
    if p.pos >= p.input.len or p.input[p.pos] != ']' return .unclosed_section{line = start_line}
    
    # ... continue on success ...
}
```

Since errors are just union values, you can also construct them separately:

```
# Construct error value (it's just a union)
e ParseError = .timeout{500}

# Return it via error channel later
return e
```

The symmetry:

* `return x` — exits with success when `x` is the success type
* `return e` — exits with error when `e` is the error type

### Propagation with `try`

The `try` keyword unwraps a successful result or returns early with the error:

```
ProcessError :: alias IoError | ParseError
process func(path []u8) Ast ! ProcessError {
    data :: try read_file(path)   # read_file also returns []u8 ! ProcessError in v1
    ast :: try parse(data)        # parse also returns Ast ! ProcessError in v1
    return ast
}
```

`try` propagates when the success type matches the enclosing fallible function and the callee's error channel either exactly matches or can widen into the enclosing composed error channel.

### Handling with `catch`

The `catch` keyword handles errors and provides a value to continue with. It supports both fallback values and block handlers.

**Provide a fallback value:**

```
data :: read_file(path) catch default_data
```

**Block form using** `yield`:

```
data :: read_file(path) catch |e| {
    log("read failed: {}", e)
    yield empty_data
}
use(data)  # continues with data = empty_data
```

The `yield` keyword provides a value from a block to the enclosing expression. Execution continues after the statement. For single expressions, yield is implicit (e.g., `catch default_data`). For blocks, explicit `yield` is required. See the Yield section under Control Flow for the full rule.

**Planned return-from-handler form** (deferred in v1):

```
data :: read_file(path) catch |e| {
    log("read failed: {}", e)
    return  # exits the enclosing function
}
use(data)  # never reached if error occurred
```

Use `return` when the error is unrecoverable at this level.

**Planned match-on-error form** (deferred in v1):

```
data :: read_file(path) catch |e| match e {
    .NotFound |info|: {
        print("file not found: {s}", info.path)
        yield create_default(info.path)
    },
    .Timeout |info|: {
        print("timed out after {d}ms", info.after_ms)
        yield retry(path)
    },
    .PermissionDenied: panic("cannot recover from permission error"),
    .ConnectionReset: retry(path),
}
```

### Summary

| Syntax | Meaning |
| -- | -- |
| `T ! E` | Function returns `T` or fails with `E` |
| `T ! E1 | E2` | Planned direct spelling; use a named alias in v1 |
| `T ! (E1 | E2)` | Planned direct spelling with parentheses; use a named alias in v1 |
| `return e` | Exit function via error channel when `e : E` |
| `error e` | Planned shorthand, not v1 |
| `.variant{payload}` | Construct a payload-carrying tagged-union variant from one payload expression |
| `.variant{field = value, ...}` | Construct a struct-payload tagged-union variant from context |
| `error .variant{...}` | Planned shorthand, not v1 |
| `try expr` | Unwrap success or propagate an exact/sum-widenable error channel |
| `expr catch fallback` | Provide fallback value on error |
| `expr catch |e| { ... }` | Bind `e : E` and yield a fallback value from the handler |
| `yield value` | Provide value from innermost block |
| `yield :label value` | Provide value from labeled block |
| `return` / `return value` | Exit the current function; in fallible functions, return value dispatches by type |

## A word on comptime

```
make_array func($N usize) [N]u8 { ... }  # implemented: integer comptime value params
max func($T type, a, b T) T { ... }      # implemented: comptime type params
x :: $32                                 # implemented: force comptime expression evaluation
n :: $sum(1, 2)                          # implemented: ordinary functions can run at comptime
res :: ${ yield 4 }                      # implemented: comptime value block
p :: $Point { x = 1, y = 2 }             # implemented: typed aggregate comptime values
total :: $sum_loop(4)                    # implemented: mutable locals/loops/defer/match/try/catch
```

## A word on memory allocation

Current v1 is intentionally byte-oriented and plain data:

```
mem :: import "@std/mem"

bytes := mem.alloc(mem.c_allocator, 128, 1)
defer mem.free(mem.c_allocator, bytes, 128, 1)
```

Typed allocation helpers, arenas, pools, build-mode heap policy, and escaping-allocation diagnostics are future work. Older examples below are design sketches where noted, not committed syntax.

Memory allocation in Brolang is designed to be **explicit but not verbose**. We reject the dogma that global state is inherently evil — allocators are a cross-cutting concern that nearly every function needs, making them a perfect candidate for sensible defaults.

### Philosophy

```
┌─────────────────────────────────────────────────────────────┐
│  DESIGN PRINCIPLES                                          │
│                                                             │
│  1. No hidden magic: allocation calls are visible           │
│  2. Sensible defaults: thread-local heap for common cases   │
│  3. Explicit override: custom allocators when needed        │
│  4. Build-mode aware: different behavior for debug/release  │
│  5. Immutable defaults: no "action at a distance" bugs      │
│  6. Escaping allocations: caller provides allocator         │
└─────────────────────────────────────────────────────────────┘
```

### The Default C Allocator

Brolang provides a libc-backed allocator value:

* Available as `mem.c_allocator`
* Passed explicitly to `mem.alloc`, `mem.realloc`, and `mem.free`
* **Immutable at runtime** — cannot be reconfigured

```
mem :: import "@std/mem"

process func(input []u8) u64 {
    temp ?*mut u8 = mem.alloc(mem.c_allocator, input.len * 2, 1)
    defer mem.free(mem.c_allocator, temp, input.len * 2, 1)

    # ... work with temp ...

    return compute_hash(temp)
}
```

Future build modes can add other allocator values without changing the allocator contract:

| Build Mode | Allocator Behavior |
| -- | -- |
| Debug | Tracking allocator with leak detection |
| Release | Fast allocator, zero overhead |
| ReleaseSafe | Bounds-checking allocator |

Allocator policy is configured at compile time. You cannot change which allocator value an allocation used after the fact. This is intentional — it prevents bugs where memory allocated with one allocator is freed with another.

### The Escaping Allocation Rule

**If a function heap-allocates memory that escapes its scope — whether via the return value or via writes through mutable parameters — the function must accept an allocator parameter.** The presence of an allocator parameter is the contract that says "heap memory escapes here, and you're responsible for it."

This rule makes ownership transfer visible at the function signature level. The caller never needs to read the function's implementation to know whether heap cleanup is involved:

```
mem :: import "@std/mem"

# Allocation escapes via return value — requires allocator
duplicate func(input []u8, allocator mem.Allocator) ?*mut u8 {
    result ?*mut u8 = mem.alloc(allocator, input.len, 1)
    mem.copy(result, input)
    return result  # caller manages this memory
}

# Allocation escapes via mutable parameter — requires allocator
init func(obj @mut MyStruct, allocator mem.Allocator) void {
    obj.buffer = mem.alloc(allocator, 100, 1)
    # caller now knows heap memory was written into obj
}

# No allocation escapes — no allocator needed
process func(input []u8) u64 {
    temp ?*mut u8 = mem.alloc(mem.c_allocator, input.len, 1)
    defer mem.free(mem.c_allocator, temp, input.len, 1)
    # ... work with temp ...
    return compute_hash(temp)
}

# No heap allocation at all — no allocator needed
reset func(obj @mut MyStruct) void {
    obj.count = 0
}

main func() void {
    data := duplicate("hello", mem.c_allocator)
    defer mem.free(mem.c_allocator, data, 5, 1)

    mut obj := MyStruct{ ... }
    init(&obj, mem.c_allocator)
    defer mem.free(mem.c_allocator, obj.buffer, 100, 1)
}
```

**Why this matters:**

* Without the rule, a function like `init(obj: @mut MyStruct) void` is ambiguous — did it heap-allocate into `obj`, or just set some fields to stack/static data? The caller has no way to know without reading the implementation.
* With the rule, the allocator parameter is a clear signal: "this function produces heap memory that outlives its scope, and you are responsible for cleaning it up."
* Internal allocations (temporary buffers, scratch space) use an explicit allocator value directly and are freed before the function returns. No allocator parameter needed, no burden on the caller.

The compiler should eventually enforce this rule. If a function heap-allocates memory that escapes without accepting an allocator parameter, the compiler should emit an error.

### Why Immutable Defaults?

Consider what would happen if you could reconfigure the default allocator:

```
# This is not allowed and does not exist in Brolang.
mem.default_allocator_set(my_custom_allocator)

# Somewhere else in the codebase...
data := mem.alloc(mem.default_allocator, 100, 1)

# Later, someone changes it again...
mem.default_allocator_set(different_allocator)

# Now who frees `data`? With which allocator?
mem.free(mem.default_allocator, data, 100, 1)  # wrong allocator - undefined behavior
```

This is "action at a distance" — the behavior of `mem.free(mem.default_allocator, ...)` depends on what some unrelated code did earlier. By making allocator values explicit and immutable, Brolang guarantees:

**Whatever you allocate with, you free with.**

### Custom Allocators

For specialized needs, you create explicit allocator instances. These are not global — you manage their lifetime and pass them where needed.

The examples in this section are future typed API sketches. The v1 allocator contract is still `mem.Allocator` plus byte-oriented `mem.alloc`/`mem.free`.

**Arena Allocator**: Fast bump allocation, bulk deallocation:

```
mem :: import "@std/mem"

process_file func(path []u8, allocator mem.Allocator) !Data {
    # arena manages its own backing memory via the supplied allocator
    arena := mem.Arena.init(mem.c_allocator, capacity: mem.megabytes(1))
    defer arena.deinit()

    # all temporary allocations from arena (fast bump allocation)
    file_contents := arena.alloc(u8, size: file_size)
    parsed := arena.alloc(ParsedData)         # size defaults to 1
    tokens := arena.alloc(Token, size: 1000)

    # ... process ...

    # escaping allocation uses the caller's allocator
    result := allocator.create(Data)
    mem.copy(result, parsed)

    return result
    # arena.deinit() frees all arena memory — no individual frees needed
}
```

**Pool Allocator**: O(1) fixed-size allocation, no fragmentation:

```
mem :: import "@std/mem"

EntitySystem :: struct {
    pool: mem.Pool(Entity),
}

init_entities func(allocator mem.Allocator) EntitySystem {
    return EntitySystem{
        pool = mem.Pool(Entity).init(allocator, capacity: 10_000),
    }
}

spawn func(sys: @mut EntitySystem) @Entity {
    return sys.pool.alloc()  # O(1), no fragmentation
}

despawn func(sys: @mut EntitySystem, entity: @Entity) void {
    sys.pool.free(entity)  # returned to pool for reuse
}
```

### Passing Allocators to Functions

As described in the escaping allocation rule, when a function heap-allocates memory that escapes its scope, it must accept an allocator parameter. The caller decides which allocator to use:

```
mem :: import "@std/mem"

# Function that uses caller's allocator
parse func(input []u8, allocator mem.Allocator) !ParseResult {
    buffer := mem.alloc(allocator, input.len, 1)
    defer mem.free(allocator, buffer, input.len, 1)
    
    # ... parse into buffer ...
    
    result := mem.alloc(allocator, parse_result_size, parse_result_alignment)
    return result
}

# Caller decides which allocator to use
main func() void {
    # use an arena for this parsing work
    arena := mem.Arena.init(mem.c_allocator, capacity: mem.kilobytes(64))
    defer arena.deinit()
    result := parse(input, &arena) catch |err| {
        # handle error
    }

    # or use a pool
    pool := mem.Pool(ParseResult).init(capacity: 100)
    defer pool.deinit()
    result := parse(input, &pool) catch |err| {
        # handle error
    }
}
```

### Memory Allocation Summary

| What | How | When to Use |
| -- | -- | -- |
| `mem.alloc(mem.c_allocator, n, a)` | Libc-backed allocator | General purpose byte allocation |
| `mem.realloc(mem.c_allocator, ptr, old_n, new_n, a)` | Libc-backed allocator | Resize while preserving alignment and up to `min(old_n, new_n)` bytes |
| `mem.free(mem.c_allocator, ptr, n, a)` | Libc-backed allocator | Free byte allocation with original size/alignment |
| `mem.alloc(allocator, n, a)` | Caller-provided allocator | Escaping allocations (returned or written to caller's data) |
| typed helpers / arenas / pools | Future APIs | Higher-level allocation patterns |

Note that `mem.c_allocator` is a `mem.Allocator`, so callers can pass it as the allocator argument when they don't need a specialized allocator — which is most of the time.

**The golden rule:** Allocate and free with the same allocator. In v1 this is explicit in the call sites; future diagnostics should use the escaping allocation rule to ensure the caller always knows which allocator was used.

### Compared to Other Languages

| Language | Approach | Brolang's Advantage |
| -- | -- | -- |
| C | Hidden malloc, easy to mismatch | Explicit allocator at call site |
| C++ | Allocator templates, complex | Simple, no template complexity |
| Rust | Explicit everywhere, verbose | Sensible defaults reduce noise |
| Zig | Allocator parameter threading | Only required for escaping allocations, not internal work |
| Odin | Hidden context parameter | Fully transparent, nothing hidden |
| Go | Hidden GC | Explicit control, no GC pauses |

Brolang sits in a sweet spot: explicit enough to always know what's happening, convenient enough that you don't drown in boilerplate.
