User-directed redirection. The compiler should expose ONLY declare() and
define(handle, info) as comptime type-table primitives; reify / make_enum /
RecvResult / TryResult all become plain sx in meta.sx (reify ==
{ h := declare(); define(h, info); return h; }). The AST-walking reifyType
and every syntactic reify recognition (decl.zig E :: reify hook, generic.zig
findReturnReifyCall routing) are to be DELETED, replaced by generic comptime
evaluation of a Type-returning expression.
PLAN-REIFY gains a RE-ARCHITECTURE section: the irreducible compiler floor
(declare = empty nominal slot; define = decode a TypeInfo VALUE + fill via
updatePreservingKey; comptime-eval a Type-returning ::-RHS/type-fn body),
the resolved naming/identity story (declare mints anonymous, the binding site
names it; identity via the existing instantiation cache), and an F1-F5 phase
table that re-greens 0614/0615/0617 on the floor.
No code change in this commit — the in-session Phase 3.2 attempt (make_enum +
eval-decode reader) was reverted (reset to 9306ad5) so the floor is built
first. Checkpoint records the revert + sets next step = F1.
16 KiB
PLAN-REIFY — comptime type reflection + construction (type_info / reify)
Goal
Add the two comptime metaprogramming builtins — type_info($T) -> TypeInfo
(reflect a type → data) and reify(info: TypeInfo) -> Type (construct a new
nominal type from data) — plus the sx-lib helpers (make_enum, field_type,
RecvResult/TryResult) built over them. This is step 3 of the async-first
sequence (../design/execution-evolution-roadmap.md
§7); it gates channel result types (RecvResult($T)) and race's synthesized
tagged-union, and replaces a would-be enum($T) generic-enum language feature.
Rationale + the five validated contracts: design doc §7 step 3 + §8.1. The approach was grounded by three codebase reviewers — it is a small extension reusing existing machinery, not net-new architecture.
Locked design (the five reify contracts — all codebase-validated)
- Nominal identity via type-fn memoization.
RecvResult(i64)is oneTypeIdbecause type-fns dedup by mangled(fn,args)name (generic.zig:1620-1629) + reifyfindByName. NOT structural dedup — enums are nominal (types.zig:1110). - Functional through codegen. A reify'd enum has no backing AST decl, and
every enum stage is type-table-driven (layout, construct, match+exhaustiveness,
toLLVMType,type_name/format) — so it flows through unmodified. - Validate loudly at the
intern/internNominalchoke point (types.zig:411-439). - Comptime-only, JIT-free — a type-table op in the interpreter; no S1 dependency.
- Reference-based self-reference (
*Self/[]Self) via an explicitdeclare()→define()pair (reserve-placeholder→complete reuse ofnominal.zig:86/108/120,types.zig:442); by-value recursion rejected. (See Phase 4 — thedeclare/defineAPI supersedes the earlierreify_recclosure.)
Surface follows the #builtin pattern of the existing reflection builtins
(type_of/field_count/field_name in library/modules/std/core.sx,
specs.md:2594-2600) — NOT the BuildOptions compiler-hook registry.
Key code anchors (verified by review)
- Type minting:
TypeTable.intern/internNominal—src/ir/types.zig:411-439. - Type-fn instantiation + mangled-name cache —
src/ir/lower/generic.zig:1575-1689(cache check:1620-1629; register inline-struct result:1663-1689). - Forward-declare reserve (recursive types) —
src/ir/lower/nominal.zig:86/108/120; complete a forward-declared type —src/ir/types.zig:442. - Enum codegen (all type-table-driven, the reify target shape): size
types.zig:633-636;resolveVariantIndexlower/expr.zig:1159-1177; matchlower/control_flow.zig:748-945;toLLVMTypebackend/llvm/types.zig:111-154;type_nametypes.zig:846-882. - Existing reflection builtins to mirror —
core.sx(#builtin) + their interp/lower handlers (src/ir/interp.zigtype_name/reflection at ~:1911). - Match form —
specs.md:408-424.
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, deterministic), 11xx (diagnostics for loud failures).
RE-ARCHITECTURE (user-directed, 2026-06-16) — declare/define are the ONLY compiler primitives
Decision: the compiler must expose only the two comptime type-table
primitives declare() and define(handle, info). reify, make_enum,
RecvResult, TryResult all become plain sx in meta.sx — reify is literally
{ h := declare(); define(h, info); return h; }. The AST-walking reifyType and
every syntactic recognition of reify (decl.zig's E :: reify(...) hook,
generic.zig's findReturnReifyCall type-fn routing) are deleted — replaced by
generic comptime evaluation of a Type-returning expression.
This supersedes the old builtin-reify approach of Phases 0/1/3 below (which are
GREEN today but compiler-coupled). Phase 3.2 (make_enum + the eval-decode reader)
was implemented then reverted (commit reset to 9306ad5) to rebuild on this floor.
The irreducible compiler floor (cannot be sx — mints TypeIds / evaluates types)
declare() -> Type— intern a fresh EMPTY forward nominal slot (uniquenominal_id, anonymous name), return it as a first-classTypevalue. Reuses thereserveShadowEnumSlotshape (nominal.zig:98).define(handle: Type, info: TypeInfo)— decode theTypeInfovalue (the revertedreifyEnumFromVariantsValuedecode logic moves here: interpValue→ names + payloadType-tags →tagged_unionbyte-identical tobuildEnumInfo), fill the slot viaupdatePreservingKey(types.zig:442). Loud on bad input.- Comptime-evaluate a Type-returning
::RHS / type-fn body through the interpreter to atype_tag, then bind / rename / identity-cache. This is the new general capability that REPLACES thereify-name pattern-matching.
Naming + identity (resolved)
declare()mints ANONYMOUS; the binding site names it: a directX :: <expr>renames the handle toX; a type-fn instantiation renames to the mangled name.- Identity (Contract 1) still rides the existing
instantiateTypeFunctionmangled-name cache —RecvResult(i64)memoizes, body (hencedeclare) runs once.
Floor phases (REPLACES old Phases 0/1/3; re-greens 0614/0615/0617)
| Step | Commit | What | Files |
|---|---|---|---|
| F1.0 | xfail | examples/06xx mints a recursive enum directly: List :: declare(); + define(List, .enum(.{ variants = .[ … payload = *List … ] })); construct + match a cons/nil list. Red (declare/define undefined). |
examples/06xx-* |
| F1.1 | green | declare/define as comptime builtins + the X :: declare() decl hook + scan-time define(...) side-effect + the value-decode (from the reverted 3.2). Self-ref works because the handle predates the body. |
src/ir/lower/{decl,nominal}.zig, src/ir/interp.zig, meta.sx |
| F2.0 | xfail | Reimplement reify in meta.sx as declare+define+return; switch E :: reify(...) to the general comptime-Type-expr decl path; delete the E :: reify AST special-case + reifyType literal walk. Red until the general path lands. |
meta.sx, src/ir/lower/decl.zig |
| F2.1 | green | General comptime evaluation of a Type-returning :: RHS (run interp → type_tag → rename/bind). examples/0614 green again with ZERO reify knowledge in the compiler. |
src/ir/lower/decl.zig, src/ir/interp.zig |
| F3.0 | xfail | Delete findReturnReifyCall routing; make instantiateTypeFunction run a Type-returning body through the interp generally. Red for 0615/0617 until it lands. |
src/ir/lower/generic.zig |
| F3.1 | green | General type-fn body comptime-eval + rename to mangled name + identity cache. examples/0615 (cross-site identity) + 0617 (RecvResult/TryResult) green on the floor. |
src/ir/lower/generic.zig |
| F4 | xfail→green | make_enum(variants) in meta.sx — now TRIVIAL (reify is sx, type-fns eval generally). New examples/06xx (inline + computed variants). |
meta.sx, examples/06xx-* |
| F5 | xfail→green | Validation + loud diagnostics: dup variant names, by-VALUE self-ref (payload = List not *List → "infinite size"), a declare() never define()d (hard error at end of comptime). |
src/ir/interp.zig / src/ir/types.zig, examples/11xx-* |
type_info($T) -> TypeInfo(old Phase 2.2 — reflect a type INTO a value) is orthogonal to this floor and still pending; it CONSTRUCTS aTypeInfovalue (inverse ofdefine's decode). Round-trips through the floor once it lands.
(SUPERSEDED by the re-architecture above) Original builtin-reify phases
Phase 0 — reify of a flat enum (the core)
| Step | Commit | What | Files |
|---|---|---|---|
| 0.0 | lock | TypeInfo/EnumInfo/EnumVariant lib types in core.sx (data only); reify/type_info/field_type as bodyless #builtin decls (parsed, unimplemented → loud bail). Unit: decls parse. |
library/modules/std/core.sx, src/ir/interp.zig |
| 0.1 | xfail | examples/06xx-comptime-reify-enum.sx — reify(.enum_(.{variants=[.{name="value",payload=i64},.{name="closed",payload=void}]})), construct .value(3), match it. Red (reify unimplemented). |
examples/06xx-* |
| 0.2 | green | implement reify(.enum_) → build EnumInfo/TaggedUnionInfo TypeInfo, internNominal(info, fresh_nominal_id), return TypeId. Example green; construct + match work unmodified (Contract 2). |
src/ir/interp.zig, (src/ir/types.zig if a helper is wanted) |
Phase 1 — type-fn → reify identity
| Step | Commit | What | Files |
|---|---|---|---|
| 1.0 | xfail | examples/06xx-comptime-reify-typefn-identity.sx — R :: ($T)->Type { reify(...) }; assert R(i64) from two sites is ONE type (assignable/matchable across sites). Red if reify-result not registered by mangled name. |
examples/06xx-* |
| 1.1 | green | register a reify-returning type-fn's result under the instantiation mangled name (mirror the inline-struct path generic.zig:1663-1689). Identity holds (Contract 1). |
src/ir/lower/generic.zig |
Phase 2 — field_type (done) + type_info (reflect → value, pending)
| Step | Commit | What | Files |
|---|---|---|---|
| 2.0 | xfail | reflect a struct/tuple/tagged-union → read field/variant names + types (field_type($T,i)). Red. |
examples/0616-* |
| 2.1 | green | DONE. field_type($T, i) -> Type over the type table (fieldTypeOf in generic.zig): struct field / tagged-union+union variant payload / tuple element / array+vector element; OOB + memberless → loud poison. Folds at lower time, composes in type_eq/type_name. |
src/ir/lower/generic.zig |
| 2.2 | xfail→green | PENDING. type_info($T) -> TypeInfo — reflect a type into a TypeInfo value (the inverse of reify). Needs the TypeInfo data model widened (struct/tuple variants beyond `enum) AND comptime construction of a []EnumVariant-style value (slice of structs holding strings + Type tags) read from the type table. Still bails loudly in call.zig:tryLowerReflectionCall. Larger than 2.1 — its own step. |
library/modules/std/meta.sx, src/ir/interp.zig / call.zig |
Phase 3 — RecvResult/TryResult (done) + make_enum (pending)
| Step | Commit | What | Files |
|---|---|---|---|
| 3.0 | xfail | examples/0617 uses RecvResult(i64)/TryResult(i64) (construct + match). Red (undefined). |
examples/0617-* |
| 3.1 | green | DONE. RecvResult($T)/TryResult($T) as type-fns over reify in meta.sx — needs NO new machinery (reify-of-literal in a type-fn body = Phase 1). 0617 green. |
library/modules/std/meta.sx |
| 3.2 | pending | make_enum(variants: []EnumVariant) -> Type — sx lib over reify with a RUNTIME (non-literal) variants arg. BLOCKED on the generalized reify reader: reifyType currently reads a LITERAL TypeInfo off the AST, so reify(.enum(.{ variants = variants })) (variable, not a literal) can't be read yet. Pairs with Phase 2.2 (interp-evaluate the TypeInfo value instead of AST-walking). |
library/modules/std/meta.sx, src/ir/lower/nominal.zig |
Phase 4 — reference-based self-reference (explicit declare → define)
API decision (user-directed): an explicit
declare()/define(h, info)pair (the canonical declaration-vs-definition split), NOT areify_rec((self) => …)closure.declare()returns a forward-declared nominalTypeHANDLE — a realTypevalue usable freely in any laterTypeInfo(*List,[]List, and across types for MUTUAL recursion, which a one-selfclosure can't express).define(handle, info)fills the body.reify(info)stays as the one-shot sugar (= declare + define + return) for the non-recursive case. No special self-ref node inTypeInfo— a declared type is just aType.List :: declare(); // name from the `::` LHS define(List, .enum(.{ variants = .[ EnumVariant.{ name = "cons", payload = *List }, // reference it freely EnumVariant.{ name = "nil", payload = void } ] }));Loud invariants: (a) a
declare()neverdefine()d = hard error at end of comptime (no silent empty type); (b) by-VALUE self-inclusion (payload = List, not*List) rejected — infinite size, same check recursive source types get.
| Step | Commit | What | Files |
|---|---|---|---|
| 4.0 | xfail | declare() returns a forward nominal Type; define() fills it. Recursive enum via *Self (tree/list). Red. |
examples/06xx-* |
| 4.1 | green | declare = intern empty nominal slot (reserveShadowEnumSlot-style); define = buildEnumInfo + updatePreservingKey; struct-stub→tagged_union re-key already handled by internNamedTypeDecl (adoptsForwardStructStub). |
src/ir/lower/nominal.zig, src/ir/lower/decl.zig, src/ir/types.zig |
Phase 5 — validation + loud diagnostics
| Step | Commit | What | Files |
|---|---|---|---|
| 5.0 | xfail | examples/11xx-diagnostics-reify-* — dup variant names, non-integer backing, by-value self-reference ("infinite size; use *Self"). Pin the messages. |
examples/11xx-* |
| 5.1 | green | validate TypeInfo at the intern/internNominal choke point; emit diagnostics, never a broken type (Contract 3). |
src/ir/interp.zig / src/ir/types.zig |
RaceResult(tuple→tagged-union synthesis) is not in this stream — it lands withrace(async cluster), but it consumes exactly thetype_info+field_type+reifyprimitives built here.
Risks / watch
- Mangled-name plumbing (Phase 1) is the one real unknown — confirm the type-fn
path registers a reify-returned result (not just inline
struct {…}literals). Fallback: havereifyitself name the type by the instantiation key +findByName. - Self-ref completion (Phase 4) must reuse the existing recursive-type reserve→complete path; do not invent a new mutate-after-intern mechanism.
- Keep
reifycomptime-only: areifyreached at runtime is a hard error.
Status
- Phase 0 —
reifyflat enum (reify(.enum(...))mints a flat enum via the sharedbuildEnumInfopath;examples/0614green; Contract 2 confirmed) - Phase 1 — type-fn identity (
Box :: ($T)->Type { reify(...) }memoizes by mangled name;Box(i64)at two sites is one type;examples/0615green) - [~] Phase 2 —
field_typeDONE (examples/0616green);type_info(reflect → aTypeInfovalue) PENDING as step 2.2 - [~] Phase 3 —
RecvResult($T)/TryResult($T)DONE (examples/0617green, type-fns overreifyinmeta.sx);make_enum(variants)PENDING (step 3.2 — needs the generalized reify reader, pairs with 2.2) - Phase 2 —
type_info+field_type - Phase 3 —
make_enum+RecvResult/TryResult - Phase 4 — reference self-reference
- Phase 5 — validation + diagnostics
Kickoff prompt (paste into a fresh session)
Work the REIFY stream per
current/PLAN-REIFY.md(+ checkpointcurrent/CHECKPOINT-REIFY.md). Read the plan header (goal, five locked contracts, key anchors) first; rationale is indesign/execution-evolution-roadmap.md§7 step 3
- §8.1. This session = Phase 0 only (
TypeInfolib types +reifyof a flat enum: construct + match). Cadence (IMPASSIBLE): no commit both adds a test and makes it pass — lock, then xfail→green.zig build && zig build testafter every step. If you hit an unrelated compiler bug, follow the CLAUDE.md IMPASSIBLE RULE (file an issue, stop). Stop at the end of Phase 0; update the checkpoint.