# 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 **`type_info` / `define` widened to TUPLE types — reflect/construct triad complete.** `TypeInfo` gained a `` `tuple(TupleInfo) `` variant (`TupleInfo{ elements: []Type }`, positional/unnamed). `reflectTypeInfo` builds `.tuple` (tag 2) as bare `type_tag` elements; `defineTuple` decodes `[]Type` and completes the declare slot as a structural `.tuple` via `replaceKeyedInfo` (tuples are structural, so the declared name is vestigial, but the slot is completed in place so `define` returns the handle like enum/struct). `call.zig`'s `type_info` guard admits `.tuple`. `examples/0623` (programmatic `Pair` + source-tuple round-trip). Suite green (684). All three TypeInfo shapes now reflect + construct + round-trip (`0619` enum, `0622` struct, `0623` tuple). ## Earlier — struct widening **`type_info` / `define` widened to STRUCT types.** `TypeInfo` gained a `` `struct(StructInfo) `` variant (`StructField{ name, type }`); the metatype system now reflects AND constructs structs, not only enums. - `meta.sx`: `StructField` / `StructInfo` / `` `struct `` TypeInfo variant. - `interp.zig`: `reflectTypeInfo` builds `.struct` (tag 1) for a source `@"struct"`; `define` dispatches on the TypeInfo tag (`defineType` → `defineEnum` (0) / `defineStruct` (1)). `defineStruct` mirrors `defineEnum` (duplicate-field-name check included) but completes the declare slot AS a struct via `replaceKeyedInfo` — a KIND change re-keys the intern map, whereas `updatePreservingKey` (the enum path) asserts the key is unchanged. - `lower/call.zig`: the lower-time `type_info` guard now admits `@"struct"`. - `examples/0622`: programmatic `Vec2` via `.struct(.{ fields = … })` + a source-struct round-trip `define(declare("RowCopy"), type_info(Row))`. Enum path (`0619`) unchanged. Suite green (683). Tuple is the last shape (Next step). ## Earlier — make_enum **`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. Pure sx in `meta.sx`. `examples/0620` assembles the list in a local then mints, exercising `define`'s value-arg SLICE decode. ## 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`/`struct`/`tuple` INTO a `TypeInfo` value (`call.zig` emits `callBuiltin(.type_info)`; `interp.zig:reflectTypeInfo` builds the Value). `define` decodes `.enum` → tagged_union, `.struct` → struct, `.tuple` → tuple (the last via `replaceKeyedInfo`). `examples/0619` (enum) / `0622` (struct) / `0623` (tuple) round-trip. All three TypeInfo shapes ship. ## 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 The reflect/construct triad is COMPLETE — `` `enum `` (`0619`), `` `struct `` (`0622`), `` `tuple `` (`0623`) all reflect AND construct + round-trip. Remaining METATYPE work is ONE deferred enhancement, a clean diagnostic rather than a crash: - **Comptime `List` growth** — `List(T).append` at comptime bails ("struct_get: base has no fields"). Doesn't block anything: array-literal locals already build variant lists (`examples/0620`/`0624`). Probe `.sx-tmp/probe_makeenum.sx` / `probe_li64.sx`. **Investigated — it's TWO layers** (both reproduce with plain `List(i64)`, not metatype-specific; List works via `#run` because that evaluates at EMIT time, after everything is lowered, while a metatype `::` const evaluates at `scanDecls` time): 1. **Null comptime allocator.** `interp.zig:defaultContextValue` builds the comptime `context.allocator` by looking up `__thunk_CAllocator_Allocator_alloc_bytes` by name in the module's functions — but at `scanDecls` time those protocol thunks aren't lowered yet, so `alloc_fn`/`dealloc_fn` are `.null_val` and any comptime allocation fails. FIX (tried, works for this layer): call `self.getOrCreateThunks("Allocator", "CAllocator")` (guarded by the same Context/Allocator/CAllocator-registered check `emitDefaultContextGlobal` uses) before the interp runs in `comptime.zig:runComptimeTypeFunc`. `createProtocolThunk` saves/restores builder state, so calling it mid-lowering is safe. After this, `alloc_fn=func_ref` — but layer 2 still bails. 2. **`struct_get` through a `*T` slot_ptr chain.** A `*List` struct receiver (`vs.append(…)` → `append(self: *List, …)`) lands in the interp as a slot whose contents are a slot_ptr to the actual value — `self.field` does `struct_get` on `base=slot_ptr field_index=1` and bails. The auto-deref in `interp.zig:.struct_get` does a single `loadSlot`; a chain-resolve loop did NOT fix it (the final loaded value is a field-pointer aggregate that `resolveFieldLoad` turns back into a slot_ptr — List's comptime representation uses field-pointers + slot_ptrs the struct_get path doesn't fully resolve). This is the deep part: comptime pointer/struct/slot resolution for `*T` receivers, its own focused effort. Both speculative fixes were REVERTED (no end-to-end testable win without layer 2). The metatype surface (declare/define/type_info/field_type + make_enum) is feature-complete for the locked design; generic type-fn body locals now work too. - ~~**Validation + loud diagnostics**~~ — COMPLETE. duplicate variant names (`examples/1180`); `declare()` never `define()`d (`examples/1181`, was a `verifySizes` panic); by-value self-reference for both source (`1178`) and CONSTRUCTED (`1182`) types via `checkInfiniteSize`. **use-before-define needs no new check** — it's subsumed by the existing guards: a by-value cycle → `checkInfiniteSize` ("infinitely sized"); an unfinished slot → declare-never- defined; a bad/non-Type payload → a 0140 clean bail; a forward reference resolves correctly via in-place slot mutation (`updatePreservingKey`); a `*Name` pointer needs no layout. Probes `.sx-tmp/probe_ubd{1..4}.sx` confirmed: no remaining crash or silent-corruption, only clean diagnostics / correct results. ### 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.** `List(T).append` at comptime bails ("struct_get: base has no fields"). Investigated — two layers (null comptime allocator at scanDecls + `struct_get` through a `*T` slot_ptr chain); see the detailed writeup under "Next step". Layer 1 has a known fix; layer 2 is deep. Probe `.sx-tmp/probe_makeenum.sx`. - ~~Generic type-fn body locals~~ — DONE. A generic `($T) -> Type` now comptime-evaluates its FULL body (prelude statements + return), so a local before the return resolves. `createComptimeFunctionWithPrelude` + `evalComptimeTypeBody`; no-prelude bodies stay on the old path. `examples/0624`. ## 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 - **Generic type-fn body locals.** A generic `($T) -> Type` comptime-evaluated only its return EXPRESSION, so a local before the return was unresolved. Now a body with a prelude (statements before the return) has its FULL body evaluated: `createComptimeFunctionWithPrelude` lowers the pre-return statements into the comptime function's scope, then the return expr. No-prelude bodies (RecvResult etc.) stay on the old path → zero regression. `examples/0624`. Suite green (685). - **Tuple widening done — reflect/construct triad complete.** `TypeInfo` gained `` `tuple(TupleInfo) `` (positional `[]Type`); `reflectTypeInfo` reflects a `.tuple` (bare type_tags, tag 2), `defineType` dispatches tag 2 → `defineTuple` (completes the slot as a structural tuple via `replaceKeyedInfo`), and the lower-time `type_info` guard admits `.tuple`. `examples/0623`. Suite green (684). enum/struct/tuple all reflect + construct + round-trip. - **Struct widening done.** `TypeInfo` gained `` `struct(StructInfo) ``; `define` dispatches on the tag (`defineType` → `defineEnum`/`defineStruct`), `reflectTypeInfo` reflects a `@"struct"`, and the lower-time `type_info` guard admits structs. `defineStruct` uses `replaceKeyedInfo` (kind change: tagged_union declare slot → struct). `examples/0622` (programmatic build + source round-trip). Suite green (683). Tuple is the last remaining shape. - **Validation story COMPLETE.** use-before-define needs no new check — subsumed by `checkInfiniteSize` (by-value cycles), declare-never-defined (unfinished slots), 0140 bails (bad payloads), and in-place slot mutation (forward refs); `*Name` pointer use needs no layout. Probed `.sx-tmp/probe_ubd{1..4}.sx`: all clean diagnostics / correct results, no crash. `examples/1182` locks the by-value self-ref rejection for CONSTRUCTED enums (companion to source `1178`). - **declare()-never-defined validation.** A bare `declare("X")` with no `define` left a zero-field nominal slot that panicked at codegen (`verifySizes`). `evalComptimeType` now detects a zero-variant `tagged_union` result and emits a clean diagnostic naming the type. Self-reference (declared slot completed by `define`) is unaffected. `examples/1181` locks it. Suite green (681). - **Duplicate variant-name validation.** Two same-named variants in a constructed enum used to silently succeed (ambiguous construction/match). `defineEnum` now bails naming the duplicate; `evalComptimeType` renders it (post-0140). `examples/1180` locks it. Suite green (680). - **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).