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).
This commit is contained in:
127
issues/0142-comptime-minted-all-void-enum-binds-any.md
Normal file
127
issues/0142-comptime-minted-all-void-enum-binds-any.md
Normal file
@@ -0,0 +1,127 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user