Files
sx/current/CHECKPOINT-METATYPE.md

14 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 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 (defineTypedefineEnum (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 #builtins; 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 INTO a TypeInfo value (call.zig emits callBuiltin(.type_info); interp.zig:reflectTypeInfo builds the Value). define decodes .enum → tagged_union and .struct → struct. Tuple widening pending. examples/0619 (enum) / 0622 (struct) round-trip.

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

Pick any (independent):

  • Widen type_info / TypeInfo to `tuple — the last shape. `enum and `struct now ship (examples/0619/0622). A tuple has positional (unnamed) element types; needs a TupleInfo { elements: []Type } in meta.sx, a reflectTypeInfo arm for .tuple, and a defineTuple (mints a .tuple type).
  • 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. Building variants via List(EnumVariant).append at comptime bails ("struct_get: base has no fields") — the allocator/List protocol path isn't fully interp-evaluable. Probe .sx-tmp/probe_makeenum.sx.
  • Generic type-fn body locals. A generic ($T) -> Type comptime-evals only its return EXPRESSION (generic.zig:1760, findReturnTypeExpr), so a local before the return is unresolved. Workaround: build the list inline in the return, or use a non-generic () -> Type builder (whose whole body is evaluated — this is what examples/0620 uses). Probe .sx-tmp/probe_me5.sx.

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

  • Struct widening done. TypeInfo gained `struct(StructInfo); define dispatches on the tag (defineTypedefineEnum/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).