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:
agra
2026-06-17 15:45:23 +03:00
parent 88c4cbcfa5
commit 40d075ca98
14 changed files with 230 additions and 218 deletions

View File

@@ -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