Files
sx/issues/0142-comptime-minted-all-void-enum-binds-any.md
agra 9e3aabcf76 comptime VM: Phase 3 — register_type write side + payloadless-enum fixes
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).
2026-06-18 10:47:36 +03:00

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

  1. Real bug: defineEnum (and the new register_type) minted a fully payloadless enum as an all-void tagged_union, whose IR size disagrees with its LLVM size → verifySizes panic at codegen. Fix: mint a real .@"enum" when every variant is payloadless (src/ir/interp.zig defineEnum; src/ir/compiler_lib.zig handleRegisterType kind 2).
  2. Missing syntax (the "Any" error): EnumType.variant qualified 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 a Type value). Fix: src/ir/lower/expr.zig lowerFieldAccess now recognises a bare Enum.variant payloadless literal (mirroring the alias.Enum.variant namespace path), via the new isPayloadlessVariant. 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.spades errors field 'spades' not found on type 'Any'.
  • Expected: Suit is the minted enum; Suit.spades constructs 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.sxerror: 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 (definetagged_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.zig runComptimeTypeFunc / evalComptimeType — the result path. runComptimeTypeFunc already special-cases a zero-FIELD tagged_union (declared-but-never-defined); check whether an all-void (non-zero-field) tagged_union/enum is being normalized, rejected, or coalesced to .any somewhere on the way back. Print the TypeId returned by result.asTypeId() for the all-void vs mixed case to localize.
  • src/ir/interp.zig defineEnum (≈2157) / defineType — what TypeId/Value it returns for an all-void variant set; whether an all-void tagged_union interns/dedupes to a builtin (note: a 2-variant all-void union has no payload storage, so its structural key may collide with something — or replaceKeyedInfo may 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_union in the first place — and whether the .any leak 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.