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

@@ -240,6 +240,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \
$(BIN)/test_tagged_return_scratch \
$(BIN)/test_tagged_widen_f64 \
$(BIN)/test_match_spill_pointer_payload \
$(BIN)/test_struct_byval_param \
$(BIN)/test_param_shadow_mod \
$(BIN)/test_localoff_scope \
$(BIN)/test_cast_enum_movl \
@@ -491,6 +492,12 @@ $(BIN)/test_match_spill_pointer_payload: test/wcc/716_match_spill_pointer_payloa
$(LIB)/libwwrt.a | $(BIN)
$(CC) $(CFLAGS) -o $@ $<
$(BIN)/test_struct_byval_param: test/wcc/717_struct_byval_param.c \
$(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
$(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \
$(LIB)/libwwrt.a | $(BIN)
$(CC) $(CFLAGS) -o $@ $<
$(BIN)/test_use_promote_alias: test/wcc/699_use_promote_alias.c \
$(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
$(LIB)/libwwrt.a | $(BIN)

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

View File

@@ -726,18 +726,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;
};
};
};};};
};
@@ -780,12 +828,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;
@@ -794,18 +848,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

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

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

View File

@@ -0,0 +1,289 @@
/*
* 717_struct_byval_param — wwstage param-spill ABI for user-defined
* by-value struct parameters of size ≤ 16B. Sister to #9 (which
* fixed an OVER-allocation in the match-scrutinee spill path);
* this is the UNDER-allocation in the cgfn param-spill path.
*
* Pre-fix (#11): wwstage cgfnparams dispatched on TK_ELLIPSIS,
* float, tagged, slice, str — then a catch-all 8B scalar arm.
* User-defined struct params (TY_STRUCT after collectstructs)
* had no branch and fell through to the 8B arm. For
* `fn cmp(a: inst, b: inst)` with `inst = struct { sec: i64,
* nsec: i64 }` cstage allocated `TEXT cmp,$32` and spilled all
* four argregs DI/SI/DX/CX; wwstage allocated `TEXT cmp,$16`
* and spilled only DI/SI — the second-half value words of each
* arg (DX/CX) were never stored, and `b.sec`/`b.nsec` reads
* trailed into the saved-BP word. Built-in `str` (also 16B,
* ptr+len) routed correctly through the isstrtype arm, so the
* bug was a TY_STRUCT type-dispatch miss, not a missing path.
*
* Fix (#11, wwstage-only per rule 10): add structparamsize in
* cgenutil.ww (mirror cstage cgen.c struct_arg_size: returns the
* struct's totsize for sizes in (0, 16], else 0). Add a struct
* arm in cgfnparams between the str arm and the 8B catch-all,
* matching cstage's struct_eb = (size > 8) ? 2 : 1 — including
* the partial-fit stitch arm (idx=5, nw=2: one reg, one stack
* tail). Mirror the same isstr branch in cgfn's pre-scan so the
* frame reservation and emit stay lockstep.
*
* What this test pins:
* - Asm byte-identity between cstage and wwstage for 2×16B
* struct params, 1×16B struct + 1×8B struct (mixed
* eightbyte counts), 1×16B struct + 1×str (regression guard
* for the already-working str path), and 6 i64 + 1 16B
* struct (partial-fit stitch case).
* - Runtime end-to-end: each row's main returns a deterministic
* exit code derived from the received struct's fields. A
* regression in the spill ABI (e.g. CX→slot+8 missing) shows
* up as a `got != want` exit-code mismatch.
*
* NOT covered:
* - Structs > 16B by-value param: cstage's struct_arg_size
* itself gates on size <= 16, so a 24B-struct param also
* under-allocates in cstage (falls through to the 8B arm).
* Both stages agree on the wrong behaviour, so it's bug-
* compatible byte-identical; that's a separate task.
* - Single-field i32-or-smaller struct: still 8B totsize after
* wwstage's registerstruct rounds up, equivalent to a scalar
* i64 in the spill path. The 1-field i64 row exercises the
* nw=1 branch.
*/
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/stat.h>
#include <sys/wait.h>
static int
runwait(const char *cmd)
{
int rc = system(cmd);
if (rc == -1) return -1;
if (WIFEXITED(rc)) return WEXITSTATUS(rc);
return -1;
}
struct row { const char *label; const char *src; int want; };
static const struct row rows[] = {
/* 1. Canonical 2×16B struct shape (the original probe). The
* comparator reads b.sec — that's the second-arg slot+0, which
* pre-fix sat unwritten and aliased the caller's saved BP. */
{ "two_16B_struct_cmp",
"type inst = struct { sec: i64, nsec: i64 };\n"
"fn cmp(a: inst, b: inst) i32 = {\n"
" if (a.sec < b.sec) { return -1; };\n"
" if (a.sec > b.sec) { return 1; };\n"
" if (a.nsec < b.nsec) { return -1; };\n"
" if (a.nsec > b.nsec) { return 1; };\n"
" return 0;\n"
"};\n"
"fn main() i32 = {\n"
" let x: inst = inst { sec = 5i64, nsec = 200i64 };\n"
" let y: inst = inst { sec = 5i64, nsec = 100i64 };\n"
" let r: i32 = cmp(x, y);\n"
" if (r > 0) { return 7; };\n"
" return 0;\n"
"};\n",
7 },
/* 2. 16B struct + 8B struct (mixed eightbyte counts). a uses
* DI/SI (2 eb), b uses DX (1 eb). Returns a.sec + a.nsec + b.v
* = 10 + 20 + 30 = 60. */
{ "mixed_16B_8B_struct",
"type two = struct { sec: i64, nsec: i64 };\n"
"type one = struct { v: i64 };\n"
"fn sum(a: two, b: one) i32 = {\n"
" return (a.sec + a.nsec + b.v): i32;\n"
"};\n"
"fn main() i32 = {\n"
" let x: two = two { sec = 10i64, nsec = 20i64 };\n"
" let y: one = one { v = 30i64 };\n"
" return sum(x, y);\n"
"};\n",
60 },
/* 3. 16B struct + 16B str (regression guard). The pre-fix
* wwstage already handled str via the isstrtype arm; this row
* pins that the new struct arm doesn't shadow it. struct in
* DI/SI, str in DX/CX. Returns a.sec + s.len = 42 + 3 = 45. */
{ "struct_plus_str",
"type two = struct { sec: i64, nsec: i64 };\n"
"fn pick(a: two, s: str) i32 = {\n"
" return (a.sec + (s.len: i64)): i32;\n"
"};\n"
"fn main() i32 = {\n"
" let x: two = two { sec = 42i64, nsec = 0i64 };\n"
" return pick(x, \"abc\");\n"
"};\n",
45 },
/* 4. Partial-fit stitch: 5 i64 args consume DI/SI/DX/CX/R8;
* the 16B struct hits idx=5 with regs_left=1, nw=2 — one
* half (struct word 0) lands in R9, the second half spills
* onto the caller's stack at +16(BP). Wwstage's struct arm
* stitches both into a contiguous local slot. Returns
* sum(s0..s4) + x.a + x.b = 1+2+3+4+5+11+22 = 48. */
{ "partial_fit_5i64_plus_16B",
"type pair = struct { a: i64, b: i64 };\n"
"fn manyfn(s0: i64, s1: i64, s2: i64, s3: i64, s4: i64,\n"
" x: pair) i32 = {\n"
" return (s0 + s1 + s2 + s3 + s4 + x.a + x.b): i32;\n"
"};\n"
"fn main() i32 = {\n"
" let p: pair = pair { a = 11i64, b = 22i64 };\n"
" return manyfn(1i64, 2i64, 3i64, 4i64, 5i64, p);\n"
"};\n",
48 },
};
static int
run_driver(const char *driver, const struct row *r, int i)
{
char src[64], tmpdir[64], cmd[1024];
snprintf(src, sizeof src, "/tmp/wcsbp_%d_%d.ww", getpid(), i);
snprintf(tmpdir, sizeof tmpdir, "/tmp/wcsbp_%d_d_%d", getpid(), i);
FILE *f = fopen(src, "wb");
if (!f) return -1;
fputs(r->src, f);
fclose(f);
mkdir(tmpdir, 0755);
snprintf(cmd, sizeof cmd, "cd %s && %s build %s",
tmpdir, driver, src);
if (runwait(cmd) != 0) {
fprintf(stderr, "row[%s]: build via %s failed\n",
r->label, driver);
unlink(src); rmdir(tmpdir);
return -1;
}
const char *base = strrchr(src, '/');
base = base ? base + 1 : src;
char outbin[128];
snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base);
char *dot = strrchr(outbin, '.');
if (dot && strcmp(dot, ".ww") == 0) *dot = '\0';
int got = runwait(outbin);
unlink(src); unlink(outbin); rmdir(tmpdir);
return got;
}
/* asm_byte_identical — pin the spill prologue + frame size by
* diffing the w6c vs w6c_ww text output. The whole point of #11
* is that wwstage's frame stops under-bloating for user-defined
* struct params, so the bytes must match. */
static int
asm_byte_identical(const char *bin, const struct row *r, int i)
{
char src[64], cs[64], ws[64], cmd[1024];
snprintf(src, sizeof src, "/tmp/wcsbp_asm_%d_%d.ww", getpid(), i);
snprintf(cs, sizeof cs, "/tmp/wcsbp_asm_%d_%d_c.s", getpid(), i);
snprintf(ws, sizeof ws, "/tmp/wcsbp_asm_%d_%d_w.s", getpid(), i);
FILE *f = fopen(src, "wb");
if (!f) return -1;
fputs(r->src, f);
fclose(f);
snprintf(cmd, sizeof cmd, "%s/w6c -o %s %s 2>/dev/null", bin, cs, src);
if (runwait(cmd) != 0) {
fprintf(stderr, "row[%s]: w6c errored\n", r->label);
unlink(src);
return -1;
}
snprintf(cmd, sizeof cmd, "%s/w6c_ww -o %s %s 2>/dev/null",
bin, ws, src);
if (runwait(cmd) != 0) {
fprintf(stderr, "row[%s]: w6c_ww errored\n", r->label);
unlink(src); unlink(cs);
return -1;
}
FILE *fc = fopen(cs, "rb");
FILE *fw = fopen(ws, "rb");
int rc = 0;
if (!fc || !fw) {
rc = -1;
} else {
for (;;) {
int a = fgetc(fc);
int b = fgetc(fw);
if (a != b) { rc = -1; break; }
if (a == EOF) break;
}
}
if (fc) fclose(fc);
if (fw) fclose(fw);
if (rc != 0)
fprintf(stderr, "row[%s]: cstage vs wwstage asm differs\n",
r->label);
unlink(src); unlink(cs); unlink(ws);
return rc;
}
int
main(void)
{
const char *bin = getenv("BIN");
if (!bin) bin = "out/bin";
char absbin[1024];
if (bin[0] != '/') {
char cwd[1024];
if (getcwd(cwd, sizeof cwd) == NULL) return 1;
snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin);
bin = absbin;
}
char cdrv[1024];
snprintf(cdrv, sizeof cdrv, "%s/ww", bin);
char wdrv[1024];
snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin);
struct { const char *name; const char *path; int gated_on_existence; }
drivers[] = {
{ "cstage", cdrv, 0 },
{ "wwstage", wdrv, 1 },
{ NULL, NULL, 0 },
};
int n = (int)(sizeof rows / sizeof rows[0]);
int total = 0, fail = 0;
for (int d = 0; drivers[d].name; d++) {
if (drivers[d].gated_on_existence
&& access(drivers[d].path, X_OK) != 0) {
fprintf(stderr, "struct_byval_param: skip %s (no %s)\n",
drivers[d].name, drivers[d].path);
continue;
}
for (int i = 0; i < n; i++) {
int got = run_driver(drivers[d].path, &rows[i], i);
total++;
if (got != rows[i].want) {
fprintf(stderr,
"struct_byval_param[%s][%s]: exit=%d want=%d\n",
drivers[d].name, rows[i].label,
got, rows[i].want);
fail++;
}
}
}
if (access(wdrv, X_OK) == 0) {
for (int i = 0; i < n; i++) {
total++;
if (asm_byte_identical(bin, &rows[i], i) != 0)
fail++;
}
}
if (fail) {
fprintf(stderr,
"struct_byval_param: %d/%d fixtures failed\n",
fail, total);
return 1;
}
printf("struct_byval_param: %d/%d ok\n", total, total);
return 0;
}