From 18af8eb8458cbed19d00b72177dfefdd424f742a Mon Sep 17 00:00:00 2001 From: agra Date: Wed, 17 Jun 2026 19:29:36 +0300 Subject: [PATCH] comptime-API: strip the byte-weld; pivot to a flat-memory comptime VM The byte-weld (sx structs whose layout was validated to mirror the compiler's Zig records) plus the serialization/marshaling bridge was the wrong direction: it bolted a parallel layout regime and hand-built byte-copies onto a comptime value model that fundamentally isn't bytes. Strip the struct-weld machinery: - compiler_lib.zig loses the type registry (weldStruct / bound_types / BoundType / FieldLayout / findType / SxField / LayoutMismatch / validateStructLayout); it is now just the intern/text_of function host-call bridge (kept as the Phase-3 compiler-call seed). - nominal.zig loses validateWeldedStruct / weldedFieldOrderStr + the sd.abi == .zig validation call. - Remove the struct-weld unit tests and examples 0625/0627 (welded structs) + 1183/1186 (weld-layout diagnostics). - The #library / abi / extern syntax stays. Record the new direction: a bytecode VM over flat, byte-addressable memory so comptime values are native bytes (no weld/validation/marshal), target-aware (preserves cross-compilation) and sandboxed. See current/PLAN-COMPILER-VM.md (Phase 0 strip -> Phase 1 flat-memory value model -> Phase 2 bytecode -> Phase 3 compiler-API on flat memory). design/comptime-compiler-api.md gets a SUPERSEDED banner. Also drop the "~500 lines / split the step" rule from CLAUDE.md. --- CLAUDE.md | 16 +- current/CHECKPOINT-COMPILER-API.md | 161 ++++++++- current/PLAN-COMPILER-VM.md | 306 ++++++++++++++++++ design/comptime-compiler-api.md | 23 +- examples/0625-comptime-weld-struct-field.sx | 24 -- ...7-comptime-weld-struct-reflected-layout.sx | 30 -- ...183-diagnostics-weld-struct-field-count.sx | 17 - ...186-diagnostics-weld-struct-wrong-order.sx | 20 -- .../0625-comptime-weld-struct-field.exit | 1 - .../0625-comptime-weld-struct-field.stderr | 1 - .../0625-comptime-weld-struct-field.stdout | 1 - ...comptime-weld-struct-reflected-layout.exit | 1 - ...mptime-weld-struct-reflected-layout.stderr | 1 - ...mptime-weld-struct-reflected-layout.stdout | 1 - ...3-diagnostics-weld-struct-field-count.exit | 1 - ...diagnostics-weld-struct-field-count.stderr | 5 - ...diagnostics-weld-struct-field-count.stdout | 1 - ...6-diagnostics-weld-struct-wrong-order.exit | 1 - ...diagnostics-weld-struct-wrong-order.stderr | 5 - ...diagnostics-weld-struct-wrong-order.stdout | 1 - src/ir/compiler_lib.test.zig | 136 +------- src/ir/compiler_lib.zig | 170 ++-------- src/ir/lower/nominal.zig | 80 +---- 23 files changed, 505 insertions(+), 498 deletions(-) create mode 100644 current/PLAN-COMPILER-VM.md delete mode 100644 examples/0625-comptime-weld-struct-field.sx delete mode 100644 examples/0627-comptime-weld-struct-reflected-layout.sx delete mode 100644 examples/1183-diagnostics-weld-struct-field-count.sx delete mode 100644 examples/1186-diagnostics-weld-struct-wrong-order.sx delete mode 100644 examples/expected/0625-comptime-weld-struct-field.exit delete mode 100644 examples/expected/0625-comptime-weld-struct-field.stderr delete mode 100644 examples/expected/0625-comptime-weld-struct-field.stdout delete mode 100644 examples/expected/0627-comptime-weld-struct-reflected-layout.exit delete mode 100644 examples/expected/0627-comptime-weld-struct-reflected-layout.stderr delete mode 100644 examples/expected/0627-comptime-weld-struct-reflected-layout.stdout delete mode 100644 examples/expected/1183-diagnostics-weld-struct-field-count.exit delete mode 100644 examples/expected/1183-diagnostics-weld-struct-field-count.stderr delete mode 100644 examples/expected/1183-diagnostics-weld-struct-field-count.stdout delete mode 100644 examples/expected/1186-diagnostics-weld-struct-wrong-order.exit delete mode 100644 examples/expected/1186-diagnostics-weld-struct-wrong-order.stderr delete mode 100644 examples/expected/1186-diagnostics-weld-struct-wrong-order.stdout diff --git a/CLAUDE.md b/CLAUDE.md index 295ca830..a11e56b4 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -340,10 +340,10 @@ overhaul, mem.sx + protocol expansion), **LANG** (user-facing language features — diagnostics renderer, heterogeneous variadic packs), and **ERR** (error handling: separate-channel `!` errors, `try` / `catch` / `or` / `onfail`, return traces), and **COMPILER-API** (the comptime `compiler` -library — `#library "compiler"` + `abi(.zig) extern compiler`; welded compiler -types/functions that supersede the metatype `declare`/`define` `#builtin`s and -the `#compiler` attribute). They touch mostly disjoint files; any can be -advanced independently. +library that supersedes the metatype `declare`/`define` `#builtin`s and the +`#compiler` attribute — **pivoted 2026-06-17** off the byte-weld to a **flat-memory +bytecode comptime VM** as its foundation; see `current/PLAN-COMPILER-VM.md`). They +touch mostly disjoint files; any can be advanced independently. 1. Read all checkpoints to see where each stream is paused: - `current/CHECKPOINT.md` — IR progress tracker. @@ -352,14 +352,17 @@ advanced independently. - `current/CHECKPOINT-LANG.md` — LANG progress tracker. - `current/CHECKPOINT-ERR.md` — ERR progress tracker. - `current/CHECKPOINT-COMPILER-API.md` — COMPILER-API progress tracker - (has a `## ⏯ Resume` block; currently mid-Phase 2 on branch `reify`). + (has a `## ⏯ Resume` block; **pivoted to the flat-memory VM** — Phase 0 strip + pending, branch `reify`). 2. Read the plan that corresponds to the stream the user wants to advance: - `current/PLAN.md` — IR implementation plan. - `current/PLAN-FFI.md` — FFI ceremony reduction plan. - `~/.claude/plans/tidy-doodling-cray.md` — MEM (mem.sx) implementation plan. - `current/PLAN-LANG.md` — LANG implementation plan. - `current/PLAN-ERR.md` — ERR implementation plan. - - `design/comptime-compiler-api.md` — COMPILER-API design-of-record + build order. + - `current/PLAN-COMPILER-VM.md` — **COMPILER-API active plan** (flat-memory bytecode + comptime VM, then re-home the compiler-API on it). `design/comptime-compiler-api.md` + is the SUPERSEDED weld design, kept only for history + to scope the Phase 0 strip. 3. Read `specs.md` if you need to understand language behavior. 4. Pick up from the next incomplete step in the relevant `CHECKPOINT*.md`. If the user hasn't said which stream to work on, ask before picking. @@ -397,7 +400,6 @@ advanced independently. - **Never modify `src/codegen.zig` in Phases 0–1.** It is the safety net. - In Phase 3, only read specific sections of codegen.zig (grep for the relevant handler). - No step should require reading more than ~1,000 lines of existing code. If it does, split it. -- No step should produce more than ~500 lines of new code. If it does, split it. - If Claude gets confused mid-step, stop, update `current/CHECKPOINT.md` with partial progress, and tell the user to start a new session. ## Context management diff --git a/current/CHECKPOINT-COMPILER-API.md b/current/CHECKPOINT-COMPILER-API.md index 238b9339..58d451d3 100644 --- a/current/CHECKPOINT-COMPILER-API.md +++ b/current/CHECKPOINT-COMPILER-API.md @@ -7,21 +7,34 @@ Companion to the design-of-record with ONE welded mechanism. Branch: `reify` (off `master`). Update after every step. ## ⏯ Resume (fresh session) -Phase 1 done; Phase 2 **welded structs are working** via a much simpler design than -the original byte-layout-override "GEP engine" (that plan — `computeWeldPlan`, -offset-ordered LLVM structs, byte-blobs — was explored and DROPPED). The locked -design: a welded `Name :: struct abi(.zig) extern compiler { … }` is a bodied -header declaring fields in the compiler type's MEMORY order; the compiler reflects -the bound Zig type (`@typeInfo` names + `@offsetOf` offsets + `@sizeOf`, nothing -maintained by hand) and VALIDATES the header matches, with loud diagnostics. On -pass it's an ordinary byte-identical struct — so `@ptrCast` to the compiler's own -type + deref just works; no index tables, no reorder, no special emit. -**Next:** Phase 2 continues — re-express `type_info`/`define` (struct) as sx over -welded `register_struct`/`find_type` (host-call bridge, Phase 2.5/2.6); see -**## Next step**. Read order: this file → `src/ir/compiler_lib.zig` (registry + -reflection) → `src/ir/lower/nominal.zig` `validateWeldedStruct`. Build/verify: -`zig build && zig build test`. +> **⚠ DIRECTION CHANGED (2026-06-17). The active plan is now +> [`PLAN-COMPILER-VM.md`](PLAN-COMPILER-VM.md), NOT the weld.** +> The **byte-weld + serialization/marshaling** approach is the wrong direction and is +> being **stripped**. New foundation: a **bytecode VM over flat, byte-addressable +> memory** so comptime values are native bytes; then the compiler-API rides on it with +> direct memory access (no weld, no validation, no marshaling). Everything below this +> banner describes the now-superseded weld state (committed on `reify` through +> `40d075c`) and is kept only to scope the Phase 0 strip. Read +> `PLAN-COMPILER-VM.md` first. +> +> **Why the pivot:** the comptime evaluator (`src/ir/interp.zig`) represents values as +> tagged `Value` unions, NOT native bytes — so a comptime `@ptrCast(*StructInfo)` +> reads the `Value` union's memory, not a struct. The weld tried to bridge that with +> hand-marshaling — exactly what the design set out to kill. Flat memory makes comptime +> values real bytes, so the bridge disappears. (JIT-native comptime was rejected: it +> breaks cross-compilation — host vs target layout — and loses the sandbox. A +> flat-memory VM keeps both while getting native bytes + speed.) +> +> **Next action:** execute Phase 0 of `PLAN-COMPILER-VM.md` (strip the weld machinery), +> then Phase 1 (flat-memory value model). Build/verify: `zig build && zig build test`. + +### (superseded) prior weld resume +Phase 1 done; Phase 2 welded structs were working via reflection + memory-order +validation (the `computeWeldPlan`/byte-blob "GEP engine" was explored + DROPPED even +earlier). A welded `Name :: struct abi(.zig) extern compiler { … }` declared fields in +the compiler type's MEMORY order; the compiler reflected the bound Zig type and +VALIDATED the header. **This whole mechanism is now being stripped — see the banner.** > ⚠ Snapshot workflow: use `-Dname=examples/NNNN-foo.sx[,…] -Dupdate-goldens` to > regenerate ONLY the named example(s) — a full `-Dupdate-goldens` re-runs all ~690 @@ -223,6 +236,12 @@ What landed: `zig build` + `zig build test` green (450/450 unit + 685 corpus). ## Current state + +> **Pivoted — see the banner + `PLAN-COMPILER-VM.md`.** The items below are the weld +> machinery as it stands on `reify` HEAD (`40d075c`); they are the **strip list** for +> Phase 0, not the forward direction. The `#library`/`abi`/`extern` *syntax* stays; the +> weld *semantics* (layout reflection/validation, marshaling dispatch) go. + - `compiler :: #library "compiler";` parses + is recognised as the comptime-only internal surface (never dlopen'd). - `abi(.zig) extern compiler` STRUCTS: layout-validated against the registry @@ -238,9 +257,18 @@ What landed: - **Deferred**: offset-override / LLVM byte-offset GEP for non-natural layouts (needed by `StructInfo`'s slice field, Phase 2). -## Next step — Phase 2: welded compiler FUNCTIONS over the real types +## Next step — execute `PLAN-COMPILER-VM.md` -Welded structs are byte-identical mirrors now, so the API surface can grow: +> The weld is being stripped. The next step is **Phase 0 of +> [`PLAN-COMPILER-VM.md`](PLAN-COMPILER-VM.md)** — remove the weld / serialize / +> marshal machinery (`compiler_lib.zig` reflection+validation, `nominal.zig` +> `validateWeldedStruct`, the `compiler_welded` dispatch, the weld examples/diagnostics +> 0625/0627/1183/1184/1185/1186), keeping the `#library`/`abi`/`extern` *syntax*. Then +> Phase 1 (flat-memory value model). The weld-era "next step" below is **obsolete** — +> kept only as a record of what the weld surface was about to do. + +### (obsolete) weld-era next step +Welded structs were byte-identical mirrors, so the API surface was set to grow: - **Bind `register_struct` / `find_type`** over the host-call bridge (`compiler_lib.zig` `bound_fns`, like `intern`/`text_of`). `register_struct` @@ -270,6 +298,107 @@ when reached (sentinels or accessor fns; see the design doc Risks). `List` growth; orthogonal, see `current/CHECKPOINT-METATYPE.md`.) ## Log +- **Phase 1.final start (VM plan) — wiring entry point `tryEval` (2026-06-17).** + `comptime_vm.tryEval(gpa, module, func_id) ?Value` runs a comptime function entirely on + the VM, returns a legacy `Value` (deep-copied to `gpa`) or `null` to fall back. + Unit-tested (pure 6*7 → 42; unbox_any → null). NOT yet routed into the host: needs + (1) panic→error hardening of `Machine` accessors so arbitrary funcs bail instead of + crashing, (2) implicit-ctx handling, (3) wiring at `emit_llvm` const-init behind + `SX_COMPTIME_FLAT`, (4) corpus parity run. See `PLAN-COMPILER-VM.md` Phase 1.final. + 688 corpus green. +- **Phase 1 sub-step 1.5b (VM plan) — Reg↔Value boundary bridge (2026-06-17).** + Builtin/compiler_call/extern handlers are coupled to the legacy `Interpreter`, so the + wiring will use WHOLE-FUNCTION fallback (VM runs pure functions; bail → legacy re-runs + the whole eval). Built the boundary bridge that enables it: `valueToReg` (Value arg → + Reg, aggregates into flat memory) + `regToValue` (VM result → Value, deep-copied). + Covers scalars/strings/structs; other shapes bail. Transitional. Round-trip + unit-tested. 688 corpus green. Next: the wiring (flag + route a comptime entry through + the VM with legacy fallback). +- **Phase 1 sub-step 1.5 (VM plan) — direct `call` + stack-lifetime change (2026-06-17).** + `Vm` gained `module` (callee resolution) + `depth`/`max_depth` guard. `call` marshals + arg Refs → Reg and recursively runs the callee; aggregates pass as Addrs over shared + flat memory. `Frame` no longer reclaims the machine on exit (else a returned aggregate + Addr dangles) — allocations live to `Vm.deinit`. Extern/builtin callees bail (1.5b). + Unit-tested: direct call (142), recursion sum(0..n) (15/55). 688 corpus green. Next: + 1.5b (call_builtin/compiler_call/extern), then hybrid wiring. +- **Phase 1 sub-step 4d (VM plan) — deref/addr_of; pivot decision (2026-06-17).** + Ported `addr_of` (pass-through) + `deref` (readField through pointer), unit-tested + (deref *i64 → 77, addr_of struct + field → 80). DECIDED to stop porting rarer ops + (tagged-union payload/any/closures) blind — their byte semantics are ambiguous without + real call sites — and pivot to CALLS (sub-step 1.5: `call`, then builtin/compiler) + + HYBRID WIRING (`-Dcomptime-flat` → VM with legacy fallback on `error.Unsupported`), so + the VM runs the real corpus and surfaces exactly what's needed. Key design point for + calls: aggregate-return lifetime → drop per-frame stack reclaim (let a comptime eval's + allocations live to `Vm.deinit`). 688 corpus green. See `PLAN-COMPILER-VM.md` decision + block. +- **Phase 1 sub-step 4c (VM plan) — optionals + payloadless enums (2026-06-17).** + `kindOf`: enum → word; `?T` → word (pointer-child, null==0) or `{T@0,i1@sizeof(T)}` + aggregate. Ported optional_wrap/unwrap/has_value/coalesce (`optChildIsPtr`/`optHas`; + const_null reads as none) + payloadless enum_init/enum_tag. Unit-tested (?i64 → 91, + ?*i64 null==0 → 99, enum tag → 11). 688 corpus green. Next: 4d (tagged unions, any, + closures). +- **Phase 1 sub-step 4b (VM plan) — slices + strings on flat memory (2026-06-17).** + `{ptr@0(pointer_size), len@8(i64)}` fat pointers (kindOf: string/slice → aggregate). + Ported `const_string` (text+NUL + fat pointer in flat memory), `length`/`data_ptr`, + `array_to_slice`, `subslice`, index-through-slice (`elemAddr` loads `.ptr`), and + `str_eq`/`str_ne` (memcmp). Unit-tested (str length+eq/ne, array→slice index sum=23, + subslice sum=43). 688 corpus green. Next: 4c (optionals/enums/any/closures). +- **Phase 1 sub-step 4a (VM plan) — tuples + arrays on flat memory (2026-06-17).** + `kindOf` widened (tuple/array → aggregate). Ported `tuple_init`/`tuple_get` + (`tupleFieldOffset`), `index_get`/`index_gep` (`elemAddr` = base + idx*elem_size over + array/pointer/many_pointer; slice/string bases bail), `length` on array values. + Unit-tested (mixed tuple, [3]i64 index sum=42, length=3). 688 corpus green. Next: + sub-step 4b (slices/strings, then optionals/enums/any/closures). +- **Phase 1 sub-step 3 (VM plan) — memory + structs on flat memory (2026-06-17).** + `Vm` gained optional `table: *const TypeTable` (target-aware layout). Ported + `alloca`/`load`/`store` + `struct_init`/`struct_get`/`struct_gep`, laying structs out + at the table's natural offsets. Value model: scalar/pointer → register word; + struct → lives in flat memory, its value IS its address (read→addr, write→memcpy), so + nested structs compose and `struct_gep` = base+offset. `kindOf` bails loudly on + not-yet-ported types. Addr-based values survive allocator realloc. Unit-tested + (struct round-trip, alloca+gep+store+load, nested struct). 688 corpus green. Next: + sub-step 4 (arrays/slices/strings/optionals/enums/tuples/any/closures, then calls). +- **Phase 1 sub-step 2 (VM plan) — flat-memory executor: scalars + control flow + (2026-06-17).** Added `Vm` to `comptime_vm.zig`: walks the same IR `Inst` over + flat-memory frames (register `Reg` = scalar bits or `Addr`), mirroring the legacy + interp's scalar semantics (i64 wrapping/signed, f64). Ported constants, arithmetic, + comparison, logical, conversions, terminators (`br`/`cond_br`/`ret`/`ret_void`) and + `block_param`; every other op bails loudly (`error.Unsupported` + op name in + `detail`). Unit-tested on hand-built tiny IR (`Fb` builder): int add, f64 arithmetic, + cond_br selection, a block-param loop, div-by-zero + unsupported-op bails. Corpus + untouched (688 green). Next: sub-step 3 (memory + aggregates on flat memory, where + target-aware layout enters). +- **Phase 1 sub-step 1 (VM plan) — flat-memory machine substrate (2026-06-17).** + New `src/ir/comptime_vm.zig`: `Machine` (linear byte memory + bump/stack allocator + with `mark`/`reset`, scalar `readWord`/`writeWord` 1/2/4/8 LE, `bytes` views, addr 0 + reserved as `null_addr`) + `Frame` (Ref-indexed register file, stack reclamation on + deinit). `Reg` = raw u64 (immediate scalar OR `Addr`). Unit-tested + (`comptime_vm.test.zig`), registered in the barrel; standalone — the legacy + interpreter stays live, corpus untouched (688 green). Next: sub-step 2 (executor + + scalar/branch ops over the same IR). Also removed the "~500 lines / split step" rule + from CLAUDE.md per request. +- **Phase 0 (VM plan) — struct-weld stripped; `intern`/`text_of` bridge kept + (2026-06-17).** Removed the struct-weld registry from `compiler_lib.zig` + (`weldStruct`/`bound_types`/`BoundType`/`FieldLayout`/`findType`/`SxField`/ + `LayoutMismatch`/`validateStructLayout`), `validateWeldedStruct`/`weldedFieldOrderStr` + + the `sd.abi == .zig` call from `nominal.zig`, the struct-weld unit tests, and + examples `0625`/`0627`/`1183`/`1186`. KEPT (decision) the `intern`/`text_of` function + host-call bridge — a clean scalar dispatch, not weld/serialize/marshal, the Phase-3 + compiler-call seed — so `weldedCompilerFn`, the `compiler_welded` dispatch, the + `emitCall` comptime-only gate, the `#library`/`abi`/`extern` syntax, and examples + `0626`/`1184`/`1185` remain. `zig build test` green (688 corpus, 0 failed). Next: + Phase 1 (flat-memory value model) per `PLAN-COMPILER-VM.md`. +- **DIRECTION CHANGE — pivot off the byte-weld to a flat-memory bytecode VM + (2026-06-17).** Decided the weld + serialization/marshaling bridge is the wrong + direction (it hand-marshals onto a comptime value model that isn't bytes — exactly + what the design set out to kill). New foundation: a bytecode VM over flat memory so + comptime values are native bytes; the compiler-API then rides on it via direct memory + (no weld/validation/marshaling). JIT-native comptime was weighed and rejected (breaks + cross-compilation, loses the sandbox). Wrote `current/PLAN-COMPILER-VM.md` (Phase 0 + strip → Phase 1 flat-memory value model → Phase 2 bytecode → Phase 3 compiler-API on + flat memory). Banner added to `design/comptime-compiler-api.md` (superseded). Reverted + the session's uncommitted `register_struct`/`find_type` marshaling experiment back to + `reify` HEAD (40d075c). No code stripped yet — Phase 0 is the next action. - **Phase 2 — welded structs by reflection + memory-order validation.** Dropped the byte-layout-override engine (computeWeldPlan / offset-ordered LLVM struct / byte-blob — all explored, all unnecessary). Instead: the sx header declares diff --git a/current/PLAN-COMPILER-VM.md b/current/PLAN-COMPILER-VM.md new file mode 100644 index 00000000..419caab4 --- /dev/null +++ b/current/PLAN-COMPILER-VM.md @@ -0,0 +1,306 @@ +# PLAN — Comptime Bytecode VM + flat memory (then re-home the compiler-API on it) + +> **Direction change (2026-06-17).** The comptime compiler-API stream pivots off the +> **byte-weld**. The weld (sx structs whose layout is validated to mirror the +> compiler's Zig types) + the **serialization / marshaling** bridge at the call +> boundary is the wrong direction — it bolts a parallel layout regime and hand-built +> byte-copies onto a comptime value model that fundamentally isn't bytes. We strip it +> and build the right foundation: a **bytecode VM over flat, byte-addressable +> memory**, where comptime values ARE native bytes (like runtime). On that base the +> compiler-API needs no weld, no validation, no marshaling — the compiler's own types +> are read/built directly as memory and its functions take/return real pointers. +> +> Supersedes the build order in `design/comptime-compiler-api.md` (kept for history). +> This is the active plan for the stream. Branch: `reify`. + +## Why + +`src/ir/interp.zig` is a tree-walking interpreter over the SSA IR that represents +every value as a tagged `Value` union (`int`, `float`, `aggregate: []const Value`, +`type_tag`, `heap_ptr`, …). Two consequences: + +1. **Slow.** Per-value boxing in a tagged union; per-op `switch` over `Inst`; an + aggregate is a heap `[]const Value`, walked element-by-element. +2. **Not native memory.** A struct value is `[]const Value` (tagged unions), NOT the + struct's bytes. So a comptime `@ptrCast(*StructInfo)` reads the `Value` union's + memory, not a `StructInfo` — which forced the whole weld+marshal detour. + +Make comptime values **native bytes in a flat memory** and both problems dissolve: +structs/arrays/slices are their bytes at natural layout (no weld), the compiler's own +records are directly addressable (no marshal), and a bytecode loop over flat memory is +fast. + +## End state + +- Comptime execution = a **bytecode VM** over a **flat linear memory** (real + host-allocated bytes; layout is **target-aware** via the type table's sizes). Values + are bytes at addresses plus a scalar register file. No tagged `Value` union. +- The comptime compiler-API: the compiler **exposes its real types + functions** to + comptime sx. sx reads/builds them as native memory and calls compiler functions by + pointer. No `abi(.zig)` weld, no `validateStructLayout`, no `register_struct` + field-by-field marshaling — gone. +- `declare`/`define`/`type_info` and `#compiler`/`BuildOptions` ride this one + mechanism; the bespoke interp arms are deleted. + +## Principles (hold at every step) + +- **Green at every step.** `zig build && zig build test` pass after each sub-step. The + existing tagged-`Value` interpreter stays the live evaluator until the VM reaches + corpus parity; swap behind a build flag, then delete the old path. +- **Target-aware, not host-baked.** Flat-memory layout uses the type table's target + sizes (`pointer_size`, `typeSizeBytes`/offsets), NEVER host `@sizeOf`. This is what + keeps cross-compilation correct (the JIT-comptime alternative could not). +- **Sandboxed.** Flat-memory accesses are bounds-checked; step/call-depth budgets + remain; an OOB / bad access traps to a build-gating diagnostic with a source span — + never a compiler-process crash. +- **No silent fallbacks** (per CLAUDE.md): an unhandled op / shape bails loudly with a + named reason, never a zero/default that looks like success. + +## Phases + +### Phase 0 — Strip the weld / serialize / marshal machinery +Delete the wrong-direction code so the VM builds on a clean base. Pure removal + +corpus rebaseline; suite green. + +- `src/ir/compiler_lib.zig`: the reflection (`weldStruct` / `bound_types` / + `FieldLayout` / `BoundType`), the layout validation (`validateStructLayout` / + `LayoutMismatch` / `SxField`). Decide the fate of the `bound_fns` host-call registry + (`intern`/`text_of` handlers) — it is likely subsumed by the VM's compiler-call path + in Phase 3, but `intern`/`text_of` may survive as the first such calls. +- `src/ir/lower/nominal.zig`: `validateWeldedStruct` + `weldedFieldOrderStr` + the + `sd.abi == .zig` validation call in `registerStructDecl`. +- `src/ir/interp.zig`: the `compiler_welded` dispatch branch. +- `src/backend/llvm/ops.zig`: the `emitCall` comptime-only gate keyed on + `compiler_welded` (re-derive the comptime-only guard from a non-weld signal if still + needed). +- Corpus: retire / convert the weld examples + diagnostics — `0625`, `0627` (welded + struct), `1183`, `1186` (weld-layout diagnostics), `1184`/`1185` (welded-fn). Keep + `0626` (`intern`/`text_of` round-trip) only if it survives the new call path. +- **Keep (re-evaluate in Phase 3), independent of the weld semantics:** the + `#library "compiler"` decl, the `abi(.x)` annotation + `extern ` syntax, and the + `callconv → abi` unification. These are surface syntax that may still serve the + compiler-API; only the *weld semantics* are stripped here. + +**Verification:** `zig build test` green with the weld machinery gone; the surviving +syntax still parses (parser unit tests). + +### Phase 1 — Flat-memory value model (still IR-walking, no bytecode yet) +Introduce flat memory and move comptime values onto it, **decoupled from bytecode** so +the value-model change is isolated. Each sub-step ports one op group and keeps the +corpus green; the OLD tagged path stays behind a build flag (`-Dcomptime-flat`) until +all groups land, then the shim is deleted. + +1. **Machine + scalars.** A flat memory region (host `[]u8`) with a stack (frames) + + bump-allocated heap, and a scalar register file. Port `int`/`float`/`bool`/`undef` + and arithmetic/compare/branch. Aggregates still go through a compat shim to the old + representation. +2. **Aggregates.** Structs/arrays/tuples laid out in flat memory at **target** layout; + port `struct_init` / `struct_get` / `array` / `index_gep` to read/write bytes at + computed offsets. +3. **Slices / strings.** `{ptr, len}` fat pointers in flat memory. +4. **Optionals / enums / tagged unions.** Tag + payload bytes. +5. **Pointers.** `alloca` / `store` / `load` / GEP unified onto flat addresses; retire + `slot_ptr` / `heap_ptr` / `byte_ptr` in favor of flat-memory addresses. +6. **Closures.** Fn id + captured env materialized in flat memory. +7. **Extern / host calls.** A struct arg is already bytes → pass its address; this + removes most of `marshalExternArg`. +8. **Reflection / minting.** `declare` / `define` / `type_info` read flat-memory + values; type-table mutation copies escaping data into compiler-owned memory at the + boundary (lifetime), as today. + +**Verification:** with `-Dcomptime-flat` the full corpus (currently 692) is byte-for- +byte identical to the tagged path; then make flat the default and delete the shim. + +### Phase 2 — Bytecode +Compile a comptime function's IR → a compact bytecode and execute the bytecode instead +of walking `Inst`. Pure encoding/speed; semantics identical to Phase 1. Land at least a +minimal register-bytecode loop (the stream's stated goal is a *bytecode* VM); a +fragment cache is optional follow-up. + +**Verification:** corpus identical to Phase 1; comptime throughput measurably improved +on a heavy-comptime micro-benchmark. + +### Phase 1.final — host wiring (the remaining integration) +The wiring ENTRY POINT exists: `comptime_vm.tryEval(gpa, module, func_id) ?Value` runs a +comptime function entirely on the VM and returns a legacy `Value`, or `null` to fall +back. Unit-tested (pure `6*7` → 42; unsupported → null). Remaining to actually route the +host through it: +1. **Panic→error hardening (prerequisite).** `Machine.readWord`/`writeWord`/`bytes` + currently `assert` (debug panic) on null/OOB. For arbitrary host functions to be + safe, make them return `error.OutOfBounds` so a malformed run BAILS (→ null → legacy) + instead of crashing the compiler. Ripples through `readField`/`writeField`/slice + helpers (add `try`). +2. **Implicit context.** Host comptime functions may have `has_implicit_ctx` (param 0 = + `*Context`); the legacy `run` materializes a default ctx. The VM `run` does not — so + either materialize it too, or only route `tryEval` at funcs without implicit ctx. +3. **Wire one site** behind a flag/env (`SX_COMPTIME_FLAT`, → `-Dcomptime-flat` later): + the const-init fold in `emit_llvm.zig` `emitGlobals` (`result = tryEval(...) orelse + interp.call(...)`). Default off → corpus unaffected. +4. **Parity + coverage.** Run the corpus with the flag ON; results must be byte-identical + to legacy. Measure how many comptime evals the VM already handles; the bail `detail`s + name what to port next (tagged-union payload / any / closures / builtins). +5. Grow coverage (port the deferred ops + `call_builtin`/`compiler_call` via the bridge) + until the VM is the default and the legacy path is deleted. + +### Phase 3 — Compiler-API on flat memory (resume the stream — no weld) +With native-byte comptime values, re-home the compiler-API: + +- **Expose the compiler's real types.** Register the actual `types.zig` records + (`StructInfo`, `EnumInfo`, `Field`, …) into the comptime type table under sx-visible + names, with their **real (host) layout** — the type IS the compiler's, so there is + nothing to validate or keep in sync. (This is the projection that *replaces* the + weld's reflection — owned by the compiler, not declared in sx.) +- **Expose the compiler's functions.** `register_struct`, `find_type`, `intern`, + `text_of`, and the reflection readers operate on flat-memory pointers / handles + directly (no marshaling — the bytes already ARE the record). +- **Re-express** `declare` / `define` / `type_info` as sx over these; delete the + bespoke interp arms (`defineStruct` / `defineEnum` / `defineTuple` / `reflectTypeInfo`); + migrate `examples/0622` (struct), `0619`/`0620`/`0623` (enum/tuple). +- **Migrate `BuildOptions`** off `#compiler` onto this mechanism; **delete `#compiler`**. + +**Verification:** the metatype + `#compiler` surfaces are gone, re-expressed as sx over +the exposed compiler-API; full corpus green. + +## Open questions (resolve as reached, record decisions here) + +- **Host-ABI vs target-ABI split.** The compiler runs on the host, so its OWN exposed + records are host-laid-out; user comptime types are target-laid-out. The flat-memory + model must carry both regimes (a per-type ABI tag on layout queries). Confirm the + boundary where a flat-memory pointer to a compiler record is handed to host Zig code + uses host layout. +- **Exposing compiler types to sx.** Mechanism for projecting `types.zig` records into + the comptime type table with real offsets (the non-weld replacement) — a registry the + compiler owns, keyed by sx-visible name → real Zig type's layout + a host-call ABI. +- **Bytecode shape.** IR-derived vs a fresh ISA; register vs stack; fragment caching. +- **Pointer escape / lifetime.** Flat-memory pointers stored into the persistent type + table must be copied into compiler-owned memory at the boundary (as today). +- **Old-path retirement.** Keep the tagged interpreter until Phase 1 parity, then + delete — confirm no non-comptime consumer depends on `Value`. + +## File map (current → touched) + +| Area | File | Phase | +|------|------|-------| +| Comptime evaluator | `src/ir/interp.zig` | 0 (strip weld dispatch), 1–2 (rebuild) | +| Weld registry | `src/ir/compiler_lib.zig` | 0 (strip), 3 (replace with type/fn exposure) | +| Weld validation | `src/ir/lower/nominal.zig` | 0 (strip `validateWeldedStruct`) | +| Comptime-only gate | `src/backend/llvm/ops.zig` | 0 (re-derive without weld signal) | +| Host-FFI marshalling | `src/ir/host_ffi.zig` | 1 (struct-by-pointer trims it) | +| Metatype arms | `src/ir/interp.zig` (`defineStruct`/…/`reflectTypeInfo`) | 3 (delete, re-express in sx) | +| `#compiler` / BuildOptions | `library/modules/build.sx`, `src/ir/compiler_hooks.zig` | 3 (migrate, delete `#compiler`) | +| Surface syntax | `src/parser.zig`, `src/ast.zig` (`abi`/`extern`/`#library`) | kept; revisited Phase 3 | + +## Status + +- **Phase 0 — DONE (2026-06-17).** The struct-weld machinery is stripped: + `compiler_lib.zig` lost the type registry (`weldStruct`/`bound_types`/`BoundType`/ + `FieldLayout`/`findType`/`SxField`/`LayoutMismatch`/`validateStructLayout`); + `nominal.zig` lost `validateWeldedStruct`/`weldedFieldOrderStr` + the + `sd.abi == .zig` call; the struct-weld unit tests + examples `0625`/`0627`/`1183`/ + `1186` are removed. **Decision (recorded):** the `intern`/`text_of` function + host-call bridge is KEPT — it is a clean scalar dispatch (string→handle), not + weld/serialize/marshal, and is the seed Phase 3 grows the compiler-call path from. + So the `compiler_welded` dispatch (`interp.callExtern` is unchanged at HEAD — the + pre-branch in `call()`), `weldedCompilerFn` (decl.zig), the `emitCall` comptime-only + gate (ops.zig), and examples `0626`/`1184`/`1185` stay. The `#library`/`abi`/`extern` + SYNTAX stays. `zig build test` green (688 corpus, 0 failed; unit tests pass). +- **Phase 1 — in progress.** + - **Sub-step 1 — DONE.** `src/ir/comptime_vm.zig`: the flat-memory `Machine` + (linear byte memory + bump/stack allocator with `mark`/`reset` reclamation + + scalar `readWord`/`writeWord` (1/2/4/8, little-endian) + `bytes` views; addr 0 + reserved as `null_addr`) and `Frame` (register file indexed by Ref + stack + reclamation on `deinit`). A register `Reg` is a raw u64 — immediate scalar OR + `Addr`. Standalone + unit-tested (`comptime_vm.test.zig`, in the barrel); does + NOT touch the live interpreter, so the corpus stays green (688). No op execution + yet. + - **Sub-step 2 — DONE.** The executor (`Vm` in `comptime_vm.zig`): walks the SAME + IR `Inst` over flat-memory frames, mirroring the legacy interp's scalar semantics + (i64 wrapping/signed + f64 register words, keyed off the result/operand `TypeId`). + Ported: constants (`const_int`/`float`/`bool`/`null`/`undef`), arithmetic + (`add`/`sub`/`mul`/`div`/`mod`/`neg`), comparison (`cmp_*`), logical + (`bool_and`/`or`/`not`), conversions (`widen`/`narrow`/`bitcast` passthrough, + `int_to_float`/`float_to_int`), terminators (`br`/`cond_br`/`ret`/`ret_void`) and + `block_param` (branch args passed as Refs — the same frame persists, SSA-safe). + Any other op bails loudly (`error.Unsupported` + `detail = @tagName(op)`). + Unit-tested on hand-built IR (`Fb` builder): integer add, f64 arithmetic, cond_br + branch selection, a block-param loop summing i..1, div-by-zero + unsupported-op + bails. Corpus untouched (688 green) — the executor is exercised by unit tests only, + not yet wired to real comptime eval. + - **Sub-step 3 — DONE.** Memory + structs on flat memory. `Vm` gained an optional + `table: *const TypeTable` (target-aware layout). Ported `alloca`/`load`/`store` + (over flat addresses, `Store.val_ty` drives width) and `struct_init`/`struct_get`/ + `struct_gep` (structs laid out at the table's natural offsets). The value model: a + `Kind.word` (scalar/pointer ≤8B) sits in a register; a `Kind.aggregate` (struct) + lives in flat memory and its "value" IS its address (read returns the address, + write memcpys), so nested structs compose and `struct_gep` is just base+offset (no + field-pointer dance). `kindOf` bails loudly on the not-yet-ported types + (slice/string/any/optional/enum/array/tuple/…). The Addr-based value model survives + allocator realloc (offsets are stable; slices are only materialized transiently). + Unit-tested: struct_init+get round-trip, alloca+gep+store+load, nested-struct + aggregate copy + nested read. Corpus untouched (688 green). + - **Sub-step 4a — DONE.** Tuples + arrays. `kindOf` widened (`tuple`/`array` → + aggregate). Ported `tuple_init`/`tuple_get` (positional, `tupleFieldOffset`), + `index_get`/`index_gep` (`elemAddr` = base + idx*elem_size over array/pointer/ + many_pointer bases; slice/string bases bail), and `length` on an array value + (static `ArrayInfo.length`). Unit-tested: mixed tuple round-trip, `[3]i64` + gep/store + index_get sum (42), array `length` (3). 688 corpus green. + - **Sub-step 4b — DONE.** Slices + strings as `{ptr@0 (pointer_size), len@8 (i64)}` + fat pointers (`kindOf`: string/slice → aggregate). Ported `const_string` (materializes + text+NUL in flat memory + a fat pointer), `length`/`data_ptr` (read len/ptr fields), + `array_to_slice`, `subslice`, indexing *through* a slice/string (`elemAddr` loads + `.ptr` first), and `str_eq`/`str_ne` (len+memcmp). Helpers `makeSlice`/`sliceLen`/ + `sliceData`. Unit-tested: string length + str_eq/ne, array→slice + slice index + + slice length (23), array subslice (43). 688 corpus green. + - **Sub-step 4c — DONE (optionals + payloadless enums).** `kindOf`: `enum` → word; + `?T` → word if pointer-child (null==0) else `{T@0, i1@sizeof(T)}` aggregate. Ported + `optional_wrap`/`unwrap`/`has_value`/`coalesce` (with `optChildIsPtr`/`optHas` + helpers; `const_null` → `null_addr` reads as none), `enum_init` (payloadless: tag is + the value), `enum_tag` (payloadless/word). Unit-tested: non-pointer `?i64` + wrap/unwrap/coalesce (91), pointer `?*i64` null==0 (99), payloadless enum tag (11). + 688 corpus green. + - **Sub-step 4d — partial (`addr_of`/`deref` DONE).** `addr_of` passes through (an + aggregate value already IS its address; a pointer is already an address — mirrors + the legacy); `deref` = `readField` through the pointer (`ins.ty` is the pointee). + Unit-tested (deref a `*i64` → 77; addr_of a struct value + field read → 80). + **Deferred to the wiring phase (intentionally, not ported blind):** tagged-union + payload (`enum_init` w/ payload, `enum_payload` — the legacy stores *untyped* Values + and `field_index` indexes payload sub-fields, not variants, so a byte model's + payload type is ambiguous without a real call site), `any` boxing, closures, and the + bitwise ops. These have subtleties best resolved against actual corpus cases — the + VM's loud `error.Unsupported` + `detail` will name exactly what each real eval needs. + + - **Sub-step 1.5 — direct `call` DONE.** `Vm` gained `module: *const Module` + (resolves a callee `FuncId`) + a `depth`/`max_depth` recursion guard. `call` + marshals arg Refs → Reg words and recursively `run`s the callee; aggregate args/ + results pass as their `Addr` over the SHARED flat memory (no copy). **Stack-lifetime + change:** `Frame` no longer reclaims the machine on exit (a returned aggregate's + Addr would dangle) — a comptime eval's allocations live to `Vm.deinit`; + `Machine.mark`/`reset` stay for explicit use. Extern/builtin callees (no blocks) + bail loudly (1.5b). Unit-tested: direct call (`add(20,22)+100` → 142) and recursion + (`sum(0..n)` → 15/55). 688 corpus green. + - **Sub-step 1.5b — `Reg`↔`Value` boundary bridge DONE.** The builtin/`compiler_call`/ + extern handlers are all coupled to the legacy `Interpreter` (e.g. `compiler_lib` + handlers take `*Interpreter`), so the VM can't call them directly — the wiring uses + WHOLE-FUNCTION fallback instead (VM runs pure functions; a bail re-runs the whole + eval in the legacy). That needs the boundary bridge: `valueToReg` (host `Value` arg → + VM `Reg`, materializing aggregates into flat memory) + `regToValue` (VM result → + `Value`, deep-copied out). Covers scalars + strings + structs (other aggregate shapes + bail loudly; added as wiring surfaces them). Transitional — deleted once the VM owns + comptime end-to-end. Unit-tested with round-trips. 688 corpus green. + - **Then the wiring step** (below) — now unblocked. + +### Decision (2026-06-17): pivot from blind op-porting to CALLS + hybrid wiring +The common leaf ops are ported (scalars, control flow, structs, tuples, arrays, slices, +strings, optionals, payloadless enums, deref/addr_of) and unit-tested. Continuing to +port the rarer ops (tagged-union payload, any, closures) in isolation risks subtle +bugs and has low signal. The higher-value path: +1. **Calls (sub-step 1.5)** — `call` (direct), then `call_builtin`/`compiler_call`. The + shared flat memory makes aggregate args/results pass naturally (they're Addrs). The + one design point: **aggregate-return lifetime** — a callee's stack-reclaim would + dangle a returned struct Addr, so for comptime (bounded) the VM should stop + reclaiming per-frame and let the whole eval's allocations live until `Vm.deinit` + (keep `Machine.mark/reset` for explicit use; drop it from `Frame.deinit`). +2. **Hybrid wiring** — `-Dcomptime-flat` routes a comptime eval through the VM, falling + back to the legacy interp on `error.Unsupported`. This makes the VM run the REAL + corpus, proving parity incrementally and surfacing exactly which ops each real eval + needs — far better signal than more isolated unit tests. diff --git a/design/comptime-compiler-api.md b/design/comptime-compiler-api.md index dd12423b..7daabb36 100644 --- a/design/comptime-compiler-api.md +++ b/design/comptime-compiler-api.md @@ -1,10 +1,23 @@ # Comptime Compiler API — `#library "compiler"` + `abi(.zig) extern` -> **Status: design-of-record (not yet an active stream).** Captures a unified -> mechanism for sx↔compiler binding that subsumes the metatype `declare`/`define` -> primitives AND the `#compiler` struct attribute, and exposes the compiler's own -> type-table API to comptime sx. Supersedes the bespoke `meta.sx` `TypeInfo` -> projection (the "weld it" decision). Design locked 2026-06-17. +> **⚠ SUPERSEDED (2026-06-17) — direction changed. See +> [`../current/PLAN-COMPILER-VM.md`](../current/PLAN-COMPILER-VM.md).** +> The **byte-weld** approach below (sx structs whose layout is validated to mirror +> the compiler's Zig types, plus serialization / marshaling at the call boundary) is +> the **wrong direction** and is being stripped. The comptime value model +> fundamentally isn't bytes, so the weld bolts a parallel layout regime + hand-built +> byte-copies onto it. The new foundation: a **bytecode VM over flat, byte-addressable +> memory**, where comptime values ARE native bytes — so the compiler-API needs no +> weld, no validation, no marshaling (the compiler exposes its real types/functions +> and sx reads/builds them directly as memory). The goal below (unify +> `declare`/`define`/`type_info` + `#compiler` onto one mechanism, delete the bespoke +> arms) is unchanged; only the *mechanism* is. This doc is retained for history and to +> scope the Phase 0 strip — do NOT implement the weld machinery from here. +> +> **Original status:** design-of-record. Captured a unified mechanism for +> sx↔compiler binding that subsumes the metatype `declare`/`define` primitives AND the +> `#compiler` struct attribute, and exposes the compiler's own type-table API to +> comptime sx. Design locked 2026-06-17; weld mechanism pivoted same day. ## Motivation diff --git a/examples/0625-comptime-weld-struct-field.sx b/examples/0625-comptime-weld-struct-field.sx deleted file mode 100644 index 672c8bbb..00000000 --- a/examples/0625-comptime-weld-struct-field.sx +++ /dev/null @@ -1,24 +0,0 @@ -// Comptime compiler API — a layout-welded struct binding. -// -// `Field :: struct abi(.zig) extern compiler { … }` binds the sx struct to the -// compiler's real internal Zig type (`StructInfo.Field`, two u32s) via the -// `compiler` library. The compiler validates the sx declaration against the -// welded type's layout at registration time (the sx side is a header checked -// against the implementation) — a faithful declaration validates clean and the -// struct is otherwise ordinary data. The `compiler` library is the comptime-only -// internal surface, so `#library "compiler"` is NOT dlopen'd. -// -// Phase 1 (foundation): the weld is layout-validated; field offsets coincide with -// the natural layout for `Field` (two u32s). Welded host-call functions land in a -// later phase. - -#import "modules/std.sx"; - -compiler :: #library "compiler"; - -Field :: struct abi(.zig) extern compiler { name: u32; ty: u32; } - -main :: () { - f := Field.{ name = 7, ty = 3 }; - print("name={} ty={}\n", f.name, f.ty); -} diff --git a/examples/0627-comptime-weld-struct-reflected-layout.sx b/examples/0627-comptime-weld-struct-reflected-layout.sx deleted file mode 100644 index de26b442..00000000 --- a/examples/0627-comptime-weld-struct-reflected-layout.sx +++ /dev/null @@ -1,30 +0,0 @@ -// Comptime compiler API — a welded struct mirrors the compiler's real Zig type -// byte-for-byte by declaring its fields in the compiler type's MEMORY order. -// -// `StructInfo` is the real `types.TypeInfo.StructInfo`, which Zig reorders from -// source order to (fields, name, nominal_id, is_protocol). The sx header declares -// the fields in that memory order; the compiler reflects the bound Zig type -// (@offsetOf/@sizeOf) and validates the header matches — so the struct is laid -// out identically and a pointer to it can be cast to the compiler's own type and -// dereferenced. Nothing is maintained by hand: a types.zig change re-reflects. - -#import "modules/std.sx"; - -compiler :: #library "compiler"; - -Field :: struct abi(.zig) extern compiler { name: u32; ty: u32; } - -StructInfo :: struct abi(.zig) extern compiler { - fields: []Field; - name: u32; - nominal_id: u32; - is_protocol: bool; -} - -main :: () { - si : StructInfo = ---; - si.name = 42; - si.nominal_id = 7; - si.is_protocol = true; - print("name={} nominal={} proto={}\n", si.name, si.nominal_id, si.is_protocol); -} diff --git a/examples/1183-diagnostics-weld-struct-field-count.sx b/examples/1183-diagnostics-weld-struct-field-count.sx deleted file mode 100644 index bff0c34c..00000000 --- a/examples/1183-diagnostics-weld-struct-field-count.sx +++ /dev/null @@ -1,17 +0,0 @@ -// Diagnostic: a layout-welded struct whose sx declaration does NOT faithfully -// mirror the compiler's real Zig type is a build error — the sx side is a header -// checked against the implementation, not a free reinterpretation. -// -// `Field` is two u32s (`name`, `ty`) in the compiler library; declaring it with a -// single field must be rejected at registration with a clear field-count message. - -#import "modules/std.sx"; - -compiler :: #library "compiler"; - -Field :: struct abi(.zig) extern compiler { name: u32; } - -main :: () { - f := Field.{ name = 1 }; - print("{}\n", f.name); -} diff --git a/examples/1186-diagnostics-weld-struct-wrong-order.sx b/examples/1186-diagnostics-weld-struct-wrong-order.sx deleted file mode 100644 index 82fd3b4c..00000000 --- a/examples/1186-diagnostics-weld-struct-wrong-order.sx +++ /dev/null @@ -1,20 +0,0 @@ -// Diagnostic: a welded struct whose fields are NOT in the compiler type's memory -// order is a loud build error — the sx header must mirror the real Zig layout so -// the two are byte-identical. The message names the offending position and shows -// the expected memory order. (Declaring StructInfo in source order trips this: -// Zig reorders it to fields-first.) - -#import "modules/std.sx"; - -compiler :: #library "compiler"; - -Field :: struct abi(.zig) extern compiler { name: u32; ty: u32; } - -StructInfo :: struct abi(.zig) extern compiler { - name: u32; - fields: []Field; - is_protocol: bool; - nominal_id: u32; -} - -main :: () { print("unreached\n"); } diff --git a/examples/expected/0625-comptime-weld-struct-field.exit b/examples/expected/0625-comptime-weld-struct-field.exit deleted file mode 100644 index 573541ac..00000000 --- a/examples/expected/0625-comptime-weld-struct-field.exit +++ /dev/null @@ -1 +0,0 @@ -0 diff --git a/examples/expected/0625-comptime-weld-struct-field.stderr b/examples/expected/0625-comptime-weld-struct-field.stderr deleted file mode 100644 index 8b137891..00000000 --- a/examples/expected/0625-comptime-weld-struct-field.stderr +++ /dev/null @@ -1 +0,0 @@ - diff --git a/examples/expected/0625-comptime-weld-struct-field.stdout b/examples/expected/0625-comptime-weld-struct-field.stdout deleted file mode 100644 index 9c0e2d64..00000000 --- a/examples/expected/0625-comptime-weld-struct-field.stdout +++ /dev/null @@ -1 +0,0 @@ -name=7 ty=3 diff --git a/examples/expected/0627-comptime-weld-struct-reflected-layout.exit b/examples/expected/0627-comptime-weld-struct-reflected-layout.exit deleted file mode 100644 index 573541ac..00000000 --- a/examples/expected/0627-comptime-weld-struct-reflected-layout.exit +++ /dev/null @@ -1 +0,0 @@ -0 diff --git a/examples/expected/0627-comptime-weld-struct-reflected-layout.stderr b/examples/expected/0627-comptime-weld-struct-reflected-layout.stderr deleted file mode 100644 index 8b137891..00000000 --- a/examples/expected/0627-comptime-weld-struct-reflected-layout.stderr +++ /dev/null @@ -1 +0,0 @@ - diff --git a/examples/expected/0627-comptime-weld-struct-reflected-layout.stdout b/examples/expected/0627-comptime-weld-struct-reflected-layout.stdout deleted file mode 100644 index 36629681..00000000 --- a/examples/expected/0627-comptime-weld-struct-reflected-layout.stdout +++ /dev/null @@ -1 +0,0 @@ -name=42 nominal=7 proto=true diff --git a/examples/expected/1183-diagnostics-weld-struct-field-count.exit b/examples/expected/1183-diagnostics-weld-struct-field-count.exit deleted file mode 100644 index d00491fd..00000000 --- a/examples/expected/1183-diagnostics-weld-struct-field-count.exit +++ /dev/null @@ -1 +0,0 @@ -1 diff --git a/examples/expected/1183-diagnostics-weld-struct-field-count.stderr b/examples/expected/1183-diagnostics-weld-struct-field-count.stderr deleted file mode 100644 index 4c9413ad..00000000 --- a/examples/expected/1183-diagnostics-weld-struct-field-count.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: welded type 'Field': the compiler type has 2 field(s) but the declaration has 1 — declare them in memory order: name, ty - --> examples/1183-diagnostics-weld-struct-field-count.sx:12:51 - | -12 | Field :: struct abi(.zig) extern compiler { name: u32; } - | ^^^ diff --git a/examples/expected/1183-diagnostics-weld-struct-field-count.stdout b/examples/expected/1183-diagnostics-weld-struct-field-count.stdout deleted file mode 100644 index 8b137891..00000000 --- a/examples/expected/1183-diagnostics-weld-struct-field-count.stdout +++ /dev/null @@ -1 +0,0 @@ - diff --git a/examples/expected/1186-diagnostics-weld-struct-wrong-order.exit b/examples/expected/1186-diagnostics-weld-struct-wrong-order.exit deleted file mode 100644 index d00491fd..00000000 --- a/examples/expected/1186-diagnostics-weld-struct-wrong-order.exit +++ /dev/null @@ -1 +0,0 @@ -1 diff --git a/examples/expected/1186-diagnostics-weld-struct-wrong-order.stderr b/examples/expected/1186-diagnostics-weld-struct-wrong-order.stderr deleted file mode 100644 index e515b4c9..00000000 --- a/examples/expected/1186-diagnostics-weld-struct-wrong-order.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: welded type 'StructInfo': wrong field order at position 0 — found 'name', the compiler type has 'fields' here (memory order: fields, name, nominal_id, is_protocol) - --> examples/1186-diagnostics-weld-struct-wrong-order.sx:14:11 - | -14 | name: u32; - | ^^^ diff --git a/examples/expected/1186-diagnostics-weld-struct-wrong-order.stdout b/examples/expected/1186-diagnostics-weld-struct-wrong-order.stdout deleted file mode 100644 index 8b137891..00000000 --- a/examples/expected/1186-diagnostics-weld-struct-wrong-order.stdout +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/ir/compiler_lib.test.zig b/src/ir/compiler_lib.test.zig index f3cedefd..c0ea7e7c 100644 --- a/src/ir/compiler_lib.test.zig +++ b/src/ir/compiler_lib.test.zig @@ -1,139 +1,11 @@ -// Tests for the comptime `compiler` library's binding registry. +// Tests for the comptime `compiler` library's function bridge. const std = @import("std"); const compiler_lib = @import("compiler_lib.zig"); -const types = @import("types.zig"); -// Lock: `findType("Field")` resolves to the welded `StructInfo.Field` type, and -// its baked layout EQUALS the real Zig type's `@sizeOf`/`@alignOf`/`@offsetOf`. -// This is the foundation the layout sub-step builds on — the welded record's -// offsets come from the implementation, so they can't drift. -test "compiler_lib: Field welds to StructInfo.Field's real layout" { - const FieldZig = types.TypeInfo.StructInfo.Field; - - const bt = compiler_lib.findType("Field") orelse return error.MissingBoundType; - - try std.testing.expectEqualStrings("Field", bt.sx_name); - try std.testing.expectEqual(@sizeOf(FieldZig), bt.size); - try std.testing.expectEqual(@alignOf(FieldZig), bt.alignment); - - // Two u32 fields, in declaration order. - try std.testing.expectEqual(@as(usize, 2), bt.fields.len); - - try std.testing.expectEqualStrings("name", bt.fields[0].name); - try std.testing.expectEqual(@offsetOf(FieldZig, "name"), bt.fields[0].offset); - try std.testing.expectEqual(@as(usize, 4), bt.fields[0].size); - - try std.testing.expectEqualStrings("ty", bt.fields[1].name); - try std.testing.expectEqual(@offsetOf(FieldZig, "ty"), bt.fields[1].offset); - try std.testing.expectEqual(@as(usize, 4), bt.fields[1].size); - - // Sanity: the concrete shape the design calls out — two u32s, 8 bytes. - try std.testing.expectEqual(@as(usize, 8), bt.size); - try std.testing.expectEqual(@as(usize, 0), bt.fields[0].offset); - try std.testing.expectEqual(@as(usize, 4), bt.fields[1].offset); -} - -// Lock: a name NOT on the export list is unreachable — `findType` returns null -// (the safety boundary; the welded-decl path falls through to a clean error, -// never a silent default). -test "compiler_lib: unexported name returns null" { - try std.testing.expect(compiler_lib.findType("NotExported") == null); - try std.testing.expect(compiler_lib.findType("") == null); -} - -// Lock: a faithful sx header for `Field` validates clean (the natural two-u32 -// layout matches the welded type). -test "compiler_lib: validateStructLayout accepts a faithful Field header" { - const bt = compiler_lib.findType("Field").?; - const sx = [_]compiler_lib.SxField{ - .{ .name = "name", .size = 4 }, - .{ .name = "ty", .size = 4 }, - }; - try std.testing.expect(compiler_lib.validateStructLayout(bt, &sx, 8) == null); -} - -// Lock: every drift the assertion is meant to catch surfaces as the right -// `LayoutMismatch` variant (field count / name / size / total), and the first -// mismatch wins. -test "compiler_lib: validateStructLayout flags each kind of drift" { - const bt = compiler_lib.findType("Field").?; - - // Wrong field count (one field instead of two). - { - const sx = [_]compiler_lib.SxField{.{ .name = "name", .size = 4 }}; - const m = compiler_lib.validateStructLayout(bt, &sx, 4).?; - try std.testing.expect(m == .field_count); - try std.testing.expectEqual(@as(usize, 2), m.field_count.expected); - try std.testing.expectEqual(@as(usize, 1), m.field_count.got); - } - // Wrong field name (reorder / rename) at index 1. - { - const sx = [_]compiler_lib.SxField{ - .{ .name = "name", .size = 4 }, - .{ .name = "kind", .size = 4 }, - }; - const m = compiler_lib.validateStructLayout(bt, &sx, 8).?; - try std.testing.expect(m == .field_name); - try std.testing.expectEqual(@as(usize, 1), m.field_name.index); - try std.testing.expectEqualStrings("ty", m.field_name.expected); - try std.testing.expectEqualStrings("kind", m.field_name.got); - } - // Wrong field size (retype to an 8-byte field). - { - const sx = [_]compiler_lib.SxField{ - .{ .name = "name", .size = 4 }, - .{ .name = "ty", .size = 8 }, - }; - const m = compiler_lib.validateStructLayout(bt, &sx, 12).?; - try std.testing.expect(m == .field_size); - try std.testing.expectEqual(@as(usize, 1), m.field_size.index); - try std.testing.expectEqual(@as(usize, 4), m.field_size.expected); - try std.testing.expectEqual(@as(usize, 8), m.field_size.got); - } - // Right fields, wrong total (padding drift). - { - const sx = [_]compiler_lib.SxField{ - .{ .name = "name", .size = 4 }, - .{ .name = "ty", .size = 4 }, - }; - const m = compiler_lib.validateStructLayout(bt, &sx, 16).?; - try std.testing.expect(m == .total_size); - try std.testing.expectEqual(@as(usize, 8), m.total_size.expected); - try std.testing.expectEqual(@as(usize, 16), m.total_size.got); - } -} - -// Lock: `StructInfo` is reflected in MEMORY order — Zig reorders it from source -// order (name, fields, is_protocol, nominal_id) to (fields@0, name@16, -// nominal_id@20, is_protocol@24). The registry must present the fields in that -// memory order, since an sx welded header must declare them so to be -// byte-identical. -test "compiler_lib: StructInfo is reflected in Zig memory order" { - const StructInfoZig = types.TypeInfo.StructInfo; - const bt = compiler_lib.findType("StructInfo").?; - - try std.testing.expectEqual(@sizeOf(StructInfoZig), bt.size); - try std.testing.expectEqual(@as(usize, 4), bt.fields.len); - - // Memory order: fields, name, nominal_id, is_protocol. - try std.testing.expectEqualStrings("fields", bt.fields[0].name); - try std.testing.expectEqual(@offsetOf(StructInfoZig, "fields"), bt.fields[0].offset); - try std.testing.expectEqualStrings("name", bt.fields[1].name); - try std.testing.expectEqual(@offsetOf(StructInfoZig, "name"), bt.fields[1].offset); - try std.testing.expectEqualStrings("nominal_id", bt.fields[2].name); - try std.testing.expectEqual(@offsetOf(StructInfoZig, "nominal_id"), bt.fields[2].offset); - try std.testing.expectEqualStrings("is_protocol", bt.fields[3].name); - try std.testing.expectEqual(@offsetOf(StructInfoZig, "is_protocol"), bt.fields[3].offset); - - // Offsets are strictly ascending (memory order). - try std.testing.expect(bt.fields[0].offset < bt.fields[1].offset); - try std.testing.expect(bt.fields[1].offset < bt.fields[2].offset); - try std.testing.expect(bt.fields[2].offset < bt.fields[3].offset); -} - -// Lock: the welded-function export list resolves the round-trip readers and -// rejects unexported names (the boundary the interp's dispatch consults). +// Lock: the compiler-function export list resolves the round-trip readers and +// rejects unexported names (the boundary `weldedCompilerFn` + the interp's +// dispatch consult). test "compiler_lib: findFn resolves exported functions, rejects others" { try std.testing.expect(compiler_lib.findFn("intern") != null); try std.testing.expect(compiler_lib.findFn("text_of") != null); diff --git a/src/ir/compiler_lib.zig b/src/ir/compiler_lib.zig index cf1563f5..4c7e9e12 100644 --- a/src/ir/compiler_lib.zig +++ b/src/ir/compiler_lib.zig @@ -1,21 +1,20 @@ -//! The comptime `compiler` library's binding registry — the curated surface of -//! the compiler's own types (layout-welded) and functions (host-call bridged) -//! reachable from comptime sx via `abi(.zig) extern compiler`. See -//! `design/comptime-compiler-api.md`. +//! The comptime `compiler` library's function bridge — the curated set of the +//! compiler's own functions reachable from comptime sx via +//! `abi(.zig) extern compiler`. See `current/PLAN-COMPILER-VM.md`. //! -//! **This registry IS the safety boundary.** Only the entries registered here -//! are bindable from user comptime code; anything not on the export list is -//! unreachable. A welded `Name :: struct abi(.zig) extern compiler { … }` (or a -//! welded fn) resolves its layout/dispatch against this table, not the ordinary -//! extern-lib path. +//! **This registry IS the safety boundary.** Only the functions registered here +//! are bindable from user comptime code; a name not on the export list is +//! rejected at declaration (`weldedCompilerFn`), and the interpreter dispatches a +//! welded call to the matching Zig handler instead of dlsym. //! -//! **Layout is welded, not guessed.** Because the sx compiler is itself a Zig -//! program, the real internal type's layout is available at compiler-build time: -//! each `BoundType` bakes `@sizeOf`/`@alignOf`/`@offsetOf` from the bound Zig -//! type. A `types.zig` change re-bakes the offsets on the next build, so both -//! sides move together. The sx-side `struct abi(.zig) …` declaration is then a -//! *header* checked against these offsets (the build-time layout-equality -//! assertion lands in the layout sub-step). +//! **Direction note (2026-06-17 pivot).** The byte-weld of TYPES (sx structs whose +//! layout was validated to mirror the compiler's Zig records) was stripped — it +//! bolted a parallel layout regime + hand-marshaling onto a comptime value model +//! that isn't bytes. The replacement is a flat-memory comptime VM where values are +//! native bytes, so the compiler-API needs no weld/validation/marshaling (Phase 3 +//! of the plan re-homes the type/function exposure on that VM). `intern`/`text_of` +//! survive here as the first compiler-call seed: clean scalar host-calls (string in, +//! handle out), no weld involved. const std = @import("std"); const types = @import("types.zig"); @@ -25,135 +24,10 @@ const Interpreter = interp_mod.Interpreter; const InterpError = interp_mod.InterpError; const StringId = types.StringId; -/// One field of a welded type: its sx-visible name plus the byte offset + size -/// taken from the bound Zig type. -pub const FieldLayout = struct { - name: []const u8, - offset: usize, - size: usize, -}; - -/// A type exported by the `compiler` library, welded to a real internal Zig -/// type. `size`/`alignment`/`fields` are baked from that Zig type at -/// compiler-build time (so they cannot drift from the implementation). -pub const BoundType = struct { - /// The sx-side name a welded `struct abi(.zig) extern compiler` uses. - sx_name: []const u8, - size: usize, - alignment: usize, - fields: []const FieldLayout, -}; - -/// The real internal Zig type each welded export binds to. Kept as named -/// aliases so the binding sites read as a curated list. -const FieldZig = types.TypeInfo.StructInfo.Field; // { name: StringId, ty: TypeId } — two u32s -const StructInfoZig = types.TypeInfo.StructInfo; // { name, fields: []Field, is_protocol, nominal_id } — Zig-reordered - -/// Bake a `BoundType` by REFLECTING the real Zig struct type `T` — field names -/// from `@typeInfo`, offsets from `@offsetOf`, sizes from `@sizeOf`. Nothing is -/// maintained by hand: a `types.zig` change re-bakes on the next compiler build. -/// Fields are returned in ascending-OFFSET (memory) order, which is the order an -/// sx welded header must declare them in to be byte-identical (Zig may reorder a -/// struct's fields from source order). The sx-visible field name IS the Zig -/// field identifier. -fn weldStruct(comptime sx_name: []const u8, comptime T: type) BoundType { - const zig_fields = @typeInfo(T).@"struct".fields; - comptime var layouts: [zig_fields.len]FieldLayout = undefined; - inline for (zig_fields, 0..) |zf, i| { - layouts[i] = .{ - .name = zf.name, - .offset = @offsetOf(T, zf.name), - .size = @sizeOf(zf.type), - }; - } - // Sort into memory order so the sx header is checked against the layout the - // compiler actually uses (declaration order != memory order under Zig's - // auto-layout). - comptime std.sort.insertion(FieldLayout, &layouts, {}, struct { - fn lt(_: void, a: FieldLayout, b: FieldLayout) bool { - return a.offset < b.offset; - } - }.lt); - const frozen = layouts; - return .{ - .sx_name = sx_name, - .size = @sizeOf(T), - .alignment = @alignOf(T), - .fields = &frozen, - }; -} - -/// The welded-type export list. Each entry reflects a real internal Zig type; -/// the sx header that binds it must mirror these fields IN THIS (memory) ORDER. -/// `Field` (two u32s) is naturally ordered; `StructInfo` is Zig-reordered -/// (`fields`@0, `name`@16, `nominal_id`@20, `is_protocol`@24). -pub const bound_types = [_]BoundType{ - weldStruct("Field", FieldZig), - weldStruct("StructInfo", StructInfoZig), -}; - -/// Look up a welded type by its sx name. Returns null when the name is not on -/// the `compiler` library's export list (the lookup the welded-decl resolution -/// path consults instead of the ordinary extern-lib path). -pub fn findType(sx_name: []const u8) ?*const BoundType { - for (&bound_types) |*bt| { - if (std.mem.eql(u8, bt.sx_name, sx_name)) return bt; - } - return null; -} - -/// The name of the only welded library. A `struct abi(.zig) extern ` with a -/// different `` is rejected — `compiler` is the sole comptime weld source. +/// The name of the only compiler library. A `fn abi(.zig) extern ` with a +/// different `` is rejected — `compiler` is the sole comptime bind source. pub const lib_name = "compiler"; -/// One field of an sx welded-struct declaration, as the lowering observed it: -/// the field's sx name plus the size the sx type system computed for its type. -pub const SxField = struct { - name: []const u8, - size: usize, -}; - -/// The first way an sx welded-struct declaration fails to faithfully mirror the -/// bound Zig type. The sx declaration is a *header* checked against the real -/// implementation, so any drift is a build error rather than a silent -/// reinterpretation. The caller renders the chosen variant into a diagnostic. -pub const LayoutMismatch = union(enum) { - /// The sx declaration has a different field count than the welded type. - field_count: struct { expected: usize, got: usize }, - /// Field `index` carries the wrong sx name (a weld is positional + by-name). - field_name: struct { index: usize, expected: []const u8, got: []const u8 }, - /// Field `index` (`name`) is a different size than the welded type's field. - field_size: struct { index: usize, name: []const u8, expected: usize, got: usize }, - /// The total struct size differs (padding / alignment drift). - total_size: struct { expected: usize, got: usize }, -}; - -/// Check an sx welded-struct declaration against the bound Zig type. Returns the -/// FIRST mismatch, or null if the sx declaration is a faithful header. Fields are -/// checked positionally + by name + by size, and the total size is compared — for -/// a natural (C-like) layout this catches a missing/extra field (count), a rename -/// or reorder (name), a retype (size), and padding drift (total). Explicit -/// per-field OFFSET overrides (for non-natural Zig layouts — slices, reordered or -/// `union(enum)` fields) arrive with `StructInfo` in Phase 2; `Field`'s two-u32 -/// natural layout needs none. -pub fn validateStructLayout( - bt: *const BoundType, - sx_fields: []const SxField, - sx_total_size: usize, -) ?LayoutMismatch { - if (sx_fields.len != bt.fields.len) - return .{ .field_count = .{ .expected = bt.fields.len, .got = sx_fields.len } }; - for (sx_fields, bt.fields, 0..) |sf, bf, i| { - if (!std.mem.eql(u8, sf.name, bf.name)) - return .{ .field_name = .{ .index = i, .expected = bf.name, .got = sf.name } }; - if (sf.size != bf.size) - return .{ .field_size = .{ .index = i, .name = bf.name, .expected = bf.size, .got = sf.size } }; - } - if (sx_total_size != bt.size) - return .{ .total_size = .{ .expected = bt.size, .got = sx_total_size } }; - return null; -} - // ── Functions (comptime-only, host-call bridged) ──────────────────────────── /// A welded `compiler` function: dispatched under the comptime interpreter to its @@ -167,16 +41,16 @@ pub const BoundFn = struct { handler: FnHandler, }; -/// The welded-function export list. Start small (Phase 1): the `StringId` -/// round-trip readers. `find_type` / the guarded `register_*` mutators join in -/// later phases. +/// The compiler-function export list. The `StringId` round-trip readers are the +/// seed; the type-table API (lookup / register) is re-homed onto the flat-memory +/// VM in Phase 3 of `PLAN-COMPILER-VM.md`. pub const bound_fns = [_]BoundFn{ .{ .sx_name = "intern", .handler = handleIntern }, .{ .sx_name = "text_of", .handler = handleTextOf }, }; -/// Look up a welded function by its sx name. Returns null when the name is not on -/// the `compiler` library's function-export list. +/// 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 { for (&bound_fns) |*bf| { if (std.mem.eql(u8, bf.sx_name, sx_name)) return bf; diff --git a/src/ir/lower/nominal.zig b/src/ir/lower/nominal.zig index 47706ce8..d527c84e 100644 --- a/src/ir/lower/nominal.zig +++ b/src/ir/lower/nominal.zig @@ -6,7 +6,6 @@ const mod_mod = @import("../module.zig"); const type_bridge = @import("../type_bridge.zig"); const program_index_mod = @import("../program_index.zig"); const resolver_mod = @import("../resolver.zig"); -const compiler_lib = @import("../compiler_lib.zig"); const StructTemplate = program_index_mod.StructTemplate; const TemplateParam = program_index_mod.TemplateParam; @@ -674,13 +673,7 @@ pub fn registerStructDecl(self: *Lowering, sd: *const ast.StructDecl, source_fil // any forward-reference stub. Same-name structs in DIFFERENT sources get // distinct TypeIds instead of last-wins clobbering the first. const info: types.TypeInfo = .{ .@"struct" = .{ .name = name_id, .fields = fields.items } }; - const struct_tid = self.internNamedTypeDecl(decl_key, name_id, info, nominal_id); - - // Welded `struct abi(.zig) extern compiler { … }`: the sx declaration is a - // header checked against the compiler's real Zig type — validate the layout - // matches the binding registry (a mismatch is a build error). See - // design/comptime-compiler-api.md. - if (sd.abi == .zig) validateWeldedStruct(self, sd, struct_tid, fields.items); + _ = self.internNamedTypeDecl(decl_key, name_id, info, nominal_id); // Store field defaults for struct literal lowering if (sd.field_defaults.len > 0) { @@ -716,77 +709,6 @@ pub fn registerStructDecl(self: *Lowering, sd: *const ast.StructDecl, source_fil } } -/// Validate a welded `struct abi(.zig) extern { … }` against the `compiler` -/// library's binding registry: the bound library must be `compiler`, the name -/// must be on the export list, and the sx-declared layout must match the real Zig -/// type's (the sx side is a *header* checked against the implementation). Any -/// failure is a build-gating `.err` diagnostic — never a silent reinterpretation. -fn validateWeldedStruct(self: *Lowering, sd: *const ast.StructDecl, tid: TypeId, fields: []const types.TypeInfo.StructInfo.Field) void { - const diags = self.diagnostics orelse return; - const table = &self.module.types; - - // A span that points into the struct (its first field, else zero) — the decl - // has no name span of its own. - const span: ast.Span = if (sd.field_types.len > 0) sd.field_types[0].span else .{ .start = 0, .end = 0 }; - - // The bound library must be the sole welded source. - if (sd.extern_lib == null or !std.mem.eql(u8, sd.extern_lib.?, compiler_lib.lib_name)) { - diags.addFmt(.err, span, "abi(.zig) struct '{s}' must bind the compiler library — write `extern {s}`", .{ sd.name, compiler_lib.lib_name }); - return; - } - - // The name must be on the curated export list (the safety boundary). - const bt = compiler_lib.findType(sd.name) orelse { - diags.addFmt(.err, span, "'{s}' is not a type exported by the '{s}' library", .{ sd.name, compiler_lib.lib_name }); - return; - }; - - // Build the observed sx layout (field name + computed size) and total size. - var sx_fields = std.ArrayList(compiler_lib.SxField).empty; - defer sx_fields.deinit(self.alloc); - for (fields) |f| { - sx_fields.append(self.alloc, .{ - .name = table.getString(f.name), - .size = table.typeSizeBytes(f.ty), - }) catch return; - } - const total = table.typeSizeBytes(tid); - - const mismatch = compiler_lib.validateStructLayout(bt, sx_fields.items, total) orelse return; - // The compiler type's fields, in the memory order an sx header must mirror — - // included in the order/count diagnostics so the fix is obvious. - const order = weldedFieldOrderStr(self.alloc, bt); - defer if (order.len > 0) self.alloc.free(order); - switch (mismatch) { - .field_count => |m| diags.addFmt(.err, span, "welded type '{s}': the compiler type has {d} field(s) but the declaration has {d} — declare them in memory order: {s}", .{ sd.name, m.expected, m.got, order }), - .field_name => |m| { - // Distinguish "this name isn't a field at all" from "right field set, - // wrong order". - const exists = blk: { - for (bt.fields) |bf| if (std.mem.eql(u8, bf.name, m.got)) break :blk true; - break :blk false; - }; - if (exists) - diags.addFmt(.err, span, "welded type '{s}': wrong field order at position {d} — found '{s}', the compiler type has '{s}' here (memory order: {s})", .{ sd.name, m.index, m.got, m.expected, order }) - else - diags.addFmt(.err, span, "welded type '{s}': field '{s}' is not a field of the compiler type (its fields, in memory order: {s})", .{ sd.name, m.got, order }); - }, - .field_size => |m| diags.addFmt(.err, span, "welded type '{s}': type layout mismatch — field '{s}' is {d} byte(s) in the compiler type but {d} as declared", .{ sd.name, m.name, m.expected, m.got }), - .total_size => |m| diags.addFmt(.err, span, "welded type '{s}': layout mismatch — the compiler type is {d} byte(s) but the declaration is {d} (alignment/padding)", .{ sd.name, m.expected, m.got }), - } -} - -/// The bound type's field names in memory order, `, `-joined, for diagnostics. -/// Returns an owned string; empty (no free needed) on allocation failure. -fn weldedFieldOrderStr(alloc: std.mem.Allocator, bt: *const compiler_lib.BoundType) []const u8 { - var buf = std.ArrayList(u8).empty; - for (bt.fields, 0..) |bf, i| { - if (i > 0) buf.appendSlice(alloc, ", ") catch return ""; - buf.appendSlice(alloc, bf.name) catch return ""; - } - return buf.toOwnedSlice(alloc) catch ""; -} - /// Register a top-level ENUM decl under a per-decl nominal identity (E6a) — /// the enum twin of `registerStructDecl`. A GENUINE same-name shadow already /// reserved its DISTINCT slot up-front in `scanDecls` (the first at id 0, the