8.1 KiB
8.1 KiB
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 NAMEDname, returned as a first-classTypehandle. 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 aTypeInfovalue, and return the handle (so the one-shot form chains).type_info($T) -> TypeInfo— reflect a type INTO data (the inverse ofdefine'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 #builtins 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:
// 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 §7 step 3), and replaces a would-be enum($T) language feature.
How it works (the locked design)
- Two comptime interp builtins.
declaremints an emptytagged_unionslot in the type table;definedecodes theTypeInfovalue (variant-name strings + payloadType-tags) and completes the slot byte-identical to a source enum'sbuildEnumInfooutput, so it flows through enum codegen unmodified. The interp mutates the type table via aminthandle the host sets (setMintTable). - No syntactic constructor recognition. A
::binding or type-fn body that calls aType-returning fn is comptime-evaluated (evalComptimeType): the expression runs through the interpreter, thedeclare/definebuiltins mint the type, and the resulttype_tagis bound.decl.zigtriggers on a non-generic-> Typefn call;instantiateTypeFunctiontriggers on a type-fn body that returns adefine(…)call (or a bodied-> Typehelper) — seegeneric.zig:returnExprMintsType. - Name on
declare.declare("Name")carries the name as a compile-time string sopreregisterForwardTypes(inevalComptimeType) can register the forward type — and bind it as a type alias — BEFORE the body lowers. That's what makes a*Nameself-reference resolve (aName :: ctor()decl makesNamea const_decl author, so*Nameresolves through the forward-ALIAS path; the alias binding, not just the table registration, is what satisfies it). The interp'sdeclarereturns the same slot by name;definefills it in place. - Nominal identity rides the existing type-fn mangled-name instantiation cache:
RecvResult(i64)at two sites memoizes to ONETypeId(the body runs once;renameNominalTypere-keys the minted type to the mangled name). - Comptime-only, JIT-free.
declare/defineare interp ops; reaching them at runtime / emit is a hard error. - Undefined-until-defined.
declare()mints an undefined slot; using it (construct / match / size) before itsdefineis 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 tocallBuiltin(src/ir/lower/call.zig:tryLowerReflectionCall); interp exec +defineEnum+decodeVariantElements(src/ir/interp.zig);mintfield +setMintTable. - Comptime evaluation:
evalComptimeType/renameNominalType(src/ir/lower/comptime.zig); decl triggerfnReturnsTypeValue(src/ir/lower/decl.zig); type-fn triggerreturnExprMintsType+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.irsnapshot).
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
declare/definecomptime builtins + themintplumbing.- Comptime evaluation of a
Type-returning::RHS and type-fn body (the only triggers; no constructor-name knowledge in the compiler). - Name-in-
TypeInfo; nominal identity via the instantiation cache. field_typereflection (examples/0616).- Examples green on the floor:
0614(one-shot),0615(type-fn identity),0617(channel result types). - Self-reference — recursive enums via
declare("Name")+*Namein a constructor fn (preregisterForwardTypesregisters 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(name, variants: []EnumVariant)— the general enum constructor over a COMPUTED (value, non-literal) variant list. Pure sx inmeta.sx; exercisesdefinedecoding a value-arg slice.examples/0620(array-literal local) /0624(generic builder).- Comptime slice over a non-string aggregate —
arr[lo..hi]over an array yields a real slice value at comptime (base_tythreaded ontoSubslice; open-endedhifolded to the array's static length;subsliceElements).examples/0621. type_info($T) -> TypeInfo— reflectenum/tagged_union/struct/tupleINTO a value (inverse ofdefine's decode);definedecodes all three back (defineEnum/defineStruct/defineTuple, dispatched on the TypeInfo tag). Round-trips:examples/0619(enum) /0622(struct) /0623(tuple). The reflect/construct triad is complete.- Generic type-fn body locals — a generic
($T) -> Typecomptime-evaluates its FULL body (prelude statements + return), so a local before the return resolves (createComptimeFunctionWithPrelude/evalComptimeTypeBody).examples/0624. - Validation + loud diagnostics — by-value self-reference (
checkInfiniteSize, source1178+ constructed1182; issue 0139), duplicate variant/field names (1180),declare()neverdefine()d (1181, was averifySizespanic), and the 0140 bail-surfacing (1179). use-before-define is subsumed by these (no new check needed). Validation story COMPLETE. - Comptime
Listgrowth (issue 0141, DEFERRED) —List(T).appendat comptime bails (two layers: null comptime allocator at scanDecls +*Tslot_ptrstruct_get). Non-blocking; array-literal locals cover the use case.
Risks / watch
- Self-ref timing —
definefor 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/definecomptime-only: reaching them at runtime is a hard error (emit should bail loudly if one ever leaks into codegen).