cstage+selfhost+test: System V AMD64 sret discipline for >24B struct return (#23)
Class B shared miscompile pre-fix: cstage skipped the CALL emit at the
receive site (frame collapsed, exit 11); wwstage emitted CALL but
truncated 32B return to AX only (slice payload garbage, segfault on
g.b[0]). Both stages now lower plain TY_STRUCT > 24B through the SysV
sret discipline: caller pre-allocates dest, passes &dest in RDI as a
hidden first-arg (user args shift to SI/DX/CX/R8/R9/+stack), callee
saves RDI to @sretarg at the prologue and writes through it, returns
RDI in RAX. Surfaced by lib/encoding/utf8 pre-flight when the
Hoehrmann decoder (32B) hit 698_cgreturn_struct.c's OUT-OF-SCOPE
marker.
Scope: plain TY_STRUCT > 24B only — tagged unions, tuples, str, slice
keep their existing register-return ABIs. `return f()` forwarding
from a sret callee is fail-loud-not-wired (compile-time error in
both stages, follow-up filed); the workaround `let r = f(); return
r;` is wired and byte-identical. Discard-context calls (`f();` of an
sret-returning function) share a per-fn single-slot @sretscr;
consecutive discards reuse the same slot.
698_cgreturn_struct.c's OUT-OF-SCOPE marker retired in the same
commit; three positive rows (32B quad, 32B decoder, 40B five) now
assert the sret discipline across both stages via byte-id diff.
Tests:
- 721_sret_struct_return pins three asm-presence sentinels per
row: (a) LEAQ -K(BP), DI immediately before CALL at the receive
site, (b) MOVQ -K(BP), AX before RET in the callee (sret return-
the-pointer), (c) negative-assert no MOVQ AX, -K(BP) capture for
return type >8B. Three rows × both stages × cmp -s byte-id.
- 925_sret_struct_return_run runtime-pins 7 rows × 2 stages
including the collision row (25B+ struct BOTH returned AND passed
by-value as arg — catches arg-shift, sister site to #11), nested
struct payload, slice payload, reassign-receive, N_IDENT return
rhs.
89/89 ok. 995_self_rebuild stays green (ww2==ww3==ww4 byte-id).
This commit is contained in:
@@ -291,15 +291,106 @@ fn cgreturn(c: *cgen, n: *node) void = {
|
||||
c.lastwasreturn = 1;
|
||||
return;
|
||||
};
|
||||
// sret return (#23): plain TY_STRUCT > 24B. Callee writes
|
||||
// through *(@sretarg) (the caller-prealloc dest saved at
|
||||
// the prologue), then loads @sretarg into RAX and rets —
|
||||
// the SysV "return the pointer" discipline. Two rhs shapes
|
||||
// are wired: N_IDENT (word-copy from rhs slot to *(dest))
|
||||
// and N_STRUCTLIT (cgstructlitfill with mode=1 PTR_LOCAL).
|
||||
if (c.sretargoff != 0) {
|
||||
let scs: i32 = sretretsize(c, c.fnret);
|
||||
if (scs > 0) {
|
||||
// `return f();` from a sret callee would silent-
|
||||
// miscompile: cgexpr writes inner's result to
|
||||
// @sretscr but outer never copies into *@sretarg
|
||||
// and never sets RAX. Fail loud per CLAUDE.md
|
||||
// rule 7; the `let r = f(); return r;` workaround
|
||||
// is already wired and byte-id with cstage.
|
||||
if (rhs.kind == nkind.N_CALL) {
|
||||
let m: str = "ww: cgreturn: sret return-forwarding for >24B struct not wired (task #23)\n";
|
||||
os.write(2, m.ptr, m.len: u64);
|
||||
os.exit(1);
|
||||
};
|
||||
let okrhs: bool = false;
|
||||
if (rhs.kind == nkind.N_IDENT) { okrhs = true; };
|
||||
if (rhs.kind == nkind.N_STRUCTLIT) { okrhs = true; };
|
||||
if (okrhs) {
|
||||
if (rhs.kind == nkind.N_STRUCTLIT) {
|
||||
let trefn: *node = rhs.lhs;
|
||||
let sname: str;
|
||||
sname.ptr = nil; sname.len = 0;
|
||||
if (trefn != nil) {
|
||||
if (trefn.kind == nkind.N_IDENT) { sname = trefn.str; }
|
||||
else { if (trefn.kind == nkind.N_TNAME) { sname = trefn.str; }; };
|
||||
};
|
||||
let sret_si: *structinfo = structlookup(c, sname);
|
||||
if (sret_si != nil) {
|
||||
let emptys: str;
|
||||
emptys.ptr = nil; emptys.len = 0;
|
||||
// mode=1 (PTR_LOCAL): base reg = BX,
|
||||
// reloaded from @sretarg(BP) before
|
||||
// each field store. disp = 0 because
|
||||
// the dest pointer IS the struct base.
|
||||
cgstructlitfill(c, sret_si, rhs,
|
||||
1, c.sretargoff, emptys,
|
||||
0, scs);
|
||||
};
|
||||
} else {
|
||||
let rl: *local = localfindnode(c, rhs.str);
|
||||
if (rl != nil) {
|
||||
emitline("\tMOVQ\t");
|
||||
emitoff(c.sretargoff: i64);
|
||||
emitline("(BP), BX\n");
|
||||
let k: i32 = 0;
|
||||
for (k + 8 <= scs) {
|
||||
emitline("\tMOVQ\t");
|
||||
emitoff((rl.off + k): i64);
|
||||
emitline("(BP), AX\n");
|
||||
emitline("\tMOVQ\tAX, ");
|
||||
emitoff(k: i64);
|
||||
emitline("(BX)\n");
|
||||
k += 8;
|
||||
};
|
||||
for (k + 4 <= scs) {
|
||||
emitline("\tMOVL\t");
|
||||
emitoff((rl.off + k): i64);
|
||||
emitline("(BP), AX\n");
|
||||
emitline("\tMOVL\tAX, ");
|
||||
emitoff(k: i64);
|
||||
emitline("(BX)\n");
|
||||
k += 4;
|
||||
};
|
||||
for (k < scs) {
|
||||
emitline("\tMOVB\t");
|
||||
emitoff((rl.off + k): i64);
|
||||
emitline("(BP), AX\n");
|
||||
emitline("\tMOVB\tAX, ");
|
||||
emitoff(k: i64);
|
||||
emitline("(BX)\n");
|
||||
k += 1;
|
||||
};
|
||||
};
|
||||
};
|
||||
// sret return: RAX = dest pointer.
|
||||
emitline("\tMOVQ\t");
|
||||
emitoff(c.sretargoff: i64);
|
||||
emitline("(BP), AX\n");
|
||||
emitline("\tMOVQ\tBP, SP\n");
|
||||
emitline("\tPOPQ\tBP\n");
|
||||
emitline("\tRET\n");
|
||||
c.lastwasreturn = 1;
|
||||
return;
|
||||
};
|
||||
};
|
||||
};
|
||||
// Whole-struct return for sizes <= 24B. ABI: AX=bytes[0..7],
|
||||
// DX=bytes[8..15], CX=bytes[16..23]. Mirrors cstage cgen.c
|
||||
// N_RETURN TY_STRUCT branch. Two rhs shapes are wired:
|
||||
// N_IDENT (word-copy from rhs local slot) and N_STRUCTLIT
|
||||
// (field-by-field store at scratch+foff, with tagged fields
|
||||
// delegated to cgwidentaggedstore). Call-result chain return
|
||||
// is deferred to #5's receive side. Sizes > 24B fall through
|
||||
// to the scalar path below (only AX gets the first qword),
|
||||
// pending sret.
|
||||
// is deferred to #5's receive side. Sizes > 24B route through
|
||||
// the sret arm above.
|
||||
let rname: str;
|
||||
rname.ptr = nil; rname.len = 0;
|
||||
if (c.fnret != nil) {
|
||||
@@ -623,6 +714,22 @@ fn cglet(c: *cgen, n: *node) void = {
|
||||
return;
|
||||
};
|
||||
};
|
||||
// sret receive (#23): plain TY_STRUCT > 24B from a call.
|
||||
// The let's own slot IS the caller-prealloc dest; the
|
||||
// nested cgexpr → cgcall path emits `LEAQ off(BP), DI`
|
||||
// before the CALL and the callee writes through it. No
|
||||
// AX/DX/CX shuffle; AX returns the dest pointer per SysV
|
||||
// sret discipline (irrelevant here).
|
||||
if (rhs.kind == nkind.N_CALL) {
|
||||
let scs: i32 = callsretsize(c, rhs);
|
||||
if (scs > 0) {
|
||||
c.sretdestoff = off;
|
||||
cgexpr(c, rhs);
|
||||
c.sretdestoff = 0;
|
||||
c.lastwasreturn = 0;
|
||||
return;
|
||||
};
|
||||
};
|
||||
// Whole-struct receive for sizes <=24B (call-result rhs).
|
||||
// Counterpart of #4's cgreturn ABI: cgexpr leaves
|
||||
// AX=bytes[0..7], DX=bytes[8..15], CX=bytes[16..23],
|
||||
|
||||
Reference in New Issue
Block a user