Files
sx/current/CHECKPOINT-METATYPE.md
agra f845fc6413 issue(0139): by-value self-referential type segfaults (typeSizeBytes recursion)
Discovered while testing metatype self-reference: a by-VALUE self-ref
(`payload = List`, not `*List`) infinite-loops typeSizeBytes → segfault
instead of a loud "infinite size" diagnostic. PRE-EXISTING — a hand-written
source enum `enum { node: Bad; leaf }` crashes identically, so it's a
general type-system gap (the comptime F5 by-value-rejection inherits the
fix). Filed per the IMPASSABLE rule; metatype checkpoint notes it.
2026-06-16 22:10:53 +03:00

5.2 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

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) 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 still bails loudly (call.zig:tryLowerReflectionCall) — pending.

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

  • make_enum(variants: []EnumVariant) sx helper over a COMPUTED (non-literal) variant list — exercises the interpreter decoding a value-arg slice in define (vs. the literal .[ … ] the current examples use).
  • type_info($T) -> TypeInfo — reflect a type INTO a value (inverse of define's decode); widen TypeInfo past `enum + construct a []EnumVariant value in the interp. Bails loudly today in call.zig:tryLowerReflectionCall. Its own focused effort.
  • Validation + loud diagnostics — duplicate variant names, by-VALUE self-reference (payload = List not *List → infinite size), a declare() never define()d (hard error), use-before-define.

Known issues

  • issue 0139 — by-value self-reference segfaults (typeSizeBytes infinite recursion). A by-VALUE self-referential type (payload = List instead of *List) stack-overflows instead of emitting a loud "infinite size" diagnostic. PRE-EXISTING (a hand-written source enum enum { node: Bad; leaf } crashes identically) — a general type-system gap, not specific to declare/define. Closes the F5 "by-VALUE self-reference rejected" item for the comptime path once fixed. Filed issues/0139-byvalue-self-reference-segfault.md; session stopped here per the CLAUDE.md IMPASSABLE rule (do not work around / roll forward after filing).

Log

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