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

@@ -1347,6 +1347,55 @@ cg_structlit_fill(Cg *c, Local **locals_p, Type *lu, Node *lit,
mode, srcoff, name, disp + (int)foff);
continue;
}
/* 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`. Mirrors #4's receive
* shape at the N_LET / N_ASSIGN call-rhs sites; the
* MOVW-for-tail==2 emission 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 (MOVQ/LEAQ) is safe; any other emission
* added here will silently corrupt the return value. */
if (fu && fu->kind == TY_STRUCT
&& f->lhs && f->lhs->kind == N_CALL
&& fsz <= 24
&& (fsz % 8 == 0 || fsz % 8 == 1
|| fsz % 8 == 2 || fsz % 8 == 4)) {
cgexpr(c, f->lhs, *locals_p);
if (mode == DST_PTR_LOCAL)
ins2(c, A_MOVQ, amem(D_BP, srcoff),
areg(D_BX));
else if (mode == DST_GLOBAL)
ins2(c, A_LEAQ, masym(c, name), areg(D_BX));
int regs[3] = { D_AX, D_DX, D_CX };
int full = fsz / 8;
int tail = fsz % 8;
for (int i = 0; i < full; i++)
ins2(c, A_MOVQ, areg(regs[i]),
amem(base_reg,
disp + (int)foff + i * 8));
if (tail > 0) {
int op = (tail == 4) ? A_MOVL
: (tail == 2) ? A_MOVW : A_MOVB;
ins2(c, op, areg(regs[full]),
amem(base_reg,
disp + (int)foff + full * 8));
}
continue;
}
cgexpr(c, f->lhs, *locals_p);
/* For non-BP modes, cgexpr just clobbered BX; reload it
* before the store. */