Files
sx/current/PLAN-REIFY.md
agra ac8c689518 green(reify): field_type($T, i) -> Type over the type table
REIFY Phase 2.1. fieldTypeOf (lower/generic.zig, re-exported on Lowering)
returns the i-th member type of T: struct field / tagged-union + union
variant payload (.void for a tagless variant) / tuple element / array +
vector element. Out-of-range and memberless types poison to .unresolved
with a loud diagnostic (never a silent default). Wired into
resolveTypeCallWithBindings (replacing the Phase-2 bail); since it folds
to a TypeId at lower time it composes inside type_eq / type_name / any
type-arg slot.

examples/0616 green: struct fields (name via field_name + type via
field_type), type_eq fold, tagged-union payloads incl. quit -> void.
Suite green (672 examples, 447 unit).

type_info($T) -> TypeInfo (reflect into a value, inverse of reify) is
NOT done — still bails loudly; it's the larger Phase 2.2 step (widen the
TypeInfo data model + comptime value construction). Plan/checkpoint updated.
2026-06-16 19:06:57 +03:00

10 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)

  1. Nominal identity via type-fn memoization. RecvResult(i64) is one TypeId because type-fns dedup by mangled (fn,args) name (generic.zig:1620-1629) + reify findByName. NOT structural dedup — enums are nominal (types.zig:1110).
  2. 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.
  3. Validate loudly at the intern/internNominal choke point (types.zig:411-439).
  4. Comptime-only, JIT-free — a type-table op in the interpreter; no S1 dependency.
  5. Reference-based self-reference (*Self/[]Self) via an explicit declare()define() pair (reserve-placeholder→complete reuse of nominal.zig:86/108/120, types.zig:442); by-value recursion rejected. (See Phase 4 — the declare/define API supersedes the earlier reify_rec closure.)

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 / internNominalsrc/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; resolveVariantIndex lower/expr.zig:1159-1177; match lower/control_flow.zig:748-945; toLLVMType backend/llvm/types.zig:111-154; type_name types.zig:846-882.
  • Existing reflection builtins to mirror — core.sx (#builtin) + their interp/lower handlers (src/ir/interp.zig type_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).


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.sxreify(.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.sxR :: ($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 — make_enum + RecvResult/TryResult (sx lib)

Step Commit What Files
3.0 lock make_enum(variants) -> Type (sx lib over reify); RecvResult($T)/TryResult($T) as type-fns. Behavior-lock: RecvResult(i64) constructs + matches. library/modules/std/*

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 a reify_rec((self) => …) closure. declare() returns a forward-declared nominal Type HANDLE — a real Type value usable freely in any later TypeInfo (*List, []List, and across types for MUTUAL recursion, which a one-self closure 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 in TypeInfo — a declared type is just a Type.

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() never define()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 with race (async cluster), but it consumes exactly the type_info+field_type+reify primitives 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: have reify itself 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 reify comptime-only: a reify reached at runtime is a hard error.

Status

  • Phase 0 — reify flat enum (reify(.enum(...)) mints a flat enum via the shared buildEnumInfo path; examples/0614 green; 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/0615 green)
  • [~] Phase 2 — field_type DONE (examples/0616 green); type_info (reflect → a TypeInfo value) PENDING as step 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 (+ checkpoint current/CHECKPOINT-REIFY.md). Read the plan header (goal, five locked contracts, key anchors) first; rationale is in design/execution-evolution-roadmap.md §7 step 3

  • §8.1. This session = Phase 0 only (TypeInfo lib types + reify of 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 test after 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.