`alloc(Outer{ x = Inner{q=10} })` dropped the nested struct-literal field:
the alloc path had its own inline fill loop with only scalar/float/str
arms, so a field whose value is itself an N_STRUCTLIT fell to the scalar
tail and stored MOVQ $0 (cgexpr leaves a whole aggregate in no register)
over the inner slot. Both stages emitted the identical wrong fill, so the
byte-id gate was blind to it.
Route alloc's fill through the existing shared structlit-fill helper (the
one the BP-relative/global/local structlit sites already use -- it handles
nested-struct recursion, N_ARRLIT, str/slice and tagged) via a new 4th
destination mode DST_PTR_SP that reloads the heap base from (SP). This
deletes alloc's divergent inline loop, the lone site lacking the recursion.
As a side effect it also fixes a latent slice-field drop in the driver's
own alloc(sepgraph{...}) (pkg.len/.cap were dropped; the consumer reads
neither -- g.n is the count SSoT). Nested-array fields are closed in-class;
a nested tuple-LITERAL field now errors loudly and symmetrically (the #49
non-addressable gap, previously dropped silently at alloc only).
Surfaced by the codegen miscompile hunt (finding C7c). Pinned by
test/lang/alloc_nested_field_test.ww (nested struct depth 1+2, nested
array, adjacent multi-nested, sibling-no-clobber; reddens on revert).
Routing preservation proven: the whole test/lang corpus is byte-identical
HEAD vs fixed except the new pin; self-compile byte-id (990-996) green.
78 lines
3.4 KiB
Plaintext
78 lines
3.4 KiB
Plaintext
// alloc_nested_field_test — a nested aggregate field VALUE inside an
|
|
// alloc(struct-literal) heap init must survive (C7c, task #6). The
|
|
// alloc(value) field-fill loop had only float/str/scalar arms; a field
|
|
// whose value is itself an N_STRUCTLIT / N_ARRLIT fell to the scalar
|
|
// tail, so cgexpr(inner-literal) left AX=0 and the loop stored $0 over
|
|
// the whole inner slot — silently dropping every inner leaf (byte-id-
|
|
// gate-blind: both stages emitted the same wrong fill). The fix routes
|
|
// the heap fill through the shared cg_structlit_fill / cgstructlitfill
|
|
// helper in DST_PTR_SP mode (base reloaded from the pushed heap ptr at
|
|
// (SP)), which recurses to arbitrary depth.
|
|
//
|
|
// Every row reads the heap struct back via `let o: T = *p` (a whole-
|
|
// struct deref-load to a BP-rel local) so the assertions never traverse
|
|
// the chained-dot-through-heap-ptr read path (a separate pre-existing
|
|
// cs!=ww the C7c fill fix does not touch). Each row asserts the nested
|
|
// leaf VALUE (reddens when the fix is reverted: the dropped leaf reads
|
|
// 0) AND the sibling scalar leaves before/after it (no clobber).
|
|
|
|
package alloc_nested_field_test;
|
|
|
|
type Inner = struct { q: i64 };
|
|
type Outer = struct { x: Inner, y: i64 };
|
|
|
|
type Mid = struct { lo: i64, inner: Inner, hi: i64 };
|
|
type Nest = struct { a: i64, mid: Mid, b: i64 };
|
|
|
|
type WithArr = struct { lead: i64, arr: [3]i64, trail: i64 };
|
|
|
|
type TwoNest = struct { head: i64, s: Inner, arr: [2]i64, tail: i64 };
|
|
|
|
@test fn depth1_nested_struct() void = {
|
|
// Inner{q=10} is the field VALUE of x; pre-fix x.q dropped to 0.
|
|
let p = alloc(Outer{ x = Inner{ q = 10 }, y = 5 })!;
|
|
let o: Outer = *p;
|
|
assert(o.x.q == 10); // the dropped leaf
|
|
assert(o.y == 5); // trailing sibling, must not clobber
|
|
};
|
|
|
|
@test fn depth2_nested_struct() void = {
|
|
// Outer{ mid = Mid{ inner = Inner{q} } } — two levels of nesting.
|
|
// Every inner leaf lands at base + accumulated offset.
|
|
let p = alloc(Nest{ a = 1, mid = Mid{ lo = 2, inner = Inner{ q = 3 }, hi = 4 }, b = 5 })!;
|
|
let o: Nest = *p;
|
|
assert(o.a == 1); // lead sibling
|
|
assert(o.mid.lo == 2); // inner lead sibling
|
|
assert(o.mid.inner.q == 3); // depth-2 dropped leaf
|
|
assert(o.mid.hi == 4); // inner trail sibling
|
|
assert(o.b == 5); // trail sibling
|
|
};
|
|
|
|
@test fn nested_array_field() void = {
|
|
// arr = [10,20,30] is an N_ARRLIT field VALUE; pre-fix every
|
|
// element dropped (scalar tail stored word0 only). lead/trail
|
|
// bracket the array to prove the element stores stay in bounds.
|
|
let p = alloc(WithArr{ lead = 7, arr = [10, 20, 30], trail = 9 })!;
|
|
let o: WithArr = *p;
|
|
assert(o.lead == 7); // lead sibling
|
|
assert(o.arr[0] == 10); // dropped elements
|
|
assert(o.arr[1] == 20);
|
|
assert(o.arr[2] == 30);
|
|
assert(o.trail == 9); // trail sibling
|
|
};
|
|
|
|
@test fn multi_nested_fields() void = {
|
|
// Two ADJACENT nested-aggregate fields (nested STRUCT s, then nested
|
|
// ARRAY arr) bracketed by scalar siblings — proves the heap-base
|
|
// reload after a full nested recursion serves the NEXT nested field's
|
|
// recursion, not only a trailing scalar (every other row follows a
|
|
// nested field with a scalar). pre-fix both s.q and the arr elems drop.
|
|
let p = alloc(TwoNest{ head = 1, s = Inner{ q = 2 }, arr = [3, 4], tail = 5 })!;
|
|
let o: TwoNest = *p;
|
|
assert(o.head == 1); // lead sibling
|
|
assert(o.s.q == 2); // nested-struct leaf
|
|
assert(o.arr[0] == 3); // nested-array elems, after a nested struct
|
|
assert(o.arr[1] == 4);
|
|
assert(o.tail == 5); // trail sibling
|
|
};
|