diff --git a/Makefile b/Makefile index a5512cd8..2b55b0ac 100644 --- a/Makefile +++ b/Makefile @@ -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) diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index da30c990..fe095a1e 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -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 diff --git a/selfhost/cmd/wcc/cgendecl.ww b/selfhost/cmd/wcc/cgendecl.ww index bff24e72..a98dd8b7 100644 --- a/selfhost/cmd/wcc/cgendecl.ww +++ b/selfhost/cmd/wcc/cgendecl.ww @@ -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 diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index f668e46a..5bd80d72 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -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 diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index ce5d316a..3b9f28cd 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -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 diff --git a/test/wcc/717_struct_byval_param.c b/test/wcc/717_struct_byval_param.c new file mode 100644 index 00000000..26ab8ad8 --- /dev/null +++ b/test/wcc/717_struct_byval_param.c @@ -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 +#include +#include +#include +#include +#include + +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; +}