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

128 lines
5.9 KiB
Markdown

# 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
```sx
#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):
```sx
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.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.