# PLAN-METATYPE — comptime type metaprogramming (`declare` / `define` + reflection)

## Goal

Comptime type metaprogramming with the smallest possible compiler surface:

- **`declare(name) -> Type`** — mint a NEW empty (undefined) nominal type NAMED
  `name`, returned as a first-class `Type` handle. The compiler registers the
  forward type at compile time, so the body can reference it (`*Name`).
- **`define(handle, info) -> Type`** — fill a declared handle's body from a
  `TypeInfo` *value*, and return the handle (so the one-shot form chains).
- **`type_info($T) -> TypeInfo`** — reflect a type INTO data (the inverse of
  `define`'s decode). *Done for enums* (`interp.zig:reflectTypeInfo`,
  `examples/0619`); struct/tuple widening pending.
- **`field_type($T, i) -> Type`** — the i-th field / variant-payload / element
  type of `$T`. *Done.*

These four `#builtin`s in `library/modules/std/meta.sx` are the **entire**
compiler surface. Every higher-level constructor is **plain sx built over
`declare`/`define`** — the compiler knows none of them by name:

```sx
// one-shot (non-recursive): declare + define chained, define returns the handle
T :: define(declare("T"), .enum(.{ variants = .[ … ] }));

// recursive: a ctor fn names the forward type via declare, references it as *Name
List :: make_list();
make_list :: () -> Type {
    h := declare("List");
    return define(h, .enum(.{ variants = .[
        EnumVariant.{ name = "cons", payload = *List },   // self-reference
        EnumVariant.{ name = "nil",  payload = void } ] }));
}

// type-fns are ordinary sx (channel result types, etc.)
RecvResult :: ($T: Type) -> Type {
    return define(declare("RecvResult"), .enum(.{ variants = .[
        EnumVariant.{ name = "value",  payload = T    },
        EnumVariant.{ name = "closed", payload = void } ] }));
}
```

This gates channel result types (`RecvResult($T)`) and `race`'s synthesized
tagged-union (design [../design/execution-evolution-roadmap.md](../design/execution-evolution-roadmap.md) §7 step 3), and replaces a would-be `enum($T)` language feature.

## How it works (the locked design)

1. **Two comptime interp builtins.** `declare` mints an empty `tagged_union` slot
   in the type table; `define` decodes the `TypeInfo` value (variant-name strings +
   payload `Type`-tags) and completes the slot byte-identical to a source enum's
   `buildEnumInfo` output, so it flows through enum codegen unmodified. The interp
   mutates the type table via a `mint` handle the host sets (`setMintTable`).
2. **No syntactic constructor recognition.** A `::` binding or type-fn body that
   calls a `Type`-returning fn is **comptime-evaluated** (`evalComptimeType`): the
   expression runs through the interpreter, the `declare`/`define` builtins mint the
   type, and the result `type_tag` is bound. `decl.zig` triggers on a non-generic
   `-> Type` fn call; `instantiateTypeFunction` triggers on a type-fn body that
   returns a `define(…)` call (or a bodied `-> Type` helper) — see
   `generic.zig:returnExprMintsType`.
3. **Name on `declare`.** `declare("Name")` carries the name as a compile-time
   string so `preregisterForwardTypes` (in `evalComptimeType`) can register the
   forward type — and bind it as a type alias — BEFORE the body lowers. That's
   what makes a `*Name` self-reference resolve (a `Name :: ctor()` decl makes
   `Name` a const_decl author, so `*Name` resolves through the forward-ALIAS path;
   the alias binding, not just the table registration, is what satisfies it). The
   interp's `declare` returns the same slot by name; `define` fills it in place.
4. **Nominal identity** rides the existing type-fn mangled-name instantiation cache:
   `RecvResult(i64)` at two sites memoizes to ONE `TypeId` (the body runs once;
   `renameNominalType` re-keys the minted type to the mangled name).
5. **Comptime-only, JIT-free.** `declare`/`define` are interp ops; reaching them at
   runtime / emit is a hard error.
6. **Undefined-until-defined.** `declare()` mints an undefined slot; *using* it
   (construct / match / size) before its `define` is a loud diagnostic. A *pointer*
   to an undefined slot (`*Self`) is fine — that's what self-reference needs.

## Key code anchors

- Builtins: `BuiltinId.declare` / `.define` (`src/ir/inst.zig`); lowering to
  `callBuiltin` (`src/ir/lower/call.zig:tryLowerReflectionCall`); interp exec +
  `defineEnum` + `decodeVariantElements` (`src/ir/interp.zig`); `mint` field +
  `setMintTable`.
- Comptime evaluation: `evalComptimeType` / `renameNominalType`
  (`src/ir/lower/comptime.zig`); decl trigger `fnReturnsTypeValue`
  (`src/ir/lower/decl.zig`); type-fn trigger `returnExprMintsType` +
  `instantiateTypeFunction` (`src/ir/lower/generic.zig`).
- Reflection: `field_type` → `fieldTypeOf` (`src/ir/lower/generic.zig`).
- Surface: `library/modules/std/meta.sx` (on-demand import — NOT the prelude, to
  avoid shifting every `.ir` snapshot).

## Cadence (IMPASSIBLE)

No commit may both add a test AND make it pass (xfail-then-green, or a behavior
lock). `zig build && zig build test` after every step. Never regenerate snapshots
while red. Examples: `06xx` (comptime), `11xx` (diagnostics).

## Status

- [x] `declare` / `define` comptime builtins + the `mint` plumbing.
- [x] Comptime evaluation of a `Type`-returning `::` RHS and type-fn body
      (the only triggers; no constructor-name knowledge in the compiler).
- [x] Name-in-`TypeInfo`; nominal identity via the instantiation cache.
- [x] `field_type` reflection (`examples/0616`).
- [x] Examples green on the floor: `0614` (one-shot), `0615` (type-fn identity),
      `0617` (channel result types).
- [x] **Self-reference** — recursive enums via `declare("Name")` + `*Name` in a
      constructor fn (`preregisterForwardTypes` registers the forward type + alias
      before the body lowers). `examples/0618` (recursive `*List`: construct, match
      through the pointer, recursive traversal). Mutual recursion / by-value-self-ref
      rejection fall out of the same mechanism (F5 adds the loud by-value check).
- [ ] **`make_enum(variants: []EnumVariant)`** sx helper over a COMPUTED
      (non-literal) variant list — exercises the interpreter decoding a value-arg
      slice in `define`.
- [x] **`type_info($T) -> TypeInfo`** (enum-only) — reflect an `enum`/`tagged_union`
      INTO a value (inverse of `define`'s decode): `interp.zig:reflectTypeInfo`
      constructs the `.enum(EnumInfo{ variants })` Value `defineEnum` decodes, so
      `define(declare(n), type_info(T))` round-trips. `examples/0619`. Non-enum args
      rejected loudly at lower time. **Still pending:** widen `TypeInfo` past
      `` `enum `` (struct/tuple variants) + the matching `define` decode arms.
- [~] **Validation + loud diagnostics** — by-VALUE self-reference DONE
      (`checkInfiniteSize` Pass 1g: loud "infinitely sized" diagnostic + cycle
      break, covers source + comptime types; `examples/1178`, issue 0139 resolved).
      Still pending: duplicate variant names, a `declare()` never `define()`d
      (hard error), use-before-define.

## Risks / watch

- **Self-ref timing** — `define` for the two-statement form must complete before any
  code uses the type's layout; a use-before-define must be a loud diagnostic, not a
  silent empty enum.
- Keep `declare`/`define` **comptime-only**: reaching them at runtime is a hard error
  (emit should bail loudly if one ever leaks into codegen).
