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:
7
Makefile
7
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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
289
test/wcc/717_struct_byval_param.c
Normal file
289
test/wcc/717_struct_byval_param.c
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user