# 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.
