compiler-API: welded structs by reflection + memory-order validation
Replace the explored byte-layout-override engine (offset-ordered LLVM structs /
weld plans / byte-blobs — all unnecessary) with a much simpler design: a welded
`struct abi(.zig) extern compiler { … }` is a bodied header declaring its fields
in the bound compiler type's MEMORY order. The compiler reflects the real Zig
type (field names via @typeInfo, offsets via @offsetOf, size via @sizeOf —
nothing hand-maintained) and validates the header matches, with loud diagnostics.
On pass it is an ordinary struct whose natural layout already equals the Zig
layout — no reorder, no padding, no index/remap tables, no special LLVM path — so
@ptrCast'ing it to the compiler's own type and dereferencing is byte-identical.
When types.zig shifts, the header stops matching and the developer gets a specific
message to fix it.
- compiler_lib.zig: weldStruct reflects field names and bakes bound_types fields
in ascending-offset (memory) order; deleted computeWeldPlan/WeldPlan/WeldElement.
- nominal.zig validateWeldedStruct: precise diagnostics — field-not-found,
wrong-field-order (+ expected memory order), type-layout (size) mismatch,
total-size mismatch.
- Examples: 0627 (StructInfo in memory order, byte-identical, usable),
1186 (source-order StructInfo -> wrong-field-order diagnostic); 1183 refreshed.
- Design doc + checkpoint updated.
This commit is contained in:
@@ -7,25 +7,59 @@ Companion to the design-of-record
|
||||
with ONE welded mechanism. Branch: `reify` (off `master`). Update after every step.
|
||||
|
||||
## ⏯ Resume (fresh session)
|
||||
Phase 1 is COMPLETE and committed (`cd5b958`); Phase 2 (full byte-layout weld)
|
||||
just started. **Do sub-step 2.2 next** — make `src/backend/llvm/types.zig`'s
|
||||
`.@"struct"` case build a welded struct's LLVM type from `compiler_lib.computeWeldPlan`
|
||||
(offset-ordered field elements + `[N x i8]` padding) with a build-time
|
||||
`LLVMOffsetOfElement == plan offset` + `LLVMABISizeOfType == total_size` assertion;
|
||||
cache the plan per TypeId for the GEP sites. The plan math (sub-step 2.1) is done,
|
||||
pure, and unit-tested — see `computeWeldPlan` in `src/ir/compiler_lib.zig`. Full
|
||||
2.2–2.6 breakdown under **## Next step**. Read order: this file → the design doc →
|
||||
`src/ir/compiler_lib.zig`. Build/verify: `zig build && zig build test` (green now).
|
||||
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.
|
||||
|
||||
> ⚠ Snapshot gotcha: `zig build test -Dupdate-goldens` on this aarch64 host clobbers
|
||||
> cross-arch examples' CI-captured `.stdout` (1228/1231/1639/1651/1657–1660) with
|
||||
> host-specific empties. After regenerating, revert those (`git checkout` / `rm`)
|
||||
> before committing — they are NOT part of this stream.
|
||||
**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`.
|
||||
|
||||
> ⚠ 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
|
||||
> and a flaky/host-divergent example (AOT/cross-arch) can clobber good snapshots.
|
||||
> See CLAUDE.md → Snapshot integrity.
|
||||
|
||||
## Last completed step
|
||||
**Phase 2, sub-step 1 — the weld-plan layout math + `StructInfo` registered.**
|
||||
The de-risked core of the byte-layout-override ("GEP") engine, pure + unit-tested,
|
||||
no emit/interp wiring yet (suite trivially green).
|
||||
**Phase 2 — welded structs by reflection + memory-order validation (byte-identical,
|
||||
no GEP engine).** A welded `struct abi(.zig) extern compiler { … }` now works
|
||||
end-to-end as a byte-identical mirror of the bound Zig type.
|
||||
|
||||
Design (locked, supersedes the byte-layout-override plan):
|
||||
- The sx header declares fields in the compiler type's MEMORY order. The compiler
|
||||
REFLECTS the bound Zig type — field names from `@typeInfo`, offsets from
|
||||
`@offsetOf`, size from `@sizeOf` — and validates the header matches. Nothing is
|
||||
maintained by hand; a `types.zig` change re-reflects on the next compiler build.
|
||||
- On pass it's an ORDINARY struct whose natural layout already equals the Zig
|
||||
layout → `@ptrCast` to the compiler type + deref is byte-identical. No
|
||||
byte-blob, no index/remap tables, no reorder, no special LLVM path.
|
||||
- Loud, precise diagnostics on any drift: *field not found* (+ memory order),
|
||||
*wrong field order at position N* (+ expected memory order), *type layout
|
||||
mismatch* (field size), *layout mismatch* (total size / count).
|
||||
|
||||
What changed from the dropped plan:
|
||||
- `compiler_lib.zig`: `weldStruct` now REFLECTS field names (`@typeInfo`) and bakes
|
||||
`bound_types` fields in ascending-OFFSET (memory) order — no hand-listed names.
|
||||
Deleted `computeWeldPlan`/`WeldPlan`/`WeldElement`. `validateStructLayout` checks
|
||||
the sx header against the memory-ordered registry.
|
||||
- `nominal.zig` `validateWeldedStruct`: renders the precise diagnostics
|
||||
(+ `weldedFieldOrderStr`).
|
||||
- Examples: `0627` (StructInfo in memory order, byte-identical, usable);
|
||||
`1186` (source-order StructInfo → wrong-field-order diagnostic). `1183` message
|
||||
refreshed.
|
||||
- `zig build` + `zig build test` green (692 corpus, unit tests pass).
|
||||
|
||||
### Earlier — Phase 2.1 (weld-plan layout math, now removed)
|
||||
**The weld-plan offset math + `StructInfo` registered.** Was the core of the
|
||||
byte-layout-override engine; superseded by the reflection+validation design above.
|
||||
|
||||
Decision (locked 2026-06-17): **full byte-layout weld** — a welded sx struct is
|
||||
laid out byte-identically to the bound Zig type (Zig's `@offsetOf`, reordering +
|
||||
@@ -204,54 +238,48 @@ 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 decomposition (byte-layout weld for `StructInfo`)
|
||||
## Next step — Phase 2: welded compiler FUNCTIONS over the real types
|
||||
|
||||
The weld plan (sub-step 1) is the pure layout math. The remaining sub-steps wire
|
||||
it through emit + interp so a non-natural welded struct actually works. Each must
|
||||
stay green; do ONE per session (the IR-stream split rule).
|
||||
Welded structs are byte-identical mirrors now, so the API surface can grow:
|
||||
|
||||
- **2.2 — LLVM type honours the plan.** In `src/backend/llvm/types.zig` `.@"struct"`
|
||||
case: if the struct's name is in `compiler_lib.findType`, build the LLVM struct
|
||||
from `computeWeldPlan` — elements in offset order (real field types + `[N x i8]`
|
||||
padding), and **assert** `LLVMOffsetOfElement(elem) == plan.elements[e].offset`
|
||||
for every field element + `LLVMABISizeOfType == total_size` (the build-time
|
||||
layout-equality assertion; mismatch = a loud emit failure). Cache the plan per
|
||||
TypeId (the GEP sites + interp need the remap). Prove: a welded struct's LLVM
|
||||
type has the Zig offsets (an emit-level test or an `.ir`/codegen check).
|
||||
- **2.3 — field access honours the remap.** Every `struct_gep` / field load+store
|
||||
for a welded struct maps the sx field index → `plan.sx_to_llvm[i]` before
|
||||
`LLVMBuildStructGEP2` (`src/backend/llvm/ops.zig` — `emitFieldAccess` /
|
||||
struct-literal init / the `field_ptr` paths). Prove with a REORDERED welded
|
||||
struct used as runtime data: construct + read each field back correct.
|
||||
- **2.4 — interp comptime layout.** The comptime interp represents structs as
|
||||
`Value.aggregate` by logical index — fine for field access. The byte layout
|
||||
matters at the handler boundary: serialize a welded-struct `Value` into
|
||||
Zig-layout memory (via the plan's offsets) so a handler can take `*ZigType`,
|
||||
and read a Zig-layout result back into a `Value`. (Or: keep handlers reading
|
||||
`Value` aggregates logically — decide when wiring `register_struct`.)
|
||||
- **2.5 — `register_struct` / `find_type` handlers.** Bind
|
||||
`register_struct(StructInfo) -> Type` (guarded: dup field names, kind) +
|
||||
`find_type(StringId) -> ?Type` over the host-call bridge, consuming a welded
|
||||
`StructInfo`. Prove: build a struct programmatically + round-trip a source one.
|
||||
- **2.6 — re-express `type_info`/`define` (struct) as sx** over `register_struct`/
|
||||
- **Bind `register_struct` / `find_type`** over the host-call bridge
|
||||
(`compiler_lib.zig` `bound_fns`, like `intern`/`text_of`). `register_struct`
|
||||
takes a welded `StructInfo` and mints a real `TypeId` (guarded: dup field names,
|
||||
kind well-formedness — the checks `define` does today). Because the welded
|
||||
`StructInfo` is byte-identical, the handler can read it as the real Zig
|
||||
`*StructInfo` (cast + deref) rather than marshalling a `Value` field-by-field —
|
||||
the payoff of the byte-weld. `find_type(StringId) -> ?Type` reads the table.
|
||||
Prove: build a struct programmatically + round-trip a source one.
|
||||
- **Re-express `type_info`/`define` (struct) as sx** over `register_struct`/
|
||||
`find_type`; migrate `examples/0622`; delete the bespoke struct interp arms
|
||||
(`defineStruct`/`reflectTypeInfo` struct path). Design build-order steps 2–3.
|
||||
(`defineStruct` / the `reflectTypeInfo` struct path).
|
||||
|
||||
Then Phase 3+: widen to enum/tuple (`EnumInfo`/`TaggedUnionInfo`/`TupleInfo`,
|
||||
optional fields → sentinels), migrate `BuildOptions` to `abi(.zig) extern
|
||||
compiler` (the `#compiler` registry re-homes under the `compiler` lib), delete
|
||||
Then Phase 3+: widen the welded types to `EnumInfo`/`TaggedUnionInfo`/`TupleInfo`
|
||||
(optional fields → sentinels) — each just needs an sx header in the compiler
|
||||
type's memory order + the matching `register_*` fn. Finally migrate `BuildOptions`
|
||||
to `abi(.zig) extern compiler` (re-home the `#compiler` registry) and delete
|
||||
`#compiler`.
|
||||
|
||||
Note: a welded struct with an `?T` / `union(enum)` field (e.g. `EnumInfo`'s
|
||||
`backing_type: ?TypeId`, `explicit_values: ?[]const i64`) is the next layout
|
||||
wrinkle — the sx header must mirror Zig's optional/union representation. Handle
|
||||
when reached (sentinels or accessor fns; see the design doc Risks).
|
||||
|
||||
## Known issues
|
||||
- None for this stream. (Metatype's deferred enhancement is issue 0141 — comptime
|
||||
`List` growth; orthogonal, see `current/CHECKPOINT-METATYPE.md`.)
|
||||
|
||||
## Log
|
||||
- **Phase 2.1 — weld-plan layout math + `StructInfo` registered.** Decision:
|
||||
full byte-layout weld (not logical-field marshalling). `computeWeldPlan`
|
||||
(offset-order elements + padding + sx→element remap), pure + unit-tested
|
||||
against `Field` (identity) and `StructInfo` (reordered, remap `[1,0,3,2]`).
|
||||
No emit/interp wiring yet. Build + suite green.
|
||||
- **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
|
||||
fields in the compiler type's memory order; the compiler reflects the bound Zig
|
||||
type (`@typeInfo`/`@offsetOf`/`@sizeOf`) and validates the header matches with
|
||||
loud diagnostics (field-not-found, wrong-order+expected-order, size mismatch).
|
||||
On pass it's an ordinary byte-identical struct — cast + deref just works.
|
||||
Examples 0627 (usable) / 1186 (wrong-order diagnostic). Suite green (692).
|
||||
- **Phase 2.1 — weld-plan layout math (REMOVED).** The byte-layout-override math;
|
||||
superseded by the reflection+validation design and deleted.
|
||||
- **Phase 1 polish — comptime-only enforcement.** A runtime call to a welded fn is
|
||||
a clean build-gating error (`emitCall` gate, guarded by enclosing-`is_comptime`
|
||||
so `#run`/`::` uses stay green), not a link failure. Example 1185. Build + suite
|
||||
|
||||
@@ -93,6 +93,19 @@ build error — the sx side is a header checked against the implementation). Bec
|
||||
the same compiler builds both, they're guaranteed identical, and a `types.zig`
|
||||
change re-bakes the offsets on the next build — both sides move together.
|
||||
|
||||
> **Implementation note (how it's exact, concretely).** No layout-override engine
|
||||
> is needed. The sx header DECLARES its fields in the compiler type's **memory
|
||||
> order** (Zig may reorder a struct from source order). The compiler REFLECTS the
|
||||
> bound Zig type — field names from `@typeInfo`, offsets from `@offsetOf`, size
|
||||
> from `@sizeOf`, nothing hand-maintained — and VALIDATES the header matches that
|
||||
> memory order, with loud diagnostics on drift (*field not found*, *wrong field
|
||||
> order* + the expected order, *type/layout size mismatch*). On pass the sx
|
||||
> struct's NATURAL layout already equals the Zig layout, so it is an ordinary
|
||||
> struct — no reorder, no padding tricks, no index/remap tables, no special LLVM
|
||||
> path — and `@ptrCast`ing it to the compiler's own type and dereferencing is
|
||||
> byte-identical. When `types.zig` shifts, the header stops matching and the
|
||||
> developer gets a specific message to fix it.
|
||||
|
||||
This is what C-ABI `extern` can't do: it copies Zig's REAL layout, so Zig slices
|
||||
(`{ptr,len}`), field reordering, and `union(enum)` tag placement all "just work" —
|
||||
no slice→ptr+len surgery on `types.zig`, no version fragility.
|
||||
|
||||
30
examples/0627-comptime-weld-struct-reflected-layout.sx
Normal file
30
examples/0627-comptime-weld-struct-reflected-layout.sx
Normal file
@@ -0,0 +1,30 @@
|
||||
// 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);
|
||||
}
|
||||
20
examples/1186-diagnostics-weld-struct-wrong-order.sx
Normal file
20
examples/1186-diagnostics-weld-struct-wrong-order.sx
Normal file
@@ -0,0 +1,20 @@
|
||||
// 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"); }
|
||||
@@ -0,0 +1 @@
|
||||
0
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
name=42 nominal=7 proto=true
|
||||
@@ -1,4 +1,4 @@
|
||||
error: welded type 'Field' has 2 field(s) in the compiler library but the declaration has 1
|
||||
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; }
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
1
|
||||
@@ -0,0 +1,5 @@
|
||||
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;
|
||||
| ^^^
|
||||
@@ -0,0 +1 @@
|
||||
|
||||
@@ -104,66 +104,32 @@ test "compiler_lib: validateStructLayout flags each kind of drift" {
|
||||
}
|
||||
}
|
||||
|
||||
// Lock: `Field` (natural two-u32 layout) has the trivial weld plan — two field
|
||||
// elements in declaration order, no padding, identity remap.
|
||||
test "compiler_lib: weld plan for Field is the identity (no reorder, no pad)" {
|
||||
const alloc = std.testing.allocator;
|
||||
const bt = compiler_lib.findType("Field").?;
|
||||
var plan = try compiler_lib.computeWeldPlan(alloc, bt.fields, bt.size);
|
||||
defer plan.deinit(alloc);
|
||||
|
||||
try std.testing.expectEqual(@as(usize, 8), plan.total_size);
|
||||
try std.testing.expectEqual(@as(usize, 2), plan.elements.len);
|
||||
// Identity remap.
|
||||
try std.testing.expectEqual(@as(usize, 0), plan.sx_to_llvm[0]);
|
||||
try std.testing.expectEqual(@as(usize, 1), plan.sx_to_llvm[1]);
|
||||
// Both elements are real fields at 0 and 4.
|
||||
try std.testing.expectEqual(@as(?usize, 0), plan.elements[0].sx_field);
|
||||
try std.testing.expectEqual(@as(usize, 0), plan.elements[0].offset);
|
||||
try std.testing.expectEqual(@as(?usize, 1), plan.elements[1].sx_field);
|
||||
try std.testing.expectEqual(@as(usize, 4), plan.elements[1].offset);
|
||||
}
|
||||
|
||||
// Lock: `StructInfo` is the first NON-natural layout — Zig reorders it to
|
||||
// (fields@0, name@16, nominal_id@20, is_protocol@24) with a 7-byte alignment
|
||||
// tail. The plan must reproduce exactly that order + the sx→element remap, so the
|
||||
// LLVM type built from it is byte-identical to the Zig type. sx declaration order
|
||||
// is (name, fields, is_protocol, nominal_id) = sx indices 0,1,2,3.
|
||||
test "compiler_lib: weld plan for StructInfo reorders + pads to the Zig layout" {
|
||||
const alloc = std.testing.allocator;
|
||||
// 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").?;
|
||||
var plan = try compiler_lib.computeWeldPlan(alloc, bt.fields, bt.size);
|
||||
defer plan.deinit(alloc);
|
||||
|
||||
try std.testing.expectEqual(@as(usize, 32), plan.total_size);
|
||||
try std.testing.expectEqual(@sizeOf(StructInfoZig), bt.size);
|
||||
try std.testing.expectEqual(@as(usize, 4), bt.fields.len);
|
||||
|
||||
// Elements in ascending offset order: fields, name, nominal_id, is_protocol,
|
||||
// then a trailing 7-byte pad (25 → 32).
|
||||
try std.testing.expectEqual(@as(usize, 5), plan.elements.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);
|
||||
|
||||
// elem 0: fields (sx index 1) @ 0, size 16
|
||||
try std.testing.expectEqual(@as(?usize, 1), plan.elements[0].sx_field);
|
||||
try std.testing.expectEqual(@as(usize, 0), plan.elements[0].offset);
|
||||
try std.testing.expectEqual(@as(usize, 16), plan.elements[0].size);
|
||||
// elem 1: name (sx index 0) @ 16, size 4
|
||||
try std.testing.expectEqual(@as(?usize, 0), plan.elements[1].sx_field);
|
||||
try std.testing.expectEqual(@as(usize, 16), plan.elements[1].offset);
|
||||
// elem 2: nominal_id (sx index 3) @ 20, size 4
|
||||
try std.testing.expectEqual(@as(?usize, 3), plan.elements[2].sx_field);
|
||||
try std.testing.expectEqual(@as(usize, 20), plan.elements[2].offset);
|
||||
// elem 3: is_protocol (sx index 2) @ 24, size 1
|
||||
try std.testing.expectEqual(@as(?usize, 2), plan.elements[3].sx_field);
|
||||
try std.testing.expectEqual(@as(usize, 24), plan.elements[3].offset);
|
||||
// elem 4: trailing pad @ 25, size 7
|
||||
try std.testing.expectEqual(@as(?usize, null), plan.elements[4].sx_field);
|
||||
try std.testing.expectEqual(@as(usize, 25), plan.elements[4].offset);
|
||||
try std.testing.expectEqual(@as(usize, 7), plan.elements[4].size);
|
||||
|
||||
// sx → element remap: name→1, fields→0, is_protocol→3, nominal_id→2.
|
||||
try std.testing.expectEqual(@as(usize, 1), plan.sx_to_llvm[0]);
|
||||
try std.testing.expectEqual(@as(usize, 0), plan.sx_to_llvm[1]);
|
||||
try std.testing.expectEqual(@as(usize, 3), plan.sx_to_llvm[2]);
|
||||
try std.testing.expectEqual(@as(usize, 2), plan.sx_to_llvm[3]);
|
||||
// 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
|
||||
|
||||
@@ -49,26 +49,31 @@ pub const BoundType = struct {
|
||||
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` from a real Zig struct type `T`. Field offsets/sizes come
|
||||
/// from `@offsetOf`/`@sizeOf` on `T`; `sx_field_names` supplies the sx-visible
|
||||
/// names positionally (must match `T`'s field order and count — a mismatch is a
|
||||
/// compile error, never a silent truncation).
|
||||
fn weldStruct(
|
||||
comptime sx_name: []const u8,
|
||||
comptime T: type,
|
||||
comptime sx_field_names: []const []const u8,
|
||||
) BoundType {
|
||||
/// 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;
|
||||
if (zig_fields.len != sx_field_names.len)
|
||||
@compileError("compiler-lib weld '" ++ sx_name ++ "': sx field count != Zig field count");
|
||||
comptime var layouts: [zig_fields.len]FieldLayout = undefined;
|
||||
inline for (zig_fields, 0..) |zf, i| {
|
||||
layouts[i] = .{
|
||||
.name = sx_field_names[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,
|
||||
@@ -78,14 +83,13 @@ fn weldStruct(
|
||||
};
|
||||
}
|
||||
|
||||
/// The welded-type export list. `Field` (two u32s, natural layout) proved the
|
||||
/// weld in Phase 1; `StructInfo` (Phase 2) is the first NON-natural layout —
|
||||
/// Zig reorders its fields (`fields`@0, `name`@16, `nominal_id`@20,
|
||||
/// `is_protocol`@24), so it exercises the offset-override engine. `EnumInfo` /
|
||||
/// `TaggedUnionInfo` / `TupleInfo` join later.
|
||||
/// 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, &.{ "name", "ty" }),
|
||||
weldStruct("StructInfo", StructInfoZig, &.{ "name", "fields", "is_protocol", "nominal_id" }),
|
||||
weldStruct("Field", FieldZig),
|
||||
weldStruct("StructInfo", StructInfoZig),
|
||||
};
|
||||
|
||||
/// Look up a welded type by its sx name. Returns null when the name is not on
|
||||
@@ -150,91 +154,6 @@ pub fn validateStructLayout(
|
||||
return null;
|
||||
}
|
||||
|
||||
// ── Weld plan (byte-layout override) ────────────────────────────────────────
|
||||
//
|
||||
// A welded struct must be laid out byte-identically to the bound Zig type, whose
|
||||
// fields Zig may REORDER (and pad). The sx struct's natural layout generally
|
||||
// won't match — so the compiler imposes the Zig layout: it builds the struct's
|
||||
// LLVM type as the fields in ascending-OFFSET order, with explicit padding
|
||||
// elements filling the gaps, and remaps each sx field index to its LLVM element
|
||||
// index. `computeWeldPlan` is that pure layout math; the LLVM type builder + the
|
||||
// struct-GEP / field-access sites consume the plan (later sub-steps), and the
|
||||
// interp serializes comptime struct Values through the same offsets.
|
||||
|
||||
/// One element of a welded struct's LLVM layout: either a real field (carrying
|
||||
/// its sx field index) or a padding gap. Always in ascending `offset` order.
|
||||
pub const WeldElement = struct {
|
||||
/// The sx field index this element holds, or null for a padding gap.
|
||||
sx_field: ?usize,
|
||||
/// Byte offset of this element within the struct (the bound Zig offset).
|
||||
offset: usize,
|
||||
/// Byte width of this element (the field's size, or the gap width).
|
||||
size: usize,
|
||||
};
|
||||
|
||||
/// The byte-layout plan for a welded struct: its LLVM elements in offset order
|
||||
/// (fields + padding) and the sx-field → LLVM-element-index remap. Owns its
|
||||
/// slices — `deinit` with the same allocator passed to `computeWeldPlan`.
|
||||
pub const WeldPlan = struct {
|
||||
elements: []const WeldElement,
|
||||
/// `sx_to_llvm[i]` is the index into `elements` of sx field `i`.
|
||||
sx_to_llvm: []const usize,
|
||||
total_size: usize,
|
||||
|
||||
pub fn deinit(self: *WeldPlan, alloc: std.mem.Allocator) void {
|
||||
alloc.free(self.elements);
|
||||
alloc.free(self.sx_to_llvm);
|
||||
}
|
||||
};
|
||||
|
||||
/// Compute the byte-layout plan for a struct whose fields carry their bound Zig
|
||||
/// offsets (`fields[i].offset`/`.size`, e.g. from a `BoundType`). `total_size` is
|
||||
/// the bound Zig `@sizeOf`. The result lists LLVM elements in ascending-offset
|
||||
/// order — real fields interleaved with padding gaps — plus the sx-field →
|
||||
/// element-index remap that struct-GEP uses. Pure; allocates the result slices.
|
||||
pub fn computeWeldPlan(
|
||||
alloc: std.mem.Allocator,
|
||||
fields: []const FieldLayout,
|
||||
total_size: usize,
|
||||
) !WeldPlan {
|
||||
// Order the sx field indices by ascending byte offset (stable).
|
||||
const order = try alloc.alloc(usize, fields.len);
|
||||
defer alloc.free(order);
|
||||
for (order, 0..) |*o, i| o.* = i;
|
||||
std.sort.insertion(usize, order, fields, struct {
|
||||
fn lessThan(fs: []const FieldLayout, a: usize, b: usize) bool {
|
||||
return fs[a].offset < fs[b].offset;
|
||||
}
|
||||
}.lessThan);
|
||||
|
||||
var elements = std.ArrayList(WeldElement).empty;
|
||||
errdefer elements.deinit(alloc);
|
||||
const sx_to_llvm = try alloc.alloc(usize, fields.len);
|
||||
errdefer alloc.free(sx_to_llvm);
|
||||
|
||||
var cursor: usize = 0;
|
||||
for (order) |sx_i| {
|
||||
const f = fields[sx_i];
|
||||
// Fill any gap before this field with a padding element.
|
||||
if (f.offset > cursor) {
|
||||
try elements.append(alloc, .{ .sx_field = null, .offset = cursor, .size = f.offset - cursor });
|
||||
}
|
||||
sx_to_llvm[sx_i] = elements.items.len;
|
||||
try elements.append(alloc, .{ .sx_field = sx_i, .offset = f.offset, .size = f.size });
|
||||
cursor = f.offset + f.size;
|
||||
}
|
||||
// Trailing padding up to the bound total size (alignment tail).
|
||||
if (total_size > cursor) {
|
||||
try elements.append(alloc, .{ .sx_field = null, .offset = cursor, .size = total_size - cursor });
|
||||
}
|
||||
|
||||
return .{
|
||||
.elements = try elements.toOwnedSlice(alloc),
|
||||
.sx_to_llvm = sx_to_llvm,
|
||||
.total_size = total_size,
|
||||
};
|
||||
}
|
||||
|
||||
// ── Functions (comptime-only, host-call bridged) ────────────────────────────
|
||||
|
||||
/// A welded `compiler` function: dispatched under the comptime interpreter to its
|
||||
|
||||
@@ -753,14 +753,40 @@ fn validateWeldedStruct(self: *Lowering, sd: *const ast.StructDecl, tid: TypeId,
|
||||
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}' has {d} field(s) in the compiler library but the declaration has {d}", .{ sd.name, m.expected, m.got }),
|
||||
.field_name => |m| diags.addFmt(.err, span, "welded type '{s}' field {d} is named '{s}' in the compiler library, not '{s}'", .{ sd.name, m.index, m.expected, m.got }),
|
||||
.field_size => |m| diags.addFmt(.err, span, "welded type '{s}' field '{s}' is {d} byte(s) in the compiler library but {d} as declared", .{ sd.name, m.name, m.expected, m.got }),
|
||||
.total_size => |m| diags.addFmt(.err, span, "welded type '{s}' is {d} byte(s) in the compiler library but {d} as declared (padding/alignment mismatch)", .{ sd.name, m.expected, m.got }),
|
||||
.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
|
||||
|
||||
Reference in New Issue
Block a user