Files
sx/current/CHECKPOINT-METATYPE.md

12 KiB
Raw Blame History

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

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 (not a hardcoded literal). Pure sx in meta.sx, no compiler machinery — the open-ended form the channel-result constructors are special cases of. Because variants is an ordinary comptime value, a non-generic builder assembles it in a local before minting; examples/0620 (build_level fills a local array → make_enum mints Level) exercises define decoding a value-arg SLICE (decodeVariantElements' slice branch) vs. the inline .[ … ] array the 06140618 examples pass directly. No compiler change (locks existing capability). Suite green (678). See "make_enum follow-ups" under Next step for the deferred free-form-construction gaps (subslice/List at comptime, generic-type-fn locals).

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 INTO a TypeInfo value (call.zig emits callBuiltin(.type_info); interp.zig:reflectTypeInfo builds the Value). Enum-only; struct/tuple widening pending. examples/0619 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 past `enum — struct/tuple variants: add .`struct/.`tuple to the TypeInfo enum in meta.sx, teach reflectTypeInfo to build them, and teach defineEnum (→ a defineType) to decode them. Round-trips a struct through define once it lands.
  • Validation + loud diagnostics (remaining) — duplicate variant names, a declare() never define()d (hard error at end of comptime), use-before-define.

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.
  • Widen type_info / TypeInfo past `enum — struct/tuple variants: add .`struct/.`tuple to the TypeInfo enum in meta.sx, teach reflectTypeInfo to build them, and teach defineEnum (→ a defineType) to decode them. Round-trips a struct through define once it lands.
  • Validation + loud diagnostics (remaining) — duplicate variant names, a declare() never define()d (hard error), use-before-define. (By-value self-reference already rejected — issue 0139.)

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

  • 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).