The mutating compiler-API, minting types LAZILY at lowering time (single pass,
the existing runComptimeTypeFunc path — so the write side is legacy-only; the
VM isn't wired at lowering time, and the read-side readers stay dual-path):
declare_type(name) -> Type forward nominal handle (≈ declare)
pointer_to(t) -> Type build *T references
register_type(handle, kind, members) ONE kind-branching fill (≈ unified define)
register_type branches on kind IN THE COMPILER (subsuming define's per-kind
dispatch); codes match type_kind: 1 struct, 2 actual .@"enum", 3 tagged_union,
4 tuple. Members are {name: string, ty: Type}. A non-generic `-> Type` builder is
now flagged is_comptime (decl.zig) so its dead body permits the welded calls.
Graph support: forward declare_type handles + pointer_to express a mutually-
recursive A<->B graph (*A, *B, B-by-value) before bodies are filled. register_type
is idempotent — re-filling a nominal slot (a minting module reached via two import
edges) re-mints identically rather than erroring (nominalIdent reads identity from
any nominal kind).
Fixes (issue 0142):
- A fully payloadless comptime-minted enum was minted as an all-void tagged_union,
whose IR size disagrees with its LLVM size -> verifySizes panic. Now mints a real
.@"enum" (register_type kind 2 AND the metatype defineEnum).
- Bare `EnumType.variant` qualified construction of a payloadless variant wasn't
supported (failed for hand-written enums too — the type name lowered to a Type
value). Added in lowerFieldAccess via isPayloadlessVariant; payload-carrying
variants keep their call form.
Examples: 0631 (graph + actual enum + reflection), 0632 (make_enum all-void),
0633/0634/0635 (namespaced / bare / multi-edge import of a minted type), 0187
(qualified variant construction). Unit tests added.
Parity 697/697 (gate OFF and -Dcomptime-flat).
5.9 KiB
0142 — comptime-minted all-void (fully payloadless) enum
RESOLVED (2026-06-18). Two distinct issues were tangled in the original report (the "binds to
Any" symptom was a syntax misdiagnosis):
- Real bug:
defineEnum(and the newregister_type) minted a fully payloadless enum as an all-voidtagged_union, whose IR size disagrees with its LLVM size →verifySizespanic at codegen. Fix: mint a real.@"enum"when every variant is payloadless (src/ir/interp.zigdefineEnum;src/ir/compiler_lib.zighandleRegisterTypekind 2).- Missing syntax (the "Any" error):
EnumType.variantqualified construction of a payloadless variant wasn't supported (it failed for hand-written enums too —field 'X' not found on type 'Any', because the type name lowered to aTypevalue). Fix:src/ir/lower/expr.ziglowerFieldAccessnow recognises a bareEnum.variantpayloadless literal (mirroring thealias.Enum.variantnamespace path), via the newisPayloadlessVariant. Payload-carrying variants keep their call form (Shape.circle(2.0)).Regression tests:
examples/0632-comptime-metatype-make-enum-payloadless.sx(make_enum all-void),examples/0187-types-enum-qualified-variant.sx(qualified construction),examples/0631/0633/0634(compiler-API minted enums, bare + namespaced import).
Symptom (as originally — partly a syntax misdiagnosis; see banner)
A comptime type-fn that mints a fully payloadless enum (every variant
tagless, payload = void) via make_enum / declare + define returns a type
whose alias binds to Any instead of the minted enum — so any later use of the
alias as a type fails with field '<variant>' not found on type 'Any'.
- Observed:
Suit :: make_suit()(all-void variants) →Suit.spadeserrorsfield 'spades' not found on type 'Any'. - Expected:
Suitis the minted enum;Suit.spadesconstructs the variant (exactly as it does when at least one variant carries a payload).
The type is minted correctly — reflecting it through the comptime compiler
API shows kind = 2 (enum) and the right variant count; only the type-fn's
return value / alias binding is wrong. A mixed variant list (≥1 non-void
payload) works end-to-end; only the all-void case fails. This is independent of
the new register_type write API — it reproduces with the shipped metatype
make_enum.
Reproduction
#import "modules/std.sx";
#import "modules/std/meta.sx";
make_suit :: () -> Type {
return make_enum("Suit", EnumVariant.[
EnumVariant.{ name = "hearts", payload = void },
EnumVariant.{ name = "spades", payload = void },
]);
}
Suit :: make_suit();
main :: () {
s := Suit.spades;
if s == {
case .hearts: { print("hearts\n"); }
case .spades: { print("spades\n"); }
}
}
Run: ./zig-out/bin/sx run repro.sx → error: field 'spades' not found on type 'Any'.
Contrast (works — one variant carries a payload):
make_lvl :: () -> Type {
return make_enum("Lvl", EnumVariant.[
EnumVariant.{ name = "info", payload = void },
EnumVariant.{ name = "fatal", payload = i64 }, // ← non-void makes it work
]);
}
Lvl :: make_lvl();
(This is why examples/0620-comptime-metatype-make-enum.sx passes — its variant
list is mixed, not all-void.)
Investigation prompt
A comptime type-fn returning a fully payloadless enum (define →
tagged_union with every field ty == .void) binds the result alias to Any
(TypeId 13) instead of the minted type, even though the type is correctly
registered in the table (findable by name; reflects as kind=2). A mixed list (≥1
non-void payload) returns the correct TypeId. Find why the all-void case yields
Any.
Suspected area:
src/ir/lower/comptime.zigrunComptimeTypeFunc/evalComptimeType— the result path.runComptimeTypeFuncalready special-cases a zero-FIELDtagged_union(declared-but-never-defined); check whether an all-void (non-zero-field)tagged_union/enumis being normalized, rejected, or coalesced to.anysomewhere on the way back. Print theTypeIdreturned byresult.asTypeId()for the all-void vs mixed case to localize.src/ir/interp.zigdefineEnum(≈2157) /defineType— what TypeId/Valueit returns for an all-void variant set; whether an all-voidtagged_unioninterns/dedupes to a builtin (note: a 2-variant all-void union has no payload storage, so its structural key may collide with something — orreplaceKeyedInfomay leave the handle pointing at a coalesced slot).- Whether an all-void payloadless enum should mint as
.@"enum"(payloadless) rather than an all-void.tagged_unionin the first place — and whether the.anyleak is downstream of that representation choice.
Likely fix: ensure the type-fn returns the real minted TypeId for an all-void
payloadless enum (don't coalesce/normalize it to .any), or mint it as a proper
.@"enum". Whatever the cause, surface it — never silently substitute .any.
Verification: run the repro above → expect spades printed (exit 0). Also
confirm examples/0620 still passes and add an all-void variant case as a
regression example.
Context (why this was hit)
Surfaced while building the comptime compiler-API write side (Phase 3 of
current/PLAN-COMPILER-VM.md): register_type(handle, kind, members) minting an
actual payloadless enum (kind = 2 → .@"enum"). The new write API mints the
type correctly (reflection confirms kind=2/count=2), but the alias binding of a
fully-payloadless minted type hits this pre-existing metatype bug — so the
"actual enum" example can't be verified end-to-end until this is fixed. The
register_type work (struct, tagged_union with payloads, and the
mutually-recursive A↔B graph) is otherwise working; it is uncommitted, paused
pending this fix.