diff --git a/current/CHECKPOINT-COMPILER-API.md b/current/CHECKPOINT-COMPILER-API.md index 5a0ccbea..581db930 100644 --- a/current/CHECKPOINT-COMPILER-API.md +++ b/current/CHECKPOINT-COMPILER-API.md @@ -325,6 +325,26 @@ when reached (sentinels or accessor fns; see the design doc Risks). `List` growth; orthogonal, see `current/CHECKPOINT-METATYPE.md`.) ## Log +- **Phase 3 P3.3 (VM plan) — WRITE side: declare_type + pointer_to + ONE kind-branching register_type (2026-06-18).** + The mutating compiler-API: `declare_type(name) -> Type` (forward handle), `pointer_to(t) -> Type` + (build `*T`), and `register_type(handle, kind, members: []Member) -> Type` which branches on + `kind` IN THE COMPILER (subsuming define's per-kind dispatch). Take/return real `Type` values + (matching meta.sx declare/define). **Timing (per user): mint LAZILY at lowering time, single + pass** (the existing `runComptimeTypeFunc`), so the write side is **legacy-only** (`compiler_lib` + handlers) — the VM isn't wired at lowering time, no VM mirror needed; readers stay dual-path. + A non-generic `-> Type` builder is now flagged `is_comptime` (decl.zig) so its dead body permits + the welded calls. **Graph:** forward handles + `pointer_to` express mutually-recursive A↔B (`*A`, + `*B`, B-by-value); `register_type` is **idempotent** (re-fill a nominal slot reached via two + import edges — `nominalIdent`). `kind` codes match `type_kind` (1 struct · 2 actual `.@"enum"` · + 3 tagged_union · 4 tuple). **Fixed two bugs (issue 0142):** (a) a fully payloadless minted enum + was an all-void tagged_union → verifySizes panic; now a real `.@"enum"` (register_type kind 2 AND + metatype `defineEnum`); (b) bare `EnumType.variant` payloadless qualified construction wasn't + supported (failed for hand-written enums too) — added in `lowerFieldAccess` (`isPayloadlessVariant`). + 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). + **Parity 697/697** (gate ON and OFF); unit tests added. **Next (P3.4):** re-express + declare/define/type_info as sx over the compiler-API + delete the bespoke interp arms (needs the + VM hardened for lowering-time IR, or the metatype migrated onto the legacy compiler-API calls). - **Phase 3 P3.2b (VM plan) — kind + enum-value readers: `type_kind` + `type_field_value`; READ side complete (2026-06-18).** The last two read-only readers the metatype's `type_info(T)` needs (added to `compiler_lib.bound_fns` AND `Vm.callCompilerFn`, each backed by a `TypeTable` query both diff --git a/current/PLAN-COMPILER-VM.md b/current/PLAN-COMPILER-VM.md index 07e4f24e..2acaeeae 100644 --- a/current/PLAN-COMPILER-VM.md +++ b/current/PLAN-COMPILER-VM.md @@ -302,18 +302,36 @@ host through it: - **READ side now complete:** `find_type` + `type_kind` + `type_field_count` + `type_field_name` + `type_field_type` + `type_nominal_name` + `type_field_value` cover everything `reflectTypeInfo` reads. -- **Next (P3.3) — ONE `register_type(info)` write fn (revised direction, 2026-06-18):** per - the user, the mutating side is NOT per-kind (`register_struct`/`register_enum`/…) but a - SINGLE function that takes a type-info value and **branches on the kind in the compiler**, - minting the right `TypeInfo`. This subsumes `define`'s `defineStruct`/`defineEnum`/ - `defineTuple` dispatch into one host-side switch. Open design points to resolve when - reached: the flat-memory shape of the `info` argument the sx side passes (a tagged - `{ kind, payload }` over the readers' handle types), the mutable-table / host-ABI-vs- - target-ABI boundary, and pointer-escape/lifetime (escaping field arrays copied into - compiler-owned memory at the boundary). Re-expressing `declare`/`define`/`type_info` as sx - (the metatype, which runs at LOWERING time) still needs the VM hardened against malformed - lowering-time IR first — keep that on the legacy path until then (see the resume note in - CHECKPOINT-COMPILER-API.md). +- **(P3.3) WRITE side — `declare_type` + `pointer_to` + ONE kind-branching `register_type` (DONE).** + The mutating side is a SINGLE `register_type(handle, kind, members)` that branches on `kind` + IN THE COMPILER (subsuming `define`'s `defineStruct`/`defineEnum`/`defineTuple`), plus + `declare_type(name) -> Type` (forward handle) and `pointer_to(t) -> Type` (build `*T` + references). They take/return real `Type` values (matching meta.sx's declare/define). + - **Timing decision (per the user):** mint LAZILY at LOWERING time (single pass, NOT a + pre-emit phase, NOT two-pass) — the existing `runComptimeTypeFunc` path. So the write + side is **legacy-only** (`compiler_lib` handlers); the VM isn't wired at lowering time, so + no VM mirror is needed (the read-side readers stay dual-path for emit-time reflection). A + non-generic `-> Type` builder is now flagged `is_comptime` (`decl.zig`) so its dead body + permits the welded calls (the comptime-only gate). + - **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 (same module reached via two + import edges) re-mints identically instead of erroring (`nominalIdent` reads identity from + any nominal kind). `kind` codes match `type_kind`: 1 struct · 2 enum (actual `.@"enum"`) · + 3 tagged_union · 4 tuple. + - **Two bugs fixed en route** (issue 0142): (a) a fully payloadless comptime-minted enum + was minted as an all-void `tagged_union` → `verifySizes` panic; now mints a real + `.@"enum"` (both `register_type` kind 2 AND the metatype `defineEnum`). (b) bare + `EnumType.variant` qualified construction of a payloadless variant wasn't supported (failed + for hand-written enums too) — added in `lowerFieldAccess` (`isPayloadlessVariant`). + - 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). Parity 697/697 (gate ON and OFF); unit tests added. +- **Next (P3.4):** re-express `declare`/`define`/`type_info` as sx over the read+write + compiler-API and DELETE the bespoke interp arms — needs the VM hardened against malformed + lowering-time IR first (the metatype runs at lowering time), so either harden + wire the VM + there, or migrate the metatype onto the legacy compiler-API calls first. Decide when reached. + Phase 2 (bytecode) is the orthogonal speed work. ### Phase 3 — Compiler-API on flat memory (resume the stream — no weld) With native-byte comptime values, re-home the compiler-API: diff --git a/examples/0187-types-enum-qualified-variant.sx b/examples/0187-types-enum-qualified-variant.sx new file mode 100644 index 00000000..f4180764 --- /dev/null +++ b/examples/0187-types-enum-qualified-variant.sx @@ -0,0 +1,35 @@ +// Qualified enum-variant construction: `EnumType.variant` for a payloadless +// variant, the explicit twin of the leading-dot `.variant` form. Works for a +// plain enum and for a payloadless variant of a tagged union; a payload-carrying +// variant keeps its call form (`Shape.circle(2.0)`), unaffected. + +#import "modules/std.sx"; + +Color :: enum { red; green; blue; } + +Shape :: enum { + circle: f32; // payload-carrying + dot; // payloadless +} + +main :: () { + // Plain enum, qualified construction. + c := Color.green; + if c == { + case .red: { print("red\n"); } + case .green: { print("green\n"); } + case .blue: { print("blue\n"); } + } + + // Tagged union: payloadless variant qualified, payload variant via call. + d := Shape.dot; + if d == { + case .circle: (r) { print("circle {}\n", r); } + case .dot: { print("dot\n"); } + } + s := Shape.circle(2.0); + if s == { + case .circle: (r) { print("circle {}\n", r); } + case .dot: { print("dot\n"); } + } +} diff --git a/examples/0631-comptime-compiler-register-graph.sx b/examples/0631-comptime-compiler-register-graph.sx new file mode 100644 index 00000000..3009466a --- /dev/null +++ b/examples/0631-comptime-compiler-register-graph.sx @@ -0,0 +1,88 @@ +// Comptime compiler API — the WRITE side: one kind-branching `register_type` +// minting an actual enum AND a graph of mutually-recursive types (Phase 3). +// +// `declare_type` / `pointer_to` / `register_type` are bound to the `compiler` +// library. They MINT into the type table, so they run at LOWERING time (lazily, +// on demand) — when a `-> Type` builder's result is first referenced — where the +// compiler still resolves references to the new types. (`#run` is too late: it +// runs at emit time, after the type table is frozen.) They take/return real +// `Type` values (like the metatype's declare/define), and `register_type` +// branches on the `kind` arg IN THE COMPILER — the codes match the read-side +// `type_kind`: 1 struct · 2 enum · 3 tagged_union · 4 tuple. +// +// Suit :: enum { hearts; spades; diamonds; } (actual, payloadless) +// GraphA :: enum { self_ref: *A; to_b: B; tag: u32; } (payloads → tagged_union) +// GraphB :: enum { back_a: *A; self_b: *B; num: u32; } +// +// Forward `declare_type` handles + `pointer_to` make the A<->B cycle expressible +// before either body is filled. + +#import "modules/std.sx"; + +compiler :: #library "compiler"; + +Member :: struct { name: string; ty: Type; } + +StringId :: u32; +TypeId :: u32; + +intern :: (s: string) -> StringId abi(.zig) extern compiler; +find_type :: (name: StringId) -> TypeId abi(.zig) extern compiler; +type_kind :: (t: TypeId) -> i64 abi(.zig) extern compiler; +declare_type :: (name: string) -> Type abi(.zig) extern compiler; +pointer_to :: (t: Type) -> Type abi(.zig) extern compiler; +register_type :: (handle: Type, kind: i64, members: []Member) -> Type abi(.zig) extern compiler; + +KIND_ENUM :: 2; // an ACTUAL payloadless enum +KIND_TAGGED_UNION :: 3; // a payload-carrying enum + +// An actual enum: variants are names, no payloads (ty = void). +make_suit :: () -> Type { + return register_type(declare_type("Suit"), KIND_ENUM, .[ + Member.{ name = "hearts", ty = void }, + Member.{ name = "spades", ty = void }, + Member.{ name = "diamonds", ty = void }, + ]); +} +Suit :: make_suit(); + +// The mutually-recursive A <-> B graph (payload variants → tagged_union). +build_graph :: () -> Type { + hA := declare_type("GraphA"); + hB := declare_type("GraphB"); + register_type(hA, KIND_TAGGED_UNION, .[ + Member.{ name = "self_ref", ty = pointer_to(hA) }, // *A — self-reference + Member.{ name = "to_b", ty = hB }, // B by value (forward) + Member.{ name = "tag", ty = u32 }, // a plain payload + ]); + register_type(hB, KIND_TAGGED_UNION, .[ + Member.{ name = "back_a", ty = pointer_to(hA) }, // *A — back-reference + Member.{ name = "self_b", ty = pointer_to(hB) }, // *B — self-reference + Member.{ name = "num", ty = u32 }, + ]); + return hA; +} +GraphA :: build_graph(); + +// Reflect the minted types (read side, at #run) to confirm their kinds. +suit_kind :: #run type_kind(find_type(intern("Suit"))); // 2 = actual enum +grapha_kind :: #run type_kind(find_type(intern("GraphA"))); // 3 = tagged_union + +main :: () -> i32 { + // Suit is a real, usable enum. + s := Suit.spades; + if s == { + case .hearts: { print("hearts\n"); } + case .spades: { print("spades\n"); } + case .diamonds: { print("diamonds\n"); } + } + // GraphA is a real, usable tagged union. + a := GraphA.tag(7); + if a == { + case .tag: (n) { print("tag={}\n", n); } + case .self_ref: (p) { print("self_ref\n"); } + case .to_b: (b) { print("to_b\n"); } + } + print("Suit kind={}, GraphA kind={}\n", suit_kind, grapha_kind); + return 0; +} diff --git a/examples/0632-comptime-metatype-make-enum-payloadless.sx b/examples/0632-comptime-metatype-make-enum-payloadless.sx new file mode 100644 index 00000000..26b4bfe8 --- /dev/null +++ b/examples/0632-comptime-metatype-make-enum-payloadless.sx @@ -0,0 +1,31 @@ +// Regression (issue 0142): a comptime-minted FULLY payloadless enum (every +// variant tagless) must mint as a real `.@"enum"`, not an all-void tagged_union +// — the latter has an IR/LLVM size mismatch that tripped `verifySizes` at +// codegen. `make_enum` (declare/define) with an all-void variant list now +// produces an ordinary enum, usable like a hand-written one. + +#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 }, + EnumVariant.{ name = "diamonds", payload = void }, + ]); +} +Suit :: make_suit(); + +show :: (s: Suit) { + if s == { + case .hearts: { print("hearts\n"); } + case .spades: { print("spades\n"); } + case .diamonds: { print("diamonds\n"); } + } +} + +main :: () { + show(.spades); // leading-dot, typed context + x := Suit.diamonds; // qualified construction + show(x); +} diff --git a/examples/0633-comptime-compiler-namespaced-type.sx b/examples/0633-comptime-compiler-namespaced-type.sx new file mode 100644 index 00000000..e29ddc6e --- /dev/null +++ b/examples/0633-comptime-compiler-namespaced-type.sx @@ -0,0 +1,15 @@ +// A comptime-minted type (built via the compiler API in `shapes.sx`) reached +// through a NAMESPACED import: `s.Suit`, with qualified variant construction +// `s.Suit.spades`. The bare-import form is example 0634; in-file minting + +// reflection is 0631. + +#import "modules/std.sx"; +s :: #import "0633-comptime-compiler-namespaced-type/shapes.sx"; + +main :: () { + x := s.Suit.spades; + if x == { + case .hearts: { print("hearts\n"); } + case .spades: { print("spades\n"); } + } +} diff --git a/examples/0633-comptime-compiler-namespaced-type/indirect.sx b/examples/0633-comptime-compiler-namespaced-type/indirect.sx new file mode 100644 index 00000000..b32ecc4d --- /dev/null +++ b/examples/0633-comptime-compiler-namespaced-type/indirect.sx @@ -0,0 +1,8 @@ +// Reaches shapes.sx via a NAMESPACED import; re-exports a helper over its Suit. +#import "modules/std.sx"; +s :: #import "shapes.sx"; + +name_of :: (x: s.Suit) -> string { + if x == { case .hearts: { return "hearts"; } case .spades: { return "spades"; } } + return "?"; +} diff --git a/examples/0633-comptime-compiler-namespaced-type/shapes.sx b/examples/0633-comptime-compiler-namespaced-type/shapes.sx new file mode 100644 index 00000000..75a47d08 --- /dev/null +++ b/examples/0633-comptime-compiler-namespaced-type/shapes.sx @@ -0,0 +1,17 @@ +// A module that MINTS a comptime enum via the compiler API and exports it. +#import "modules/std.sx"; + +compiler :: #library "compiler"; + +Member :: struct { name: string; ty: Type; } +declare_type :: (name: string) -> Type abi(.zig) extern compiler; +register_type :: (handle: Type, kind: i64, members: []Member) -> Type abi(.zig) extern compiler; + +build_suit :: () -> Type { + return register_type(declare_type("Suit"), 2, .[ // kind 2 = actual enum + Member.{ name = "hearts", ty = void }, + Member.{ name = "spades", ty = void }, + ]); +} + +Suit :: build_suit(); diff --git a/examples/0634-comptime-compiler-bare-import-type.sx b/examples/0634-comptime-compiler-bare-import-type.sx new file mode 100644 index 00000000..035745a6 --- /dev/null +++ b/examples/0634-comptime-compiler-bare-import-type.sx @@ -0,0 +1,14 @@ +// A comptime-minted type (built via the compiler API in 0633's `shapes.sx`) +// reached through a BARE import: the minted `Suit` is in scope flat, with +// qualified variant construction `Suit.spades`. Namespaced form is 0633. + +#import "modules/std.sx"; +#import "0633-comptime-compiler-namespaced-type/shapes.sx"; + +main :: () { + x := Suit.spades; + if x == { + case .hearts: { print("hearts\n"); } + case .spades: { print("spades\n"); } + } +} diff --git a/examples/0635-comptime-compiler-multi-edge-import.sx b/examples/0635-comptime-compiler-multi-edge-import.sx new file mode 100644 index 00000000..df33a528 --- /dev/null +++ b/examples/0635-comptime-compiler-multi-edge-import.sx @@ -0,0 +1,17 @@ +// A comptime-minting module (0633's `shapes.sx`) reached via TWO import edges in +// one build: directly (bare) here, and indirectly through `indirect.sx` (which +// imports it namespaced as `s`). The minted `Suit` must be ONE type across both +// edges — `name_of` (typed `s.Suit` in indirect.sx) accepts the bare `Suit` +// constructed here. Exercises that re-evaluating the type-fn across import paths +// is idempotent (same TypeId), not a re-mint conflict. + +#import "modules/std.sx"; +#import "0633-comptime-compiler-namespaced-type/shapes.sx"; // bare edge → `Suit` +#import "0633-comptime-compiler-namespaced-type/indirect.sx"; // edge via `s :: shapes` + +main :: () { + a := Suit.spades; // bare Suit + print("{}\n", name_of(a)); // passed where indirect expects s.Suit (same type) + b := Suit.hearts; + print("{}\n", name_of(b)); +} diff --git a/examples/expected/0187-types-enum-qualified-variant.exit b/examples/expected/0187-types-enum-qualified-variant.exit new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/examples/expected/0187-types-enum-qualified-variant.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0187-types-enum-qualified-variant.stderr b/examples/expected/0187-types-enum-qualified-variant.stderr new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/expected/0187-types-enum-qualified-variant.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0187-types-enum-qualified-variant.stdout b/examples/expected/0187-types-enum-qualified-variant.stdout new file mode 100644 index 00000000..5b3bda59 --- /dev/null +++ b/examples/expected/0187-types-enum-qualified-variant.stdout @@ -0,0 +1,3 @@ +green +dot +circle 2.000000 diff --git a/examples/expected/0631-comptime-compiler-register-graph.exit b/examples/expected/0631-comptime-compiler-register-graph.exit new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/examples/expected/0631-comptime-compiler-register-graph.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0631-comptime-compiler-register-graph.stderr b/examples/expected/0631-comptime-compiler-register-graph.stderr new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/expected/0631-comptime-compiler-register-graph.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0631-comptime-compiler-register-graph.stdout b/examples/expected/0631-comptime-compiler-register-graph.stdout new file mode 100644 index 00000000..f99b6d93 --- /dev/null +++ b/examples/expected/0631-comptime-compiler-register-graph.stdout @@ -0,0 +1,3 @@ +spades +tag=7 +Suit kind=2, GraphA kind=3 diff --git a/examples/expected/0632-comptime-metatype-make-enum-payloadless.exit b/examples/expected/0632-comptime-metatype-make-enum-payloadless.exit new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/examples/expected/0632-comptime-metatype-make-enum-payloadless.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0632-comptime-metatype-make-enum-payloadless.stderr b/examples/expected/0632-comptime-metatype-make-enum-payloadless.stderr new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/expected/0632-comptime-metatype-make-enum-payloadless.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0632-comptime-metatype-make-enum-payloadless.stdout b/examples/expected/0632-comptime-metatype-make-enum-payloadless.stdout new file mode 100644 index 00000000..7e30f600 --- /dev/null +++ b/examples/expected/0632-comptime-metatype-make-enum-payloadless.stdout @@ -0,0 +1,2 @@ +spades +diamonds diff --git a/examples/expected/0633-comptime-compiler-namespaced-type.exit b/examples/expected/0633-comptime-compiler-namespaced-type.exit new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/examples/expected/0633-comptime-compiler-namespaced-type.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0633-comptime-compiler-namespaced-type.stderr b/examples/expected/0633-comptime-compiler-namespaced-type.stderr new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/expected/0633-comptime-compiler-namespaced-type.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0633-comptime-compiler-namespaced-type.stdout b/examples/expected/0633-comptime-compiler-namespaced-type.stdout new file mode 100644 index 00000000..47e2e32e --- /dev/null +++ b/examples/expected/0633-comptime-compiler-namespaced-type.stdout @@ -0,0 +1 @@ +spades diff --git a/examples/expected/0634-comptime-compiler-bare-import-type.exit b/examples/expected/0634-comptime-compiler-bare-import-type.exit new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/examples/expected/0634-comptime-compiler-bare-import-type.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0634-comptime-compiler-bare-import-type.stderr b/examples/expected/0634-comptime-compiler-bare-import-type.stderr new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/expected/0634-comptime-compiler-bare-import-type.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0634-comptime-compiler-bare-import-type.stdout b/examples/expected/0634-comptime-compiler-bare-import-type.stdout new file mode 100644 index 00000000..47e2e32e --- /dev/null +++ b/examples/expected/0634-comptime-compiler-bare-import-type.stdout @@ -0,0 +1 @@ +spades diff --git a/examples/expected/0635-comptime-compiler-multi-edge-import.exit b/examples/expected/0635-comptime-compiler-multi-edge-import.exit new file mode 100644 index 00000000..573541ac --- /dev/null +++ b/examples/expected/0635-comptime-compiler-multi-edge-import.exit @@ -0,0 +1 @@ +0 diff --git a/examples/expected/0635-comptime-compiler-multi-edge-import.stderr b/examples/expected/0635-comptime-compiler-multi-edge-import.stderr new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/examples/expected/0635-comptime-compiler-multi-edge-import.stderr @@ -0,0 +1 @@ + diff --git a/examples/expected/0635-comptime-compiler-multi-edge-import.stdout b/examples/expected/0635-comptime-compiler-multi-edge-import.stdout new file mode 100644 index 00000000..105c78e7 --- /dev/null +++ b/examples/expected/0635-comptime-compiler-multi-edge-import.stdout @@ -0,0 +1,2 @@ +spades +hearts diff --git a/issues/0142-comptime-minted-all-void-enum-binds-any.md b/issues/0142-comptime-minted-all-void-enum-binds-any.md new file mode 100644 index 00000000..f6572e7f --- /dev/null +++ b/issues/0142-comptime-minted-all-void-enum-binds-any.md @@ -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 '' 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. diff --git a/src/ir/compiler_lib.test.zig b/src/ir/compiler_lib.test.zig index c0ea7e7c..37989c8a 100644 --- a/src/ir/compiler_lib.test.zig +++ b/src/ir/compiler_lib.test.zig @@ -7,9 +7,18 @@ const compiler_lib = @import("compiler_lib.zig"); // rejects unexported names (the boundary `weldedCompilerFn` + the interp's // dispatch consult). test "compiler_lib: findFn resolves exported functions, rejects others" { + // Seed readers. try std.testing.expect(compiler_lib.findFn("intern") != null); try std.testing.expect(compiler_lib.findFn("text_of") != null); try std.testing.expectEqualStrings("intern", compiler_lib.findFn("intern").?.sx_name); + // Phase 3 read-only reflection readers. + for ([_][]const u8{ "find_type", "type_field_count", "type_nominal_name", "type_field_name", "type_field_type", "type_kind", "type_field_value" }) |n| { + try std.testing.expect(compiler_lib.findFn(n) != null); + } + // Phase 3 write side. + for ([_][]const u8{ "declare_type", "pointer_to", "register_type" }) |n| { + try std.testing.expect(compiler_lib.findFn(n) != null); + } try std.testing.expect(compiler_lib.findFn("not_exported") == null); try std.testing.expect(compiler_lib.findFn("") == null); } diff --git a/src/ir/compiler_lib.zig b/src/ir/compiler_lib.zig index 0ca21321..233059ea 100644 --- a/src/ir/compiler_lib.zig +++ b/src/ir/compiler_lib.zig @@ -54,8 +54,20 @@ pub const bound_fns = [_]BoundFn{ .{ .sx_name = "type_field_type", .handler = handleTypeFieldType }, .{ .sx_name = "type_kind", .handler = handleTypeKind }, .{ .sx_name = "type_field_value", .handler = handleTypeFieldValue }, + // ── write side (lowering-time, mints into the type table) ──────────────── + .{ .sx_name = "declare_type", .handler = handleDeclareType }, + .{ .sx_name = "pointer_to", .handler = handlePointerTo }, + .{ .sx_name = "register_type", .handler = handleRegisterType }, }; +// Kind codes accepted by `register_type` — mirror `TypeTable.kindCode`. An +// enum-like type is minted as a `tagged_union` (the general payload-carrying +// form, as `define` does), so both 2 (`enum`) and 3 (`tagged_union`) are taken. +const kind_struct: i64 = 1; +const kind_enum: i64 = 2; +const kind_tagged_union: i64 = 3; +const kind_tuple: i64 = 4; + /// Look up a compiler function by its sx name. Returns null when the name is not /// on the export list. pub fn findFn(sx_name: []const u8) ?*const BoundFn { @@ -170,3 +182,128 @@ fn handleTypeFieldValue(interp: *Interpreter, args: []const Value) InterpError!V const v = interp.module.types.memberValue(tid, args[1].int) orelse return error.TypeError; return Value{ .int = v }; } + +// ── write side: declare_type / pointer_to / register_type ─────────────────── +// +// These MINT into the type table, so they only make sense at LOWERING time — +// where the compiler still resolves references to the new types and the `mint` +// target is open (`runComptimeTypeFunc`). They take/return real `Type` values +// (`.type_tag`), the comptime-native form, matching meta.sx's `StructField` / +// `declare` / `define`. This is the unified re-expression of the metatype: +// `declare_type` ≈ `declare`, `register_type` ≈ a single kind-branching `define`, +// and `pointer_to` builds `*T` references so a graph of types can refer to each +// other (forward handles + pointers) before their bodies are filled. + +/// `declare_type(name: string) -> Type` — mint a NEW empty forward nominal type +/// named `name` (or return the existing slot, so a self/sibling reference by name +/// resolves to the same one). Mirrors the `declare` builtin: the forward slot is +/// an empty `tagged_union` until `register_type` fills it. +fn handleDeclareType(interp: *Interpreter, args: []const Value) InterpError!Value { + if (args.len != 1 or args[0] != .string) return error.TypeError; + const tbl = mintTable(interp); + const name_id = tbl.internString(args[0].string); + if (tbl.findByName(name_id)) |existing| return Value{ .type_tag = existing }; + const info: types.TypeInfo = .{ .tagged_union = .{ .name = name_id, .fields = &.{}, .tag_type = .i64 } }; + return Value{ .type_tag = tbl.internNominal(info, 0) }; +} + +/// `pointer_to(t: Type) -> Type` — intern `*t`. Lets a member reference a type by +/// pointer (e.g. a recursive `*A`) from a `Type` handle. +fn handlePointerTo(interp: *Interpreter, args: []const Value) InterpError!Value { + if (args.len != 1 or args[0] != .type_tag) return error.TypeError; + const tbl = mintTable(interp); + return Value{ .type_tag = tbl.intern(.{ .pointer = .{ .pointee = args[0].type_tag } }) }; +} + +/// `register_type(handle: Type, kind: i64, members: []Member) -> Type` — fill a +/// `declare_type`'d forward slot, branching on `kind` IN THE COMPILER (subsuming +/// `define`'s per-kind dispatch). `Member` is `{ name: string, ty: Type }`: +/// struct → fields `{ name, ty }` (dup names rejected) +/// enum/t-union → variants `{ name, payload = ty }` (minted as a tagged_union) +/// tuple → positional element types (names ignored) +/// Returns the (now completed) handle. Every malformed input is a loud error. +fn handleRegisterType(interp: *Interpreter, args: []const Value) InterpError!Value { + if (args.len != 3 or args[0] != .type_tag or args[1] != .int) return error.TypeError; + const handle = args[0].type_tag; + const kind = args[1].int; + const elems = interp_mod.decodeVariantElements(args[2]) orelse return error.TypeError; + if (elems.len == 0) return error.TypeError; // a type with no members is never valid + const tbl = mintTable(interp); + // The slot's nominal identity. Accept the forward `tagged_union` from + // `declare_type` AND an already-completed nominal of the same name — so + // re-evaluating the same type-fn (e.g. a minting module reached via two + // import edges) RE-FILLS the slot idempotently instead of erroring. A + // non-nominal handle is rejected (not a `declare_type`'d slot). + const ident = nominalIdent(tbl.get(handle)) orelse return error.TypeError; + + if (kind == kind_tuple) { + var tys = std.ArrayList(types.TypeId).empty; + for (elems) |elem| { + const m = memberPair(elem) orelse return error.TypeError; + tys.append(interp.alloc, m.ty) catch return error.CannotEvalComptime; + } + tbl.replaceKeyedInfo(handle, .{ .tuple = .{ .fields = tys.items, .names = null } }); + return Value{ .type_tag = handle }; + } + + if (kind == kind_enum) { + // An ACTUAL (payloadless) enum: members are variant NAMES. A non-void + // payload means the caller wants a payload-carrying variant — that's a + // tagged_union (kind 3), so reject it loudly rather than dropping it. + var variants = std.ArrayList(StringId).empty; + for (elems) |elem| { + const m = memberPair(elem) orelse return error.TypeError; + if (m.ty != .void) return error.TypeError; // payload variant → use kind 3 (tagged_union) + const name_id = tbl.internString(m.name); + for (variants.items) |existing| if (existing == name_id) return error.TypeError; // dup variant + variants.append(interp.alloc, name_id) catch return error.CannotEvalComptime; + } + tbl.replaceKeyedInfo(handle, .{ .@"enum" = .{ .name = ident.name, .variants = variants.items, .nominal_id = ident.nominal_id } }); + return Value{ .type_tag = handle }; + } + + // struct / tagged_union collect `{ name, ty }` fields. + var fields = std.ArrayList(types.TypeInfo.StructInfo.Field).empty; + for (elems) |elem| { + const m = memberPair(elem) orelse return error.TypeError; + const name_id = tbl.internString(m.name); + for (fields.items) |existing| if (existing.name == name_id) return error.TypeError; // dup member name + fields.append(interp.alloc, .{ .name = name_id, .ty = m.ty }) catch return error.CannotEvalComptime; + } + const full: types.TypeInfo = switch (kind) { + kind_struct => .{ .@"struct" = .{ .name = ident.name, .fields = fields.items, .nominal_id = ident.nominal_id } }, + kind_tagged_union => .{ .tagged_union = .{ .name = ident.name, .fields = fields.items, .tag_type = .i64, .nominal_id = ident.nominal_id } }, + else => return error.TypeError, // unknown kind code + }; + tbl.replaceKeyedInfo(handle, full); + return Value{ .type_tag = handle }; +} + +/// The nominal identity (`name` + stable `nominal_id`) of a declare_type'd slot — +/// from the forward `tagged_union` OR an already-completed nominal (so a re-fill +/// preserves identity). A `tuple` is structural (no nominal name); null for a +/// non-nominal handle (not a `declare_type` result). +fn nominalIdent(info: types.TypeInfo) ?struct { name: StringId, nominal_id: u32 } { + return switch (info) { + .tagged_union => |u| .{ .name = u.name, .nominal_id = u.nominal_id }, + .@"enum" => |e| .{ .name = e.name, .nominal_id = e.nominal_id }, + .@"struct" => |s| .{ .name = s.name, .nominal_id = s.nominal_id }, + .tuple => .{ .name = StringId.empty, .nominal_id = 0 }, // structural; name vestigial + else => null, + }; +} + +/// Decode one `Member` value — a `{ name: string, ty: Type }` aggregate. +fn memberPair(elem: Value) ?struct { name: []const u8, ty: types.TypeId } { + const f = switch (elem) { + .aggregate => |a| a, + else => return null, + }; + if (f.len != 2) return null; + const name = switch (f[0]) { + .string => |s| s, + else => return null, + }; + const ty = f[1].asTypeId() orelse return null; + return .{ .name = name, .ty = ty }; +} diff --git a/src/ir/interp.zig b/src/ir/interp.zig index a07bd4c5..ad65c7e2 100644 --- a/src/ir/interp.zig +++ b/src/ir/interp.zig @@ -2197,10 +2197,37 @@ pub const Interpreter = struct { } // Complete the declared slot IN PLACE: it already has its name + nominal - // id (from `declare`); fill the body. Name/id unchanged → the intern key - // is stable, so `updatePreservingKey`. + // id (from `declare`); fill the body. const cur = tbl.get(handle); if (cur != .tagged_union) return bailDetail("comptime define(): handle is not a declare()'d enum slot"); + + // A FULLY payloadless variant set (every payload `void`) is an actual + // enum — mint a `.@"enum"`, exactly like a hand-written `enum { a; b; }`. + // Minting it as an all-void `tagged_union` instead gives a type whose IR + // size disagrees with its LLVM size (a tag, but no payload storage), which + // trips `verifySizes` at codegen. A kind change re-keys the slot, so + // `replaceKeyedInfo` (not `updatePreservingKey`, which asserts the kind is + // stable — true only for the tagged_union path below). + var all_void = true; + for (fields.items) |f| { + if (f.ty != .void) { + all_void = false; + break; + } + } + if (all_void) { + var variants = std.ArrayList(types.StringId).empty; + for (fields.items) |f| variants.append(self.alloc, f.name) catch return error.CannotEvalComptime; + const en: types.TypeInfo = .{ .@"enum" = .{ + .name = cur.tagged_union.name, + .variants = variants.items, + .nominal_id = cur.tagged_union.nominal_id, + } }; + tbl.replaceKeyedInfo(handle, en); + return .{ .value = .{ .type_tag = handle } }; + } + + // Payload-carrying enum → tagged_union. Name/id unchanged → stable key. const full: types.TypeInfo = .{ .tagged_union = .{ .name = cur.tagged_union.name, .fields = fields.items, @@ -2305,7 +2332,7 @@ pub const Interpreter = struct { /// A `[]EnumVariant` slice evaluates to a `{ data, len }` aggregate (`len` an /// int); a `[N]EnumVariant` array literal evaluates to the element aggregate /// directly. Returns null for any other shape (the caller bails loudly). -fn decodeVariantElements(result: Value) ?[]const Value { +pub fn decodeVariantElements(result: Value) ?[]const Value { const fields = switch (result) { .aggregate => |f| f, else => return null, diff --git a/src/ir/lower.zig b/src/ir/lower.zig index 9e16f191..3f9155c0 100644 --- a/src/ir/lower.zig +++ b/src/ir/lower.zig @@ -1931,6 +1931,7 @@ pub const Lowering = struct { pub const findTaggedVariant = lower_expr.findTaggedVariant; pub const emitBadVariant = lower_expr.emitBadVariant; pub const emitBadEnumVariant = lower_expr.emitBadEnumVariant; + pub const isPayloadlessVariant = lower_expr.isPayloadlessVariant; pub const dedupeExternSymbol = lower_decl.dedupeExternSymbol; pub const resolveVariantValue = lower_expr.resolveVariantValue; pub const resolveVariantIndex = lower_expr.resolveVariantIndex; diff --git a/src/ir/lower/decl.zig b/src/ir/lower/decl.zig index 5f64f765..abef5091 100644 --- a/src/ir/lower/decl.zig +++ b/src/ir/lower/decl.zig @@ -2298,6 +2298,14 @@ pub fn declareFunction(self: *Lowering, fd: *const ast.FnDecl, name: []const u8) func.is_variadic = is_variadic; func.has_implicit_ctx = wants_ctx; if (weldedCompilerFn(self, fd, name)) func.compiler_welded = true; + // A non-generic `-> Type` builder is a comptime type constructor — only ever + // evaluated at lowering time (`runComptimeTypeFunc`) to mint a type, never + // called at runtime. Flag it `is_comptime` so its emitted body is dead: the + // comptime-only `compiler`-library gate then permits welded calls inside it + // (`register_type`/`declare_type`/`pointer_to`), exactly as in a #run/`::` + // wrapper. Without this, a builder that calls a welded fn would be rejected + // as "comptime-only fn called at runtime" even though it never runs at runtime. + if (fnReturnsTypeValue(fd)) func.is_comptime = true; self.fn_decl_fids.put(fd, fid) catch {}; } diff --git a/src/ir/lower/expr.zig b/src/ir/lower/expr.zig index 7b2aaa93..c63f0412 100644 --- a/src/ir/lower/expr.zig +++ b/src/ir/lower/expr.zig @@ -490,6 +490,33 @@ pub fn lowerFieldAccess(self: *Lowering, fa: *const ast.FieldAccess, span: ast.S } } + // Bare `Enum.variant` — a qualified enum literal. When the object is a type + // NAME resolving to an enum / tagged-union (not shadowed by a value binding / + // global value) and `field` is a PAYLOADLESS variant, construct it like the + // leading-dot `.variant` in a typed context. Mirrors the `alias.Enum.variant` + // namespace path above. Restricted to payloadless variants so a payload- + // carrying `Ev.a(5)` still flows through the call path (which supplies the + // payload) rather than being hijacked into a zero-arg `.a` here. + if (fa.object.data == .identifier) { + const oname = fa.object.data.identifier.name; + const shadowed = if (self.scope) |s| s.lookup(oname) != null else false; + if (!shadowed and !self.program_index.global_names.contains(oname)) { + if (self.module.types.findByName(self.module.types.internString(oname))) |ty| { + if (!ty.isBuiltin() and self.isPayloadlessVariant(ty, fa.field)) { + const synth = self.alloc.create(Node) catch null; + if (synth) |n| { + n.* = .{ .span = span, .data = .{ .enum_literal = .{ .name = fa.field } } }; + const saved_tt = self.target_type; + self.target_type = ty; + const ref = self.lowerExpr(n); + self.target_type = saved_tt; + return ref; + } + } + } + } + } + // Pack-arity intercept: `.len` in a pack-fn mono's // body resolves to the comptime-known N. The mono doesn't // materialise the `[]Any` slice that the inline path used, so @@ -965,6 +992,25 @@ pub fn lowerEnumLiteral(self: *Lowering, el: *const ast.EnumLiteral) Ref { return self.builder.enumInit(tag, Ref.none, target); } +/// Is `field` a PAYLOADLESS variant of enum/tagged-union `ty`? A plain `.@"enum"` +/// variant is always payloadless; a `tagged_union` variant is payloadless iff its +/// payload is `void`. Used by `lowerFieldAccess` to recognise a bare +/// `Enum.variant` qualified literal (payload-carrying variants stay on the call +/// path, which supplies the payload). False for any non-enum type / unknown field. +pub fn isPayloadlessVariant(self: *Lowering, ty: TypeId, field: []const u8) bool { + return switch (self.module.types.get(ty)) { + .@"enum" => |e| blk: { + for (e.variants) |v| if (std.mem.eql(u8, self.module.types.getString(v), field)) break :blk true; + break :blk false; + }, + .tagged_union => |u| blk: { + for (u.fields) |f| if (std.mem.eql(u8, self.module.types.getString(f.name), field)) break :blk (f.ty == .void); + break :blk false; + }, + else => false, + }; +} + /// The enum twin of `emitBadVariant`: an unknown variant of a plain enum, /// with the legal variants listed. pub fn emitBadEnumVariant(