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

@@ -369,6 +369,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
@@ -1961,6 +1987,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