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).
128 lines
5.9 KiB
Markdown
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.
|