cstage+selfhost+test: fix nested call-rhs silent zero in structlit fill (3rd of family)

Sister bug to #17 / #18. The structlit-fill helper handled nested
N_STRUCTLIT field values but a struct-typed field whose VALUE is an
N_CALL (call returning a struct, #4 cgreturn ABI) fell through to the
cgexpr-then-AX-store path — landing AX=first qword and silently
dropping DX/CX. For 16B/24B inner returns the trailing 8B/16B stayed
zero (whatever was in the destination slot beforehand).

Fix: a new N_CALL+struct branch in cg_structlit_fill / cgstructlitfill,
placed between the nested-N_STRUCTLIT recursion and the scalar
cgexpr fallthrough. Emits cgexpr -> BX reload (non-BP modes only) ->
MOVQ AX/DX/CX x full + sized tail (MOVL/MOVW/MOVB) per #4's receive
shape.

INVARIANT (commented inline both stages): between cgexpr(N_CALL) and
the AX/DX/CX stores below, no instruction may touch AX/DX/CX. Only
the BX reload (MOVQ srcoff(BP),BX or LEAQ name(SB),BX) is safe.

Sized-tail dispatch is {1->MOVB, 2->MOVW, 4->MOVL, else MOVQ}. Unlike
the scalar fallthrough — which still uses the {1/4/else MOVQ} shape
to stay byte-identical with cstage pending #13 — the new branch is
correctness-by-construction: MOVW for tail==2 only fires on call-rhs
shapes that didn't compile before, and both stages emit it
symmetrically (705's 10B inner row pins this).

Guard `fsz <= 24 && fsz%8 in {0,1,2,4}` mirrors #4's cgreturn ABI:
>24B falls through (sret deferred), and fsz%8 in {3,5,6,7} would need
shift-store — also unsupported by #4. Filed as task #21 (covers both
cgreturn and call-rhs's identical gap).

Two #15 sidesteps, both documented inline:

  1. wwstage's fi.fsz for an inner-struct field is slot-padded
     (8-rounded), not natural — using it would emit 2x MOVQ where
     cstage emits MOVQ+MOVL for a 12B inner. The new wwstage branch
     uses structnaturalsize(csi) to recover the natural size, matching
     cstage's fl->type->size (check.c hands the helper natural sizes).
     This sidesteps #15 without touching its scope.

  2. The outer struct's totsize diverges across stages when
     maxalign<8 (wwstage rounds to 8 universally; cstage to maxalign).
     The 705 test rows pin `x: i64` on the outer to force outer
     maxalign=8, keeping BP offsets stable across stages. Test-side
     sidestep only; also #15 territory.

Files:
  - cmd/w6c/cgen.c              cg_structlit_fill extended
  - selfhost/cmd/wcc/cgenutil.ww  cgstructlitfill mirror
  - selfhost/cmd/{w6c,wwdump}/main.combined.ww  auto-regen
  - test/wcc/705_nested_call_rhs.c  8 rows, table-driven; pins cstage
    exit + wwstage exit + .s byte-identity. Tail widths 0/4/2/1, dst
    modes DST_BP + DST_PTR_LOCAL, shallow + 3-deep.
  - Makefile  705 wiring

Test: 65/65 PASS. 994_w6c_ww + 995_self_rebuild PASS (byte-identity
holds — load-bearing).
This commit is contained in:
2026-05-15 19:07:29 +09:00
parent 99a68a6a57
commit 98460e0220
6 changed files with 763 additions and 0 deletions

View File

@@ -8826,8 +8826,123 @@ fn cgstructlitfill(c: *cgen, si: *structinfo, lit: *node,
};
};
};
// Nested struct-typed CALL value (#20). cgexpr
// leaves AX=bytes[0..7], DX=bytes[8..15], CX=
// bytes[16..23] per #4's cgreturn ABI. Pre-#20
// the cgexpr-then-AX-store fallthrough below
// silently dropped past the first qword for any
// fsz > 8 (only AX got stored).
//
// Sized stores: MOVQ for full 8B chunks plus a
// sized tail (MOVL/MOVW/MOVB) by `tail = fsz%8`.
// Mirror of cstage cg_structlit_fill's #20 branch.
// MOVW-for-tail==2 only fires on shapes that
// didn't compile before, so no #13 byte-identity
// concern.
//
// Guard `fsz <= 24 && fsz%8 ∈ {0,1,2,4}` matches
// #4's cgreturn ABI: >24B falls through (sret
// deferred); fsz%8 ∈ {3,5,6,7} would need shift-
// store and is also unsupported by #4 — falls
// through to the existing AX-only wrongness
// (consistent, tracked as follow-up).
//
// INVARIANT: between cgexpr(N_CALL) and the
// AX/DX/CX stores below, NO instruction may touch
// AX/DX/CX. The BX reload is safe; any other
// emission added here will silently corrupt the
// return value.
let callwhole: bool = false;
if (!nested) {
if (fieldnode.lhs != nil) {
if (fieldnode.lhs.kind == nkind.N_CALL) {
if (fi.tnode != nil) {
if (fi.tnode.kind == nkind.N_TNAME) {
if (primsize(fi.tnode.str) == 0) {
let csi: *structinfo = structlookup(c, fi.tnode.str);
if (csi != nil) {
// Use the inner struct's
// NATURAL size (no 8B slot
// rounding) so MOVL/MOVW/
// MOVB tail dispatch matches
// cstage's fl->type->size
// (which is natural per
// check.c). fi.fsz here is
// wwstage's slot-padded
// totsize — using it would
// emit 2× MOVQ where cstage
// emits MOVQ+MOVL for a
// 12B inner, etc. (task #15
// territory; sidestepped
// locally.)
let cfsz: i32 = structnaturalsize(csi);
let crem: i32 = cfsz - (cfsz / 8) * 8;
if (cfsz <= 24) {
if (crem == 0 || crem == 1
|| crem == 2 || crem == 4) {
cgexpr(c, fieldnode.lhs);
if (mode == 1) {
emitline("\tMOVQ\t");
emitoff(srcoff: i64);
emitline("(BP), BX\n");
};
if (mode == 2) {
emitline("\tLEAQ\t");
emitsymname(c, srcname);
emitline("(SB), BX\n");
};
let full: i32 = cfsz / 8;
let ci: i32 = 0;
for (ci < full) {
let r: str = "AX";
if (ci == 1) { r = "DX"; };
if (ci == 2) { r = "CX"; };
emitline("\tMOVQ\t");
emitline(r);
emitline(", ");
if (mode == 0) {
emitoff((disp + fi.foff + ci * 8): i64);
emitline("(BP)\n");
} else {
emitdispreg((disp + fi.foff + ci * 8): i64, basereg);
emitline("\n");
};
ci += 1;
};
if (crem > 0) {
let top: str = "MOVB";
if (crem == 4) { top = "MOVL"; };
if (crem == 2) { top = "MOVW"; };
let tr: str = "AX";
if (full == 1) { tr = "DX"; };
if (full == 2) { tr = "CX"; };
emitline("\t");
emitline(top);
emitline("\t");
emitline(tr);
emitline(", ");
if (mode == 0) {
emitoff((disp + fi.foff + full * 8): i64);
emitline("(BP)\n");
} else {
emitdispreg((disp + fi.foff + full * 8): i64, basereg);
emitline("\n");
};
};
callwhole = true;
};
};
};
};
};
};
};
};
};
if (nested) {
fi = nil;
} else if (callwhole) {
fi = nil;
} else {
cgexpr(c, fieldnode.lhs);
// For non-BP modes, cgexpr just clobbered

View File

@@ -2954,8 +2954,123 @@ fn cgstructlitfill(c: *cgen, si: *structinfo, lit: *node,
};
};
};
// Nested struct-typed CALL value (#20). cgexpr
// leaves AX=bytes[0..7], DX=bytes[8..15], CX=
// bytes[16..23] per #4's cgreturn ABI. Pre-#20
// the cgexpr-then-AX-store fallthrough below
// silently dropped past the first qword for any
// fsz > 8 (only AX got stored).
//
// Sized stores: MOVQ for full 8B chunks plus a
// sized tail (MOVL/MOVW/MOVB) by `tail = fsz%8`.
// Mirror of cstage cg_structlit_fill's #20 branch.
// MOVW-for-tail==2 only fires on shapes that
// didn't compile before, so no #13 byte-identity
// concern.
//
// Guard `fsz <= 24 && fsz%8 ∈ {0,1,2,4}` matches
// #4's cgreturn ABI: >24B falls through (sret
// deferred); fsz%8 ∈ {3,5,6,7} would need shift-
// store and is also unsupported by #4 — falls
// through to the existing AX-only wrongness
// (consistent, tracked as follow-up).
//
// INVARIANT: between cgexpr(N_CALL) and the
// AX/DX/CX stores below, NO instruction may touch
// AX/DX/CX. The BX reload is safe; any other
// emission added here will silently corrupt the
// return value.
let callwhole: bool = false;
if (!nested) {
if (fieldnode.lhs != nil) {
if (fieldnode.lhs.kind == nkind.N_CALL) {
if (fi.tnode != nil) {
if (fi.tnode.kind == nkind.N_TNAME) {
if (primsize(fi.tnode.str) == 0) {
let csi: *structinfo = structlookup(c, fi.tnode.str);
if (csi != nil) {
// Use the inner struct's
// NATURAL size (no 8B slot
// rounding) so MOVL/MOVW/
// MOVB tail dispatch matches
// cstage's fl->type->size
// (which is natural per
// check.c). fi.fsz here is
// wwstage's slot-padded
// totsize — using it would
// emit 2× MOVQ where cstage
// emits MOVQ+MOVL for a
// 12B inner, etc. (task #15
// territory; sidestepped
// locally.)
let cfsz: i32 = structnaturalsize(csi);
let crem: i32 = cfsz - (cfsz / 8) * 8;
if (cfsz <= 24) {
if (crem == 0 || crem == 1
|| crem == 2 || crem == 4) {
cgexpr(c, fieldnode.lhs);
if (mode == 1) {
emitline("\tMOVQ\t");
emitoff(srcoff: i64);
emitline("(BP), BX\n");
};
if (mode == 2) {
emitline("\tLEAQ\t");
emitsymname(c, srcname);
emitline("(SB), BX\n");
};
let full: i32 = cfsz / 8;
let ci: i32 = 0;
for (ci < full) {
let r: str = "AX";
if (ci == 1) { r = "DX"; };
if (ci == 2) { r = "CX"; };
emitline("\tMOVQ\t");
emitline(r);
emitline(", ");
if (mode == 0) {
emitoff((disp + fi.foff + ci * 8): i64);
emitline("(BP)\n");
} else {
emitdispreg((disp + fi.foff + ci * 8): i64, basereg);
emitline("\n");
};
ci += 1;
};
if (crem > 0) {
let top: str = "MOVB";
if (crem == 4) { top = "MOVL"; };
if (crem == 2) { top = "MOVW"; };
let tr: str = "AX";
if (full == 1) { tr = "DX"; };
if (full == 2) { tr = "CX"; };
emitline("\t");
emitline(top);
emitline("\t");
emitline(tr);
emitline(", ");
if (mode == 0) {
emitoff((disp + fi.foff + full * 8): i64);
emitline("(BP)\n");
} else {
emitdispreg((disp + fi.foff + full * 8): i64, basereg);
emitline("\n");
};
};
callwhole = true;
};
};
};
};
};
};
};
};
};
if (nested) {
fi = nil;
} else if (callwhole) {
fi = nil;
} else {
cgexpr(c, fieldnode.lhs);
// For non-BP modes, cgexpr just clobbered

View File

@@ -8826,8 +8826,123 @@ fn cgstructlitfill(c: *cgen, si: *structinfo, lit: *node,
};
};
};
// Nested struct-typed CALL value (#20). cgexpr
// leaves AX=bytes[0..7], DX=bytes[8..15], CX=
// bytes[16..23] per #4's cgreturn ABI. Pre-#20
// the cgexpr-then-AX-store fallthrough below
// silently dropped past the first qword for any
// fsz > 8 (only AX got stored).
//
// Sized stores: MOVQ for full 8B chunks plus a
// sized tail (MOVL/MOVW/MOVB) by `tail = fsz%8`.
// Mirror of cstage cg_structlit_fill's #20 branch.
// MOVW-for-tail==2 only fires on shapes that
// didn't compile before, so no #13 byte-identity
// concern.
//
// Guard `fsz <= 24 && fsz%8 ∈ {0,1,2,4}` matches
// #4's cgreturn ABI: >24B falls through (sret
// deferred); fsz%8 ∈ {3,5,6,7} would need shift-
// store and is also unsupported by #4 — falls
// through to the existing AX-only wrongness
// (consistent, tracked as follow-up).
//
// INVARIANT: between cgexpr(N_CALL) and the
// AX/DX/CX stores below, NO instruction may touch
// AX/DX/CX. The BX reload is safe; any other
// emission added here will silently corrupt the
// return value.
let callwhole: bool = false;
if (!nested) {
if (fieldnode.lhs != nil) {
if (fieldnode.lhs.kind == nkind.N_CALL) {
if (fi.tnode != nil) {
if (fi.tnode.kind == nkind.N_TNAME) {
if (primsize(fi.tnode.str) == 0) {
let csi: *structinfo = structlookup(c, fi.tnode.str);
if (csi != nil) {
// Use the inner struct's
// NATURAL size (no 8B slot
// rounding) so MOVL/MOVW/
// MOVB tail dispatch matches
// cstage's fl->type->size
// (which is natural per
// check.c). fi.fsz here is
// wwstage's slot-padded
// totsize — using it would
// emit 2× MOVQ where cstage
// emits MOVQ+MOVL for a
// 12B inner, etc. (task #15
// territory; sidestepped
// locally.)
let cfsz: i32 = structnaturalsize(csi);
let crem: i32 = cfsz - (cfsz / 8) * 8;
if (cfsz <= 24) {
if (crem == 0 || crem == 1
|| crem == 2 || crem == 4) {
cgexpr(c, fieldnode.lhs);
if (mode == 1) {
emitline("\tMOVQ\t");
emitoff(srcoff: i64);
emitline("(BP), BX\n");
};
if (mode == 2) {
emitline("\tLEAQ\t");
emitsymname(c, srcname);
emitline("(SB), BX\n");
};
let full: i32 = cfsz / 8;
let ci: i32 = 0;
for (ci < full) {
let r: str = "AX";
if (ci == 1) { r = "DX"; };
if (ci == 2) { r = "CX"; };
emitline("\tMOVQ\t");
emitline(r);
emitline(", ");
if (mode == 0) {
emitoff((disp + fi.foff + ci * 8): i64);
emitline("(BP)\n");
} else {
emitdispreg((disp + fi.foff + ci * 8): i64, basereg);
emitline("\n");
};
ci += 1;
};
if (crem > 0) {
let top: str = "MOVB";
if (crem == 4) { top = "MOVL"; };
if (crem == 2) { top = "MOVW"; };
let tr: str = "AX";
if (full == 1) { tr = "DX"; };
if (full == 2) { tr = "CX"; };
emitline("\t");
emitline(top);
emitline("\t");
emitline(tr);
emitline(", ");
if (mode == 0) {
emitoff((disp + fi.foff + full * 8): i64);
emitline("(BP)\n");
} else {
emitdispreg((disp + fi.foff + full * 8): i64, basereg);
emitline("\n");
};
};
callwhole = true;
};
};
};
};
};
};
};
};
};
if (nested) {
fi = nil;
} else if (callwhole) {
fi = nil;
} else {
cgexpr(c, fieldnode.lhs);
// For non-BP modes, cgexpr just clobbered