selfhost+test: decompose user-struct by-value params (#11)

wwstage param-slot allocator dispatched isfloat/istagged/isslice/
isstr/catch-all and skipped TY_STRUCT. `fn(a: S, b: S)` where S is
16B emitted $16 frame (DI/SI only); cstage emits $32 (DI/SI/DX/CX)
per SysV ABI.

Two-site fix mirroring cmd/w6c/cgen.c:6820 (callee prologue) and
:4240 (caller push):

- New structparamsize(c, t) helper in cgenutil.ww resolves the
  TY_STRUCT TNAME chain, returns totsize for sizes (0,16], else 0.
  >16B drops to stack — bug-compat with cstage's <=16 gate.
- New struct arm in cgfnparams + matching cgfn pre-scan in
  cgendecl.ww. nw = (size>8) ? 2 : 1; partial-fit stitch (idx=5
  + nw=2) emits one reg + one stack tail.
- New struct branch in pushargsrev N_IDENT arm: MOVQ + PUSHQ
  high→low so cgcall's existing pop drains correctly.

Test 717: 4 rows × {cstage, wwstage, asm-id}. Headline 2×16B,
mixed 16B+8B (caller-side surface), str+struct regression guard,
partial-fit 5×i64+16B stitch.
This commit is contained in:
2026-05-17 00:27:22 +09:00
parent f4176b8749
commit 69a817f0f3
6 changed files with 662 additions and 39 deletions

View File

@@ -6667,6 +6667,32 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = {
};
return rest + nwords;
};
// By-value struct ident: load qword(s) from the slot
// and push high → low so left-to-right pop on the
// callee side lands word 0 / word 1 into the SysV arg
// register pair. Mirrors cstage cgen.c §4240 (call
// site) so the wwstage prologue's new struct spill arm
// (cgendecl.ww structparamsize branch) sees the same
// reg layout. Pre-#11 the call-site fell through to
// `cgexpr(c, arg)` + scalar PUSHQ AX — only the first
// 8B word made it across, and the callee's second-arg
// slots picked up the wrong neighbour's value.
let stsz: i32 = structparamsize(c, lc.tnode);
if (stsz > 0) {
if (stsz > 8) {
emitline("\tMOVQ\t");
emitoff((off + 8): i64);
emitline("(BP), AX\n");
emitline("\tPUSHQ\tAX\n");
};
emitline("\tMOVQ\t");
emitoff(off: i64);
emitline("(BP), AX\n");
emitline("\tPUSHQ\tAX\n");
let nw: i32 = 1;
if (stsz > 8) { nw = 2; };
return rest + nw;
};
};
};
// Float arg: cgexpr leaves the value in X0. Push 8 bytes from
@@ -8259,6 +8285,30 @@ fn matchspillsz(c: *cgen, scrutt: *node) i32 = {
return sz;
};
// structparamsize — bytes occupied by a user-defined by-value struct
// param if it fits in 1-2 SysV integer eightbytes (cstage cgen.c
// struct_arg_size mirror; gates on size <= 16). Returns 0 for non-
// struct types or oversized structs so callers can fall through to
// other dispatch arms. Pre-#11 the wwstage prologue had no struct
// branch — user-defined struct params dropped through to the 8B
// scalar catch-all, the second-half value registers (DX/CX) were
// never spilled, and field reads from the under-allocated slot
// trailed into the saved-BP word.
fn structparamsize(c: *cgen, t: *node) i32 = {
if (c == nil) { return 0; };
let r: *node = resolvetype(c, t);
if (r == nil) { return 0; };
if (r.kind != nkind.N_TNAME) { return 0; };
let nm: str = r.str;
if (streq(nm, "str")) { return 0; };
if (primsize(nm) > 0) { return 0; };
let si: *structinfo = structlookup(c, nm);
if (si == nil) { return 0; };
if (si.totsize <= 0) { return 0; };
if (si.totsize > 16) { return 0; };
return si.totsize;
};
// istaggedtype — alias-aware. Mirrors isstrtype: follow N_TNAME to its
// underlying decl, then unwrap a leading N_TBANG so `type error =
// !(invalid | overflow);` is still recognised as tagged. Without the
@@ -17206,18 +17256,66 @@ fn cgfnparams(c: *cgen, params: *node) void = {
localaddstack(c, nm, p.lhs, 16 + stkcursor*8);
stkcursor += 2;
};};
} else {
if (idx < 6) {
let off: i32 = localadd(c, nm, 8, p.lhs);
emitline("\tMOVQ\t");
emitline(argregname(idx));
emitline(", ");
emitoff(off: i64);
emitline("(BP)\n");
idx += 1;
} else { let stsz: i32 = structparamsize(c, p.lhs);
if (stsz > 0) {
// User-defined by-value struct ≤ 16B: 1 or 2
// integer eightbytes. Mirrors cstage's
// `struct_eb = (pu->size > 8) ? 2 : 1` and the
// matching reg/stack/stitch arms in cgen.c cgfn.
let nw: i32 = 1;
if (stsz > 8) { nw = 2; };
if (idx + nw <= 6) {
let off: i32 = localadd(c, nm, stsz, p.lhs);
let w: i32 = 0;
for (w < nw) {
emitline("\tMOVQ\t");
emitline(argregname(idx));
emitline(", ");
emitoff((off + w*8): i64);
emitline("(BP)\n");
idx += 1;
w += 1;
};
} else { if (idx < 6 && nw > 1) {
let off: i32 = localadd(c, nm, stsz, p.lhs);
let regs_left: i32 = 6 - idx;
let w: i32 = 0;
for (w < regs_left) {
emitline("\tMOVQ\t");
emitline(argregname(idx));
emitline(", ");
emitoff((off + w*8): i64);
emitline("(BP)\n");
idx += 1;
w += 1;
};
for (w < nw) {
emitline("\tMOVQ\t");
emitoff((16 + stkcursor*8): i64);
emitline("(BP), AX\n");
emitline("\tMOVQ\tAX, ");
emitoff((off + w*8): i64);
emitline("(BP)\n");
stkcursor += 1;
w += 1;
};
} else {
localaddstack(c, nm, p.lhs, 16 + stkcursor*8);
stkcursor += nw;
};};
} else {
localaddstack(c, nm, p.lhs, 16 + stkcursor*8);
stkcursor += 1;
if (idx < 6) {
let off: i32 = localadd(c, nm, 8, p.lhs);
emitline("\tMOVQ\t");
emitline(argregname(idx));
emitline(", ");
emitoff(off: i64);
emitline("(BP)\n");
idx += 1;
} else {
localaddstack(c, nm, p.lhs, 16 + stkcursor*8);
stkcursor += 1;
};
};
};};};
};
@@ -17260,12 +17358,18 @@ fn cgfn(c: *cgen, fn_: *node) void = {
let istg: bool = false;
let issl: bool = false;
let isst: bool = false;
let isstruct: bool = false;
let structsz: i32 = 0;
if (!isvar) {
isf = isfloattype(c, scanp.lhs);
istg = istaggedtype(c, scanp.lhs);
if (!isf && !istg) {
issl = isslicetype(c, scanp.lhs);
if (!issl) { isst = isstrtype(c, scanp.lhs); };
if (!issl && !isst) {
structsz = structparamsize(c, scanp.lhs);
isstruct = structsz > 0;
};
};
};
let eb: i32 = 1;
@@ -17274,18 +17378,23 @@ fn cgfn(c: *cgen, fn_: *node) void = {
else { if (istg) { sz = slotsize(c, scanp.lhs); eb = sz / 8; }
else { if (issl) { eb = 3; sz = 24; }
else { if (isst) { eb = 2; sz = 16; }
else { if (isstruct) {
sz = structsz;
eb = 1;
if (structsz > 8) { eb = 2; };
}
else { if (isf) {
eb = 1;
sz = 8;
if (isf32type(c, scanp.lhs)) { sz = 4; };
}; }; }; }; };
}; }; }; }; }; };
let regs_left: i32 = 6 - argi;
if (isf) { regs_left = 8 - fargi; };
if (regs_left >= eb) {
frame += sz;
if (isf) { fargi += 1; }
else { argi += eb; };
} else { if (eb > 1 && regs_left > 0 && (istg || issl || isst || isvar)) {
} else { if (eb > 1 && regs_left > 0 && (istg || issl || isst || isstruct || isvar)) {
// Multi-word param straddles the reg/stack boundary;
// cgfnparams stitches the tail from positive BP
// offsets into a single local slot, so we still