Investigated the last deferred enhancement. List(T).append at comptime fails in two independent layers (both reproduce with plain List(i64); List works via #run because that evaluates at emit time, after lowering): 1. null comptime allocator — defaultContextValue looks up the CAllocator->Allocator thunks by name, but they aren't lowered at scanDecls time. Fixable by forcing getOrCreateThunks before the interp runs in runComptimeTypeFunc (tried, works for this layer). 2. struct_get through a *T slot_ptr chain (the *List receiver) — the deep part; comptime pointer/struct/slot resolution, its own session. Speculative fixes reverted (no end-to-end win without layer 2).
18 KiB
CHECKPOINT-METATYPE — comptime type metaprogramming (declare / define)
Companion to 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/`structTypeInfo variant.interp.zig:reflectTypeInfobuilds.struct(tag 1) for a source@"struct";definedispatches on the TypeInfo tag (defineType→defineEnum(0) /defineStruct(1)).defineStructmirrorsdefineEnum(duplicate-field-name check included) but completes the declare slot AS a struct viareplaceKeyedInfo— a KIND change re-keys the intern map, whereasupdatePreservingKey(the enum path) asserts the key is unchanged.lower/call.zig: the lower-timetype_infoguard now admits@"struct".examples/0622: programmaticVec2via.struct(.{ fields = … })+ a source-struct round-tripdefine(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: newBuiltinId.type_info(comptime-only, alongsidedeclare/define).lower/call.zig:tryLowerReflectionCall: the old "not yet implemented" bail is gone. Resolve$Tat lower time, reject a non-enum/non-tagged_unionarg loudly (good span:"type_info: 'X' is not an enum …"), else emitcallBuiltin(.type_info, [const_type], TypeInfo).interp.zig:reflectTypeInfo: builds the exact nested-aggregate ValuedefineEnumdecodes — variant{name, payload}, slice{data, len}, EnumInfo{variants}, TypeInfo{tag0, EnumInfo}. Atagged_unionreflects eachfield.ty(tagless variants already carryvoid); a payloadless`enumreflectsvoidper variant. Round-trips both source enums AND constructed (declare/define) enums.- emit unchanged —
type_infois always comptime-evaluated; the existing comptime-onlyelsearm inemitCallBuiltin(shared with declare/define) never fires. - Scope: enum-only (the symmetric inverse of
define's current capability). Struct/tupleTypeInfowidening 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) -> Typemints an empty (undefined) nominal slot NAMEDname;define(handle, info) -> Typedecodes theTypeInfovalue (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 amintTypeTable handle;defineEnum+decodeVariantElementsininterp.zig.- Self-reference:
evalComptimeType'spreregisterForwardTypesscans the comptime expression (and a called ctor fn's body) fordeclare("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 — aName :: ctor()decl makesNamea const_decl author, so a*Nameself-reference resolves through the forward-ALIAS path (type_aliases_by_source), which a barefindByNameregistration alone does NOT satisfy (it returns a pending empty-struct stub). The interp'sdeclarereturns that same slot;definefills it. - A
::binding or type-fn body calling aType-returning fn is comptime-evaluated (evalComptimeType) — no constructor-name knowledge.decl.zigtrigger =fnReturnsTypeValue; type-fn trigger =returnExprMintsType. - Nominal identity rides the type-fn instantiation cache (
renameNominalType). - The type NAME is on
declare(name)(compile-time string), notEnumInfo.
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 }/TypeInfodata types;declare/define/type_info/field_type#builtins;RecvResult($T)/TryResult($T)+ the generalmake_enum(name, variants)sx constructors overdefine(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 anenum/tagged_union/struct/tupleINTO aTypeInfovalue (call.zigemitscallBuiltin(.type_info);interp.zig:reflectTypeInfobuilds the Value).definedecodes.enum→ tagged_union,.struct→ struct,.tuple→ tuple (the last viareplaceKeyedInfo).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
Listgrowth —List(T).appendat 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 plainList(i64), not metatype-specific; List works via#runbecause that evaluates at EMIT time, after everything is lowered, while a metatype::const evaluates atscanDeclstime):- Null comptime allocator.
interp.zig:defaultContextValuebuilds the comptimecontext.allocatorby looking up__thunk_CAllocator_Allocator_alloc_bytesby name in the module's functions — but atscanDeclstime those protocol thunks aren't lowered yet, soalloc_fn/dealloc_fnare.null_valand any comptime allocation fails. FIX (tried, works for this layer): callself.getOrCreateThunks("Allocator", "CAllocator")(guarded by the same Context/Allocator/CAllocator-registered checkemitDefaultContextGlobaluses) before the interp runs incomptime.zig:runComptimeTypeFunc.createProtocolThunksaves/restores builder state, so calling it mid-lowering is safe. After this,alloc_fn=func_ref— but layer 2 still bails. struct_getthrough a*Tslot_ptr chain. A*Liststruct receiver (vs.append(…)→append(self: *List, …)) lands in the interp as a slot whose contents are a slot_ptr to the actual value —self.fielddoesstruct_getonbase=slot_ptr field_index=1and bails. The auto-deref ininterp.zig:.struct_getdoes a singleloadSlot; a chain-resolve loop did NOT fix it (the final loaded value is a field-pointer aggregate thatresolveFieldLoadturns 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*Treceivers, 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.
- Null comptime allocator.
Validation + loud diagnostics— COMPLETE. duplicate variant names (examples/1180);declare()neverdefine()d (examples/1181, was averifySizespanic); by-value self-reference for both source (1178) and CONSTRUCTED (1182) types viacheckInfiniteSize. 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*Namepointer needs no layout. Probes.sx-tmp/probe_ubd{1..4}.sxconfirmed: 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[]EnumVariantarray now yields a real slice value at comptime (was: bailed, string-only). Fix threadedbase_tyonto theSubsliceop so the interp tells an array from a{ptr,len}slice, folded open-endedhito a fixed array's static length at lower time (no runtime/.ir change), and addedinterp.zig:subsliceElements.examples/0621locks it.- Comptime
Listgrowth.List(T).appendat comptime bails ("struct_get: base has no fields"). Investigated — two layers (null comptime allocator at scanDecls +struct_getthrough a*Tslot_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) -> Typenow 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.
evalComptimeTypenow clearslast_bail_detailbefore the interp call and, on thecatch, emits a build-gating.errat the construction span ("comptime type construction failed: {detail}") before returning the.unresolvedpoison — so the reason is shown and no unresolved type reaches emission unannounced.examples/1179locks it. - issue 0139 — by-value self-reference segfault — RESOLVED (
checkInfiniteSizePass 1g emits a loud "infinitely sized" diagnostic + breaks the cycle;examples/1178locks it).
Log
- Generic type-fn body locals. A generic
($T) -> Typecomptime-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:createComptimeFunctionWithPreludelowers 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.
TypeInfogained`tuple(TupleInfo)(positional[]Type);reflectTypeInforeflects a.tuple(bare type_tags, tag 2),defineTypedispatches tag 2 →defineTuple(completes the slot as a structural tuple viareplaceKeyedInfo), and the lower-timetype_infoguard admits.tuple.examples/0623. Suite green (684). enum/struct/tuple all reflect + construct + round-trip. - Struct widening done.
TypeInfogained`struct(StructInfo);definedispatches on the tag (defineType→defineEnum/defineStruct),reflectTypeInforeflects a@"struct", and the lower-timetype_infoguard admits structs.defineStructusesreplaceKeyedInfo(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);*Namepointer use needs no layout. Probed.sx-tmp/probe_ubd{1..4}.sx: all clean diagnostics / correct results, no crash.examples/1182locks the by-value self-ref rejection for CONSTRUCTED enums (companion to source1178). - declare()-never-defined validation. A bare
declare("X")with nodefineleft a zero-field nominal slot that panicked at codegen (verifySizes).evalComptimeTypenow detects a zero-varianttagged_unionresult and emits a clean diagnostic naming the type. Self-reference (declared slot completed bydefine) is unaffected.examples/1181locks it. Suite green (681). - Duplicate variant-name validation. Two same-named variants in a constructed
enum used to silently succeed (ambiguous construction/match).
defineEnumnow bails naming the duplicate;evalComptimeTyperenders it (post-0140).examples/1180locks it. Suite green (680). - Comptime subslice over non-string aggregates.
arr[lo..hi]at comptime used to bail (interp.subslicewas string-only) and the open-endedhicame from a.lengthop that misread a 2-elem array as a{ptr,len}fat pointer. Fix (interp-only; runtime already correct viaLLVMTypeOf): threadbase_tyonto theSubsliceop, fold open-endedhito a fixed array's static length at lower time (no IR/.ir change), addsubsliceElements.examples/0621mints an enum fromdirs[0..2]. Suite green (679). make_enumdone. General enum constructormake_enum(name, variants: []EnumVariant) -> Typeinmeta.sx(pure sx over declare/define). A non-generic builder assembles the variant list in a local, then mints from it —examples/0620exercisesdefine'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 —evalComptimeTypeswallowed the interp'slast_bail_detail. Now it clears the detail before the call and renders a build-gating.errat the construction span on thecatch.examples/1179locks 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). NewBuiltinId.type_info;lower/call.zigresolves$T, rejects non-enum loudly, emits the builtin;interp.zig:reflectTypeInfoconstructs the exact nested-aggregate ValuedefineEnumdecodes (variant{name,payload}/ slice{data,len}/ EnumInfo / TypeInfo.enum).tagged_unionreflectsfield.ty; payloadless`enumreflectsvoid. Round-trips source AND constructed enums. Enum-only; struct/tuple widening deferred.examples/0619locks it. Suite green (676).- By-value self-reference rejected (issue 0139, F5 partial). New
checkInfiniteSizepass (Pass 1g) detects by-VALUE containment cycles (source + comptime types, direct + mutual), emits a loud "infinitely sized" diagnostic, and breaks the cycle (was atypeSizeBytesstack-overflow segfault).*Self(pointer) stays valid.examples/1178locks the message. Suite green (675). - Self-reference done.
declare(name)+preregisterForwardTypes(forward type + alias before body lowers) →*Nameresolves; recursive*Listenum constructs, matches through the pointer, and traverses recursively.0618locks it.declaregained itsnamearg;EnumInfo.namedropped. 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 aType-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_typereflection (0616) unchanged. - Stream carved (earlier). Selected as the first async-first foundation: gates
channel result types (
RecvResult($T)) andrace's synthesized union, fully validated, self-contained, testable in isolation (06xxcomptime).