cgen: fix nested aggregate field dropped in alloc-heap structlit fill (C7c)

`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.
This commit is contained in:
2026-06-27 14:16:20 +09:00
parent d152f0d744
commit a52b1aa1d9
4 changed files with 168 additions and 98 deletions

View File

@@ -5911,60 +5911,19 @@ fn cgalloc(c: *cgen, n: *syntax.node) void = {
emitline("\tPUSHQ\tAX\n");
if (v.kind == syntax.nkind.N_STRUCTLIT) {
if (si != nil) {
let f: *syntax.node = v.list;
for (f != nil) {
if (f.kind == syntax.nkind.N_FIELD) {
let fname: str = f.str;
let fi: *fieldinfo = si.fields;
for (fi != nil) {
let fn_: str = fi.fname;
if (syntax.streq(fn_, fname)) {
cgexpr(c, f.lhs);
// alloc(T{ fval = v }) for f64/f32 field: cgexpr left
// the value in X0, not AX — route the store via MOVSD/MOVSS.
if (isfloattype(c, fi.tnode)) {
let mov: str = "MOVSD";
if (isf32type(c, fi.tnode)) { mov = "MOVSS"; };
emitline("\tMOVQ\t(SP), BX\n");
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitdispreg(fi.foff: i64, "BX");
emitline("\n");
fi = nil;
} else { if (isstrtype(c, fi.tnode)) {
// str IS []u8: cgexpr leaves (AX=ptr,
// BX=len, CX=cap). Route the heap base
// through DX so all three survive — CX
// holds cap, BX holds len (#1/Phase 3).
emitline("\tMOVQ\t(SP), DX\n");
emitline("\tMOVQ\tAX, ");
emitdispreg(fi.foff: i64, "DX");
emitline("\n");
emitline("\tMOVQ\tBX, ");
emitdispreg((fi.foff + 8): i64, "DX");
emitline("\n");
emitline("\tMOVQ\tCX, ");
emitdispreg((fi.foff + 16): i64, "DX");
emitline("\n");
fi = nil;
} else {
emitline("\tMOVQ\t(SP), BX\n");
let sop: str = fieldstoreop(c, fi);
emitline("\t");
emitline(sop);
emitline("\tAX, ");
emitdispreg(fi.foff: i64, "BX");
emitline("\n");
fi = nil;
};};
} else {
fi = fi.finext;
};
};
};
f = f.next;
};
// C7c: route the heap field-fill through the shared
// structlit helper in mode 3 (DST_PTR_SP) — base reloaded
// from the just-pushed heap ptr at (SP). The prior inline
// loop had only float/str/scalar arms, so a nested
// struct/array/tuple field VALUE (an inner N_STRUCTLIT /
// N_ARRLIT) fell to the scalar tail and stored AX=0 over
// the whole inner slot, dropping its leaves. The helper
// recurses to arbitrary depth and reuses the
// tagged/call/str/slice/array/agg arms, closing the class
// symmetrically with cstage's DST_PTR_SP arm. Byte-id to
// the old inline loop for the float/str/scalar fields the
// corpus actually allocs (same (SP) reload, disp=0).
cgstructlitfill(c, si, v, 3, 0, "", 0);
};
} else {
// #57 (retained cs!=ww, deferred): scalar/ptr alloc keeps the