# CHECKPOINT-METATYPE — comptime type metaprogramming (`declare` / `define`)

Companion to [PLAN-METATYPE.md](PLAN-METATYPE.md). Update after every step (one
step at a time, per the cadence rule).

## Last completed step
**`make_enum(name, variants: []EnumVariant) -> Type`** — the general enum
constructor over `declare`/`define`, minting a nominal enum from a variant list
passed as a VALUE (not a hardcoded literal). Pure sx in `meta.sx`, no compiler
machinery — the open-ended form the channel-result constructors are special cases
of. Because `variants` is an ordinary comptime value, a non-generic builder
assembles it in a local before minting; `examples/0620` (`build_level` fills a
local array → `make_enum` mints `Level`) exercises `define` decoding a value-arg
SLICE (`decodeVariantElements`' slice branch) vs. the inline `.[ … ]` array the
0614–0618 examples pass directly. No compiler change (locks existing capability).
Suite green (678). See "make_enum follow-ups" under Next step for the deferred
free-form-construction gaps (subslice/List at comptime, generic-type-fn locals).

## Prior step
**`type_info($T)` reflection — enum round-trip.** Reflect a type INTO a `TypeInfo`
value (the inverse of `define`'s decode), so `define(declare(n), type_info(T))`
mints a byte-identical copy with NO literal variant list.

- `inst.zig`: new `BuiltinId.type_info` (comptime-only, alongside `declare`/`define`).
- `lower/call.zig:tryLowerReflectionCall`: the old "not yet implemented" bail is
  gone. Resolve `$T` at lower time, reject a non-`enum`/non-`tagged_union` arg
  loudly (good span: `"type_info: 'X' is not an enum …"`), else emit
  `callBuiltin(.type_info, [const_type], TypeInfo)`.
- `interp.zig:reflectTypeInfo`: builds the exact nested-aggregate Value
  `defineEnum` decodes — variant `{name, payload}`, slice `{data, len}`, EnumInfo
  `{variants}`, TypeInfo `{tag0, EnumInfo}`. A `tagged_union` reflects each
  `field.ty` (tagless variants already carry `void`); a payloadless `` `enum ``
  reflects `void` per variant. Round-trips both source enums AND constructed
  (declare/define) enums.
- emit unchanged — `type_info` is always comptime-evaluated; the existing
  comptime-only `else` arm in `emitCallBuiltin` (shared with declare/define)
  never fires.
- Scope: **enum-only** (the symmetric inverse of `define`'s current capability).
  Struct/tuple `TypeInfo` widening is a separate later step.

`examples/0619` locks it (source enum `circle:f64 / rect:i64 / empty` reflected →
reconstructed → constructs + matches). Full suite green (676 examples + units).

## Earlier step
**Self-reference — recursive enums via `declare("Name")` + `*Name`.** The
`declare`/`define` floor now supports self-referential types.

- `declare(name) -> Type` mints an empty (undefined) nominal slot NAMED `name`;
  `define(handle, info) -> Type` decodes the `TypeInfo` value (variant names +
  payload Type-tags), fills the slot byte-identical to a source enum, and returns
  the handle (one-shot form chains: `T :: define(declare("T"), info)`). Interp
  executes both against a `mint` TypeTable handle; `defineEnum` +
  `decodeVariantElements` in `interp.zig`.
- **Self-reference:** `evalComptimeType`'s `preregisterForwardTypes` scans the
  comptime expression (and a called ctor fn's body) for `declare("Name")` calls
  and, before the body lowers, registers each as an empty forward nominal type AND
  binds it as a type alias. The alias is essential — a `Name :: ctor()` decl makes
  `Name` a const_decl author, so a `*Name` self-reference resolves through the
  forward-ALIAS path (`type_aliases_by_source`), which a bare `findByName`
  registration alone does NOT satisfy (it returns a pending empty-struct stub). The
  interp's `declare` returns that same slot; `define` fills it.
- A `::` binding or type-fn body calling a `Type`-returning fn is
  **comptime-evaluated** (`evalComptimeType`) — no constructor-name knowledge.
  `decl.zig` trigger = `fnReturnsTypeValue`; type-fn trigger = `returnExprMintsType`.
- Nominal identity rides the type-fn instantiation cache (`renameNominalType`).
- The type NAME is on `declare(name)` (compile-time string), not `EnumInfo`.

Examples green: `0614` (one-shot), `0615` (type-fn identity), `0617` (channel
results), **`0618` (recursive `*List`: construct, match through pointer, recursive
traversal)**; `field_type` reflection `0616`. Full suite green (674 examples).

## Current state
- `modules/std/meta.sx`: `EnumVariant` / `EnumInfo{ name, variants }` / `TypeInfo`
  data types; `declare` / `define` / `type_info` / `field_type` `#builtin`s;
  `RecvResult($T)` / `TryResult($T)` + the general `make_enum(name, variants)` sx
  constructors over `define(declare(), …)`.
- Compiler primitives only: `declare`/`define` (construction), `field_type`
  (reflection). No constructor-name knowledge anywhere in the compiler — every
  named constructor is sx. `declare(name)` carries the type name (compile-time
  string) for forward-type registration.
- `type_info($T)` reflects an `enum`/`tagged_union` INTO a `TypeInfo` value
  (`call.zig` emits `callBuiltin(.type_info)`; `interp.zig:reflectTypeInfo` builds
  the Value). Enum-only; struct/tuple widening pending. `examples/0619` round-trip.

## Decision (kept)
**Meta lives in `modules/std/meta.sx`, not the prelude.** Declaring its data types
in the always-loaded prelude interns them into every module's type table and
shifts every `.ir` snapshot. On-demand import keeps the prelude clean.

## Next step
Pick any (independent):
- **Widen `type_info` / `TypeInfo` past `` `enum ``** — struct/tuple variants:
  add `` .`struct ``/`` .`tuple `` to the `TypeInfo` enum in `meta.sx`, teach
  `reflectTypeInfo` to build them, and teach `defineEnum` (→ a `defineType`) to
  decode them. Round-trips a struct through `define` once it lands.
- **Validation + loud diagnostics** (remaining) — duplicate variant names, a
  `declare()` never `define()`d (hard error at end of comptime), use-before-define.

### make_enum follow-ups (deferred capability gaps — NOT crashes; clean diagnostics)
`make_enum` itself is DONE (see Last completed step). Remaining adjacent
capabilities would let the variant list be built more freely; both error cleanly
(post-0140) rather than crash, so they're enhancements, not blockers:
- ~~Comptime slice over a non-string aggregate~~ — DONE. `arr[lo..hi]` over a
  `[]EnumVariant` array now yields a real slice value at comptime (was: bailed,
  string-only). Fix threaded `base_ty` onto the `Subslice` op so the interp tells
  an array from a `{ptr,len}` slice, folded open-ended `hi` to a fixed array's
  static length at lower time (no runtime/.ir change), and added
  `interp.zig:subsliceElements`. `examples/0621` locks it.
- **Comptime `List` growth.** Building variants via `List(EnumVariant).append` at
  comptime bails ("struct_get: base has no fields") — the allocator/List protocol
  path isn't fully interp-evaluable. Probe `.sx-tmp/probe_makeenum.sx`.
- **Generic type-fn body locals.** A *generic* `($T) -> Type` comptime-evals only
  its return EXPRESSION (`generic.zig:1760`, `findReturnTypeExpr`), so a local
  before the return is unresolved. Workaround: build the list inline in the return,
  or use a non-generic `() -> Type` builder (whose whole body is evaluated — this
  is what `examples/0620` uses). Probe `.sx-tmp/probe_me5.sx`.
- **Widen `type_info` / `TypeInfo` past `` `enum ``** — struct/tuple variants:
  add `` .`struct ``/`` .`tuple `` to the `TypeInfo` enum in `meta.sx`, teach
  `reflectTypeInfo` to build them, and teach `defineEnum` (→ a `defineType`) to
  decode them. Round-trips a struct through `define` once it lands.
- **Validation + loud diagnostics** (remaining) — duplicate variant names, a
  `declare()` never `define()`d (hard error), use-before-define. (By-value
  self-reference already rejected — issue 0139.)

## Known issues
- issue 0140 — comptime type-construction bail panicked instead of diagnosing —
  RESOLVED. `evalComptimeType` now clears `last_bail_detail` before the interp
  call and, on the `catch`, emits a build-gating `.err` at the construction span
  ("comptime type construction failed: {detail}") before returning the
  `.unresolved` poison — so the reason is shown and no unresolved type reaches
  emission unannounced. `examples/1179` locks it.
- issue 0139 — by-value self-reference segfault — RESOLVED (`checkInfiniteSize`
  Pass 1g emits a loud "infinitely sized" diagnostic + breaks the cycle;
  `examples/1178` locks it).

## Log
- **Comptime subslice over non-string aggregates.** `arr[lo..hi]` at comptime
  used to bail (interp `.subslice` was string-only) and the open-ended `hi` came
  from a `.length` op that misread a 2-elem array as a `{ptr,len}` fat pointer.
  Fix (interp-only; runtime already correct via `LLVMTypeOf`): thread `base_ty`
  onto the `Subslice` op, fold open-ended `hi` to a fixed array's static length at
  lower time (no IR/.ir change), add `subsliceElements`. `examples/0621` mints an
  enum from `dirs[0..2]`. Suite green (679).
- **`make_enum` done.** General enum constructor `make_enum(name, variants:
  []EnumVariant) -> Type` in `meta.sx` (pure sx over declare/define). A non-generic
  builder assembles the variant list in a local, then mints from it —
  `examples/0620` exercises `define`'s value-arg SLICE decode. No compiler change.
  Suite green (678). Deferred free-form gaps (subslice/List at comptime,
  generic-type-fn locals) noted under Next step — all clean diagnostics now, not
  crashes (post-0140), so enhancements rather than blockers.
- **issue 0140 fixed.** A comptime type-construction bail (`declare`/`define`/
  reflection) used to panic at LLVM emission ("unresolved type reached LLVM
  emission") or hide behind a cascade — `evalComptimeType` swallowed the interp's
  `last_bail_detail`. Now it clears the detail before the call and renders a
  build-gating `.err` at the construction span on the `catch`. `examples/1179`
  locks the empty-variants case. Suite green (677). Unblocks make_enum (its
  computed-slice decode failures now surface cleanly).
- **`type_info($T)` reflection done (enum round-trip).** New `BuiltinId.type_info`;
  `lower/call.zig` resolves `$T`, rejects non-enum loudly, emits the builtin;
  `interp.zig:reflectTypeInfo` constructs the exact nested-aggregate Value
  `defineEnum` decodes (variant `{name,payload}` / slice `{data,len}` / EnumInfo /
  TypeInfo `.enum`). `tagged_union` reflects `field.ty`; payloadless `` `enum ``
  reflects `void`. Round-trips source AND constructed enums. Enum-only;
  struct/tuple widening deferred. `examples/0619` locks it. Suite green (676).
- **By-value self-reference rejected (issue 0139, F5 partial).** New
  `checkInfiniteSize` pass (Pass 1g) detects by-VALUE containment cycles (source +
  comptime types, direct + mutual), emits a loud "infinitely sized" diagnostic,
  and breaks the cycle (was a `typeSizeBytes` stack-overflow segfault). `*Self`
  (pointer) stays valid. `examples/1178` locks the message. Suite green (675).
- **Self-reference done.** `declare(name)` + `preregisterForwardTypes` (forward
  type + alias before body lowers) → `*Name` resolves; recursive `*List` enum
  constructs, matches through the pointer, and traverses recursively. `0618` locks
  it. `declare` gained its `name` arg; `EnumInfo.name` dropped. Suite green (674).
- **declare/define floor established.** The comptime type-construction surface is
  two primitives (`declare`/`define`); all named constructors are sx. A `::` binding
  or type-fn body that calls a `Type`-returning fn is comptime-evaluated (the
  builtins mint the type) — no syntactic constructor recognition in the compiler.
  Examples 0614 (one-shot) / 0615 (type-fn identity) / 0617 (channel results) on the
  floor; `field_type` reflection (0616) unchanged.
- **Stream carved (earlier).** Selected as the first async-first foundation: gates
  channel result types (`RecvResult($T)`) and `race`'s synthesized union, fully
  validated, self-contained, testable in isolation (`06xx` comptime).
