cstage+selfhost+test: extend structlit-fill helper to N_ASSIGN N_DOT lhs (4 flavors)

Sister fix to #17. The BP-rel helper from #17 covered N_LET /
N_ASSIGN N_IDENT-lhs / N_RETURN; the four N_ASSIGN N_DOT-lhs
structlit walks still went through the inline `cgexpr(field.lhs);
store-AX-sized` shape and silently dropped trailing bytes when a
struct-typed field's value was itself an N_STRUCTLIT. Affected dot
flavors: single-dot via_ptr / global / BP-rel and the chained-dot
walker (depth >= 2, all three root flavors).

Extend `cg_structlit_fill_bp` / `cgstructlitfillbp` into
`cg_structlit_fill` / `cgstructlitfill` taking a destination mode
(DST_BP / DST_PTR_LOCAL / DST_GLOBAL = 0/1/2), srcoff (PTR_LOCAL),
srcname (GLOBAL), and disp accumulator. `disp` grows by foff on
descent; srcoff/srcname stay constant across the call tree. The
pre-#17 wrappers are preserved byte-identically by delegating with
mode=DST_BP — 995_self_rebuild byte-identity holds for the no-
nested-STRUCTLIT case that selfhost source actually uses.

The non-BP modes reload BX before the ELLIPSIS zero-fill loop AND
before every field store (tagged, scalar, and the cgexpr leaf).
This is correctness-by-construction — cgexpr clobbers BX between
fields, and the redundant reload only fires on shapes that didn't
compile before. The four dot-flavor sites in each stage now compute
their dst mode + disp and call the shared helper (reducing each
from ~80-130 inline lines to ~5-12 lines of dispatch).

Stage signature asymmetry: cstage threads Local** for cgexpr; ww-
stage takes explicit totsize because #15 (split totsize into
naturalsize + slotsize) is still pending and the dot sites need
structnaturalsize while the BP-rel sites need si.totsize. Both
asymmetries are documented in the helper docstrings.

704 covers 8 rows (24 checks: 8 cstage exits, 8 wwstage exits, 8
cstage-vs-wwstage .s byte-identity diffs): 6 dst-flavors (single-
dot local/ptr/global, chained-dot local/ptr/global) plus single-
local 3-deep and single-ptr 3-deep to pin disp threading through
the helper's recursion and through DST_PTR_LOCAL BX reloads.

The nested struct-typed CALL rhs in field-walks has the same shape
as the STRUCTLIT bug fixed here but the helper only handles
STRUCTLIT — tracked as task #20.
This commit is contained in:
2026-05-15 18:45:21 +09:00
parent 9d03e02881
commit 99a68a6a57
7 changed files with 1070 additions and 1450 deletions

View File

@@ -3838,6 +3838,11 @@ fn cgassign(c: *cgen, n: *node) void = {
};
};
};
// #18: delegate to cgstructlitfill so a nested struct-
// typed structlit value recurses instead of dropping
// its trailing bytes. mode=1 (DST_PTR_LOCAL) reloads BX
// from lc.off(BP) before zero-fill and before every
// field store.
if (n.op == tkind.TK_ASSIGN
&& n.rhs != nil
&& n.rhs.kind == nkind.N_STRUCTLIT
@@ -3847,75 +3852,8 @@ fn cgassign(c: *cgen, n: *node) void = {
let ssi: *structinfo = structlookup(c, fi.tnode.str);
if (ssi != nil) {
let ssz: i32 = structnaturalsize(ssi);
if (n.rhs.op == tkind.TK_ELLIPSIS) {
emitline("\tXORQ\tAX, AX\n");
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), BX\n");
let zi: i32 = 0;
for (zi + 8 <= ssz) {
emitline("\tMOVQ\tAX, ");
emitdispreg((fi.foff + zi): i64, "BX");
emitline("\n");
zi += 8;
};
for (zi + 4 <= ssz) {
emitline("\tMOVL\tAX, ");
emitdispreg((fi.foff + zi): i64, "BX");
emitline("\n");
zi += 4;
};
for (zi < ssz) {
emitline("\tMOVB\tAX, ");
emitdispreg((fi.foff + zi): i64, "BX");
emitline("\n");
zi += 1;
};
};
let fldn: *node = n.rhs.list;
for (fldn != nil) {
if (fldn.kind == nkind.N_FIELD) {
let fnm: str = fldn.str;
let ifi: *fieldinfo = ssi.fields;
for (ifi != nil) {
if (streq(ifi.fname, fnm)) {
cgexpr(c, fldn.lhs);
if (isfloattype(c, ifi.tnode)) {
let mov: str = "MOVSD";
if (isf32type(c, ifi.tnode)) {
mov = "MOVSS";
};
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), BX\n");
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitdispreg((fi.foff + ifi.foff): i64, "BX");
emitline("\n");
ifi = nil;
} else {
let ifsz: i32 = ifi.fsz;
let op: str = "MOVQ";
if (ifsz == 1) { op = "MOVB"; };
if (ifsz == 4) { op = "MOVL"; };
emitline("\tMOVQ\t");
emitoff(lc.off: i64);
emitline("(BP), BX\n");
emitline("\t");
emitline(op);
emitline("\tAX, ");
emitdispreg((fi.foff + ifi.foff): i64, "BX");
emitline("\n");
ifi = nil;
};
} else {
ifi = ifi.finext;
};
};
};
fldn = fldn.next;
};
cgstructlitfill(c, ssi, n.rhs, 1, lc.off, "",
fi.foff, ssz);
return;
};
};
@@ -4131,6 +4069,10 @@ fn cgassign(c: *cgen, n: *node) void = {
};
};
};
// #18: delegate to cgstructlitfill so a nested struct-
// typed structlit value recurses instead of dropping
// its trailing bytes. mode=0 (DST_BP) — direct BP-rel,
// no BX reload.
if (n.op == tkind.TK_ASSIGN
&& n.rhs != nil
&& n.rhs.kind == nkind.N_STRUCTLIT
@@ -4140,66 +4082,8 @@ fn cgassign(c: *cgen, n: *node) void = {
let ssi: *structinfo = structlookup(c, fi.tnode.str);
if (ssi != nil) {
let ssz: i32 = structnaturalsize(ssi);
if (n.rhs.op == tkind.TK_ELLIPSIS) {
emitline("\tXORQ\tAX, AX\n");
let zi: i32 = 0;
for (zi + 8 <= ssz) {
emitline("\tMOVQ\tAX, ");
emitoff((lc.off + fi.foff + zi): i64);
emitline("(BP)\n");
zi += 8;
};
for (zi + 4 <= ssz) {
emitline("\tMOVL\tAX, ");
emitoff((lc.off + fi.foff + zi): i64);
emitline("(BP)\n");
zi += 4;
};
for (zi < ssz) {
emitline("\tMOVB\tAX, ");
emitoff((lc.off + fi.foff + zi): i64);
emitline("(BP)\n");
zi += 1;
};
};
let fldn: *node = n.rhs.list;
for (fldn != nil) {
if (fldn.kind == nkind.N_FIELD) {
let fnm: str = fldn.str;
let ifi: *fieldinfo = ssi.fields;
for (ifi != nil) {
if (streq(ifi.fname, fnm)) {
cgexpr(c, fldn.lhs);
if (isfloattype(c, ifi.tnode)) {
let mov: str = "MOVSD";
if (isf32type(c, ifi.tnode)) {
mov = "MOVSS";
};
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitoff((lc.off + fi.foff + ifi.foff): i64);
emitline("(BP)\n");
ifi = nil;
} else {
let ifsz: i32 = ifi.fsz;
let op: str = "MOVQ";
if (ifsz == 1) { op = "MOVB"; };
if (ifsz == 4) { op = "MOVL"; };
emitline("\t");
emitline(op);
emitline("\tAX, ");
emitoff((lc.off + fi.foff + ifi.foff): i64);
emitline("(BP)\n");
ifi = nil;
};
} else {
ifi = ifi.finext;
};
};
};
fldn = fldn.next;
};
cgstructlitfill(c, ssi, n.rhs, 0, 0, "",
lc.off + fi.foff, ssz);
return;
};
};
@@ -4438,87 +4322,25 @@ fn cgassign(c: *cgen, n: *node) void = {
};
};
};
if (n.op == tkind.TK_ASSIGN
&& n.rhs != nil
&& n.rhs.kind == nkind.N_STRUCTLIT
&& fi.tnode != nil
&& fi.tnode.kind == nkind.N_TNAME
&& primsize(fi.tnode.str) == 0) {
let ssi: *structinfo = structlookup(c, fi.tnode.str);
if (ssi != nil) {
let ssz: i32 = structnaturalsize(ssi);
if (n.rhs.op == tkind.TK_ELLIPSIS) {
emitline("\tXORQ\tAX, AX\n");
emitline("\tLEAQ\t");
emitsymname(c, bn);
emitline("(SB), BX\n");
let zi: i32 = 0;
for (zi + 8 <= ssz) {
emitline("\tMOVQ\tAX, ");
emitdispreg((fi.foff + zi): i64, "BX");
emitline("\n");
zi += 8;
};
for (zi + 4 <= ssz) {
emitline("\tMOVL\tAX, ");
emitdispreg((fi.foff + zi): i64, "BX");
emitline("\n");
zi += 4;
};
for (zi < ssz) {
emitline("\tMOVB\tAX, ");
emitdispreg((fi.foff + zi): i64, "BX");
emitline("\n");
zi += 1;
};
};
let fldn: *node = n.rhs.list;
for (fldn != nil) {
if (fldn.kind == nkind.N_FIELD) {
let fnm: str = fldn.str;
let ifi: *fieldinfo = ssi.fields;
for (ifi != nil) {
if (streq(ifi.fname, fnm)) {
cgexpr(c, fldn.lhs);
if (isfloattype(c, ifi.tnode)) {
let mov: str = "MOVSD";
if (isf32type(c, ifi.tnode)) {
mov = "MOVSS";
};
emitline("\tLEAQ\t");
emitsymname(c, bn);
emitline("(SB), BX\n");
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitdispreg((fi.foff + ifi.foff): i64, "BX");
emitline("\n");
ifi = nil;
} else {
let ifsz: i32 = ifi.fsz;
let op: str = "MOVQ";
if (ifsz == 1) { op = "MOVB"; };
if (ifsz == 4) { op = "MOVL"; };
emitline("\tLEAQ\t");
emitsymname(c, bn);
emitline("(SB), BX\n");
emitline("\t");
emitline(op);
emitline("\tAX, ");
emitdispreg((fi.foff + ifi.foff): i64, "BX");
emitline("\n");
ifi = nil;
};
} else {
ifi = ifi.finext;
};
};
};
fldn = fldn.next;
};
return;
};
// #18: delegate to cgstructlitfill so a nested struct-
// typed structlit value recurses instead of dropping
// its trailing bytes. mode=2 (DST_GLOBAL) reloads BX
// via LEAQ bn(SB) before zero-fill and before every
// field store.
if (n.op == tkind.TK_ASSIGN
&& n.rhs != nil
&& n.rhs.kind == nkind.N_STRUCTLIT
&& fi.tnode != nil
&& fi.tnode.kind == nkind.N_TNAME
&& primsize(fi.tnode.str) == 0) {
let ssi: *structinfo = structlookup(c, fi.tnode.str);
if (ssi != nil) {
let ssz: i32 = structnaturalsize(ssi);
cgstructlitfill(c, ssi, n.rhs, 2, 0, bn,
fi.foff, ssz);
return;
};
};
if (n.op == tkind.TK_ASSIGN
&& n.rhs != nil
&& n.rhs.kind == nkind.N_IDENT
@@ -4897,6 +4719,14 @@ fn cgassign(c: *cgen, n: *node) void = {
};
};
};
// #18: delegate to cgstructlitfill so a nested struct-
// typed structlit value recurses instead of dropping
// its trailing bytes. mode picks the dst flavor:
// ptrroot → mode=1 (DST_PTR_LOCAL), reload BX from
// rootoff(BP).
// isglobal → mode=2 (DST_GLOBAL), reload BX via
// LEAQ rootname(SB).
// else → mode=0 (DST_BP), direct BP-rel, no reload.
if (n.rhs != nil
&& n.rhs.kind == nkind.N_STRUCTLIT
&& leaffi.tnode != nil
@@ -4907,131 +4737,19 @@ fn cgassign(c: *cgen, n: *node) void = {
// si.totsize is slot-padded (rounded to 8);
// receive ABI needs the TYPE's natural size.
let lsz: i32 = structnaturalsize(lsi);
if (n.rhs.op == tkind.TK_ELLIPSIS) {
emitline("\tXORQ\tAX, AX\n");
if (viacx) {
if (ptrroot) {
emitline("\tMOVQ\t");
emitoff(rootoff: i64);
emitline("(BP), BX\n");
} else {
emitline("\tLEAQ\t");
emitsymname(c, rootname);
emitline("(SB), BX\n");
};
let zi: i32 = 0;
for (zi + 8 <= lsz) {
emitline("\tMOVQ\tAX, ");
emitdispreg((totaloff + zi): i64, "BX");
emitline("\n");
zi += 8;
};
for (zi + 4 <= lsz) {
emitline("\tMOVL\tAX, ");
emitdispreg((totaloff + zi): i64, "BX");
emitline("\n");
zi += 4;
};
for (zi < lsz) {
emitline("\tMOVB\tAX, ");
emitdispreg((totaloff + zi): i64, "BX");
emitline("\n");
zi += 1;
};
} else {
let zi: i32 = 0;
for (zi + 8 <= lsz) {
emitline("\tMOVQ\tAX, ");
emitoff((rootoff + totaloff + zi): i64);
emitline("(BP)\n");
zi += 8;
};
for (zi + 4 <= lsz) {
emitline("\tMOVL\tAX, ");
emitoff((rootoff + totaloff + zi): i64);
emitline("(BP)\n");
zi += 4;
};
for (zi < lsz) {
emitline("\tMOVB\tAX, ");
emitoff((rootoff + totaloff + zi): i64);
emitline("(BP)\n");
zi += 1;
};
};
let dmode: i32 = 0;
let ddisp: i32 = rootoff + totaloff;
if (ptrroot) {
dmode = 1;
ddisp = totaloff;
};
let fldn: *node = n.rhs.list;
for (fldn != nil) {
if (fldn.kind == nkind.N_FIELD) {
let fnm: str = fldn.str;
let fi: *fieldinfo = lsi.fields;
for (fi != nil) {
if (streq(fi.fname, fnm)) {
cgexpr(c, fldn.lhs);
if (isfloattype(c, fi.tnode)) {
let mov: str = "MOVSD";
if (isf32type(c, fi.tnode)) {
mov = "MOVSS";
};
if (viacx) {
if (ptrroot) {
emitline("\tMOVQ\t");
emitoff(rootoff: i64);
emitline("(BP), BX\n");
} else {
emitline("\tLEAQ\t");
emitsymname(c, rootname);
emitline("(SB), BX\n");
};
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitdispreg((totaloff + fi.foff): i64, "BX");
emitline("\n");
} else {
emitline("\t");
emitline(mov);
emitline("\tX0, ");
emitoff((rootoff + totaloff + fi.foff): i64);
emitline("(BP)\n");
};
fi = nil;
} else {
let fsz: i32 = fi.fsz;
let op: str = "MOVQ";
if (fsz == 1) { op = "MOVB"; };
if (fsz == 4) { op = "MOVL"; };
if (viacx) {
if (ptrroot) {
emitline("\tMOVQ\t");
emitoff(rootoff: i64);
emitline("(BP), BX\n");
} else {
emitline("\tLEAQ\t");
emitsymname(c, rootname);
emitline("(SB), BX\n");
};
emitline("\t");
emitline(op);
emitline("\tAX, ");
emitdispreg((totaloff + fi.foff): i64, "BX");
emitline("\n");
} else {
emitline("\t");
emitline(op);
emitline("\tAX, ");
emitoff((rootoff + totaloff + fi.foff): i64);
emitline("(BP)\n");
};
fi = nil;
};
} else {
fi = fi.finext;
};
};
};
fldn = fldn.next;
if (isglobal) {
dmode = 2;
ddisp = totaloff;
};
cgstructlitfill(c, lsi, n.rhs,
dmode, rootoff, rootname,
ddisp, lsz);
return;
};
};