cstage+selfhost+test: wire sret return-forwarding (#9)
Class A compile-time fatal retirement — `return f()` from an sret callee bailed both stages with "sret return-forwarding for >24B struct not wired (task #23)" at every site, forcing every caller into a `let r = f(); return r;` workaround that materialised an intermediate >24B copy in outer's frame. Forwarding now elides the copy: outer reloads its own @sretarg into RDI for the inner CALL via `MOVQ @sretarg(BP), DI` (NOT `LEAQ <local>, DI`), inner writes directly into outer's caller-prealloc dest, RAX (inner's returned dest pointer per the sret discipline) is already outer's return value. Wires 2 sites × 2 stages (same triangle as #23): caller arg-shift in cgcall/pushargsrev gains an RDI-source switch via cg_sret_forward / c.sretforward; callee return-arm in cgreturn replaces the fail-loud abort with cgexpr-into-cgcall + epilogue. The @sretscr scratch slot is still pre-allocated on the forwarding branch (unused) — eliding would need AST-walk awareness in scanlocals; symmetric-allocate is the simpler path and keeps byte-id with non-forwarding callers. Latent surfaced and filed during probe (NOT in this commit's scope): multi-sret-receive in a single fn diverges between stages — cstage always allocates @sretscr on first sret CALL, wwstage only when sretdestoff == 0. Bootstrap stays green because the selfhost corpus has zero >1-sret-receive call sites. Tests: - 721_sret_struct_return gains 2 forwarding rows + a 4th asm- presence sentinel: at the inner CALL site inside outer fn, the RDI source must be `MOVQ -K(BP), DI` (reload of outer's saved @sretarg) NOT `LEAQ -K(BP), DI` (a temporary local would write inner's payload into outer's frame, not caller's dest). - 925_sret_struct_return_run gains 3 forwarding rows: simple quad forward, multi-arg inner (pair-by-value + scalar args alongside the hidden RDI), and slice-payload (decoder { i64, []u8 } — the utf8 iterator shape, asserts ptr/len/cap survive the @sretarg chain). 90/90 ok. 995_self_rebuild stays green (ww2==ww3==ww4 byte-id).
This commit is contained in:
@@ -54,11 +54,18 @@ static int cg_retscr;
|
|||||||
* cg_sretscr_off — per-fn @sretscr discard slot for sret CALLs whose
|
* cg_sretscr_off — per-fn @sretscr discard slot for sret CALLs whose
|
||||||
* result is dropped (no named receiver). Single-slot
|
* result is dropped (no named receiver). Single-slot
|
||||||
* SSoT mirroring cg_retscr. Sized to the largest
|
* SSoT mirroring cg_retscr. Sized to the largest
|
||||||
* discarded sret return type in the fn. */
|
* discarded sret return type in the fn.
|
||||||
|
* cg_sret_forward — set by cgreturn `return f();` from an sret callee
|
||||||
|
* to signal cgcall: source RDI for inner from outer's
|
||||||
|
* saved @sretarg (MOVQ) instead of LEAQ'ing a local
|
||||||
|
* dest. Inner writes into outer's caller-prealloc;
|
||||||
|
* inner's RAX (the dest pointer) is already outer's
|
||||||
|
* return value. No temporary in outer's frame. */
|
||||||
static int cg_sret_arg_off;
|
static int cg_sret_arg_off;
|
||||||
static int cg_sret_dest_off;
|
static int cg_sret_dest_off;
|
||||||
static int cg_sretscr_off;
|
static int cg_sretscr_off;
|
||||||
static int cg_sretscr_sz;
|
static int cg_sretscr_sz;
|
||||||
|
static int cg_sret_forward;
|
||||||
|
|
||||||
/* Per-fn defer stack: pushed in registration order, popped (emitted)
|
/* Per-fn defer stack: pushed in registration order, popped (emitted)
|
||||||
* in reverse at each return. */
|
* in reverse at each return. */
|
||||||
@@ -4547,10 +4554,27 @@ cgexpr(Cg *c, Node *n, Local *locals)
|
|||||||
}
|
}
|
||||||
/* sret hidden first-arg (#23): load &dest into RDI AFTER
|
/* sret hidden first-arg (#23): load &dest into RDI AFTER
|
||||||
* all user-arg pops have finished — the pop loop started
|
* all user-arg pops have finished — the pop loop started
|
||||||
* its int-arg cursor at 1, so RDI was never written. */
|
* its int-arg cursor at 1, so RDI was never written.
|
||||||
if (sret_call_sz > 0)
|
*
|
||||||
ins2(c, A_LEAQ, amem(D_BP, sret_call_off),
|
* Forwarding (task #9 follow-up): when outer's `return f();`
|
||||||
areg(D_DI));
|
* forwards through an sret callee, source RDI from outer's
|
||||||
|
* saved @sretarg — inner writes directly into outer's
|
||||||
|
* caller-prealloc dest. No temporary in outer's frame.
|
||||||
|
* The @sretscr slot was still allocated above for byte-id
|
||||||
|
* lockstep with wwstage's scanlocals reservation; it goes
|
||||||
|
* unused on the forwarding branch. */
|
||||||
|
if (sret_call_sz > 0) {
|
||||||
|
if (cg_sret_forward) {
|
||||||
|
ins2(c, A_MOVQ,
|
||||||
|
amem(D_BP, cg_sret_arg_off),
|
||||||
|
areg(D_DI));
|
||||||
|
cg_sret_forward = 0;
|
||||||
|
} else {
|
||||||
|
ins2(c, A_LEAQ,
|
||||||
|
amem(D_BP, sret_call_off),
|
||||||
|
areg(D_DI));
|
||||||
|
}
|
||||||
|
}
|
||||||
/* SysV: variadic callees require AL to hold the count of
|
/* SysV: variadic callees require AL to hold the count of
|
||||||
* XMM regs used in the variable portion. We don't pass
|
* XMM regs used in the variable portion. We don't pass
|
||||||
* floats yet, so AL=0 covers every case we emit. */
|
* floats yet, so AL=0 covers every case we emit. */
|
||||||
@@ -6548,18 +6572,29 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
|
|||||||
if (n->lhs && cg_ret_type && cg_sret_arg_off != 0) {
|
if (n->lhs && cg_ret_type && cg_sret_arg_off != 0) {
|
||||||
Type *rt = cg_ret_type;
|
Type *rt = cg_ret_type;
|
||||||
if (rt->kind == TY_NAMED) rt = rt->under;
|
if (rt->kind == TY_NAMED) rt = rt->under;
|
||||||
/* `return f();` from a sret callee falls through the
|
/* sret return-forwarding (task #9 follow-up to #23):
|
||||||
* arm below (rhs is N_CALL, not N_IDENT/N_STRUCTLIT)
|
* `return f();` where outer + inner both return the
|
||||||
* and would silent-miscompile: cgexpr places inner's
|
* same >24B struct shape. Outer's @sretarg already
|
||||||
* result in @sretscr but outer never copies into
|
* holds its caller's prealloc dest; pass it to inner
|
||||||
* *@sretarg and never sets RAX. Fail loud per
|
* in RDI (set by cgcall via cg_sret_forward), inner
|
||||||
* CLAUDE.md rule 7; the workaround `let r = f();
|
* writes directly there, inner's RAX (dest pointer)
|
||||||
* return r;` is already wired and correct. */
|
* is already outer's return value. The trailing
|
||||||
|
* MOVQ @sretarg(BP), AX is redundant after inner's
|
||||||
|
* RET but kept for byte-id symmetry with the
|
||||||
|
* N_IDENT / N_STRUCTLIT arms below. */
|
||||||
if (rt && rt->kind == TY_STRUCT
|
if (rt && rt->kind == TY_STRUCT
|
||||||
&& (int)rt->size > 24
|
&& (int)rt->size > 24
|
||||||
&& n->lhs->kind == N_CALL)
|
&& n->lhs->kind == N_CALL) {
|
||||||
fatal("cgreturn: sret return-forwarding "
|
cg_sret_forward = 1;
|
||||||
"for >24B struct not wired (task #23)");
|
cgexpr(c, n->lhs, *locals);
|
||||||
|
ins2(c, A_MOVQ,
|
||||||
|
amem(D_BP, cg_sret_arg_off),
|
||||||
|
areg(D_AX));
|
||||||
|
ins2(c, A_MOVQ, areg(D_BP), areg(D_SP));
|
||||||
|
ins1(c, A_POPQ, areg(D_BP));
|
||||||
|
ins0(c, A_RET);
|
||||||
|
break;
|
||||||
|
}
|
||||||
if (rt && rt->kind == TY_STRUCT
|
if (rt && rt->kind == TY_STRUCT
|
||||||
&& (int)rt->size > 24
|
&& (int)rt->size > 24
|
||||||
&& (n->lhs->kind == N_IDENT
|
&& (n->lhs->kind == N_IDENT
|
||||||
@@ -7061,6 +7096,7 @@ cgfn(Cg *c, FILE *out, Node *fn)
|
|||||||
cg_sret_dest_off = 0;
|
cg_sret_dest_off = 0;
|
||||||
cg_sretscr_off = 0;
|
cg_sretscr_off = 0;
|
||||||
cg_sretscr_sz = 0;
|
cg_sretscr_sz = 0;
|
||||||
|
cg_sret_forward = 0;
|
||||||
|
|
||||||
int frame = 0;
|
int frame = 0;
|
||||||
Local *locals = NULL;
|
Local *locals = NULL;
|
||||||
|
|||||||
@@ -13145,10 +13145,24 @@ fn cgcall(c: *cgen, n: *node) void = {
|
|||||||
// sret hidden first-arg (#23): load &dest into RDI AFTER all
|
// sret hidden first-arg (#23): load &dest into RDI AFTER all
|
||||||
// user-arg pops have finished — intidx started at 1 so RDI was
|
// user-arg pops have finished — intidx started at 1 so RDI was
|
||||||
// never written. The CALL emit follows immediately.
|
// never written. The CALL emit follows immediately.
|
||||||
|
//
|
||||||
|
// Forwarding (task #9 follow-up): when outer's `return f();`
|
||||||
|
// forwards through an sret callee, source RDI from outer's
|
||||||
|
// saved @sretarg — inner writes directly into outer's caller-
|
||||||
|
// prealloc dest. No temporary in outer's frame. The @sretscr
|
||||||
|
// slot stays reserved for byte-id with cstage; it goes unused
|
||||||
|
// on the forwarding branch.
|
||||||
if (sretcs > 0) {
|
if (sretcs > 0) {
|
||||||
emitline("\tLEAQ\t");
|
if (c.sretforward != 0) {
|
||||||
emitoff(sretcalloff: i64);
|
emitline("\tMOVQ\t");
|
||||||
emitline("(BP), DI\n");
|
emitoff(c.sretargoff: i64);
|
||||||
|
emitline("(BP), DI\n");
|
||||||
|
c.sretforward = 0;
|
||||||
|
} else {
|
||||||
|
emitline("\tLEAQ\t");
|
||||||
|
emitoff(sretcalloff: i64);
|
||||||
|
emitline("(BP), DI\n");
|
||||||
|
};
|
||||||
};
|
};
|
||||||
if (isfnptrcall) {
|
if (isfnptrcall) {
|
||||||
// Load fn-ptr field value into AX; CALL AX. We emit the
|
// Load fn-ptr field value into AX; CALL AX. We emit the
|
||||||
@@ -15838,16 +15852,28 @@ fn cgreturn(c: *cgen, n: *node) void = {
|
|||||||
if (c.sretargoff != 0) {
|
if (c.sretargoff != 0) {
|
||||||
let scs: i32 = sretretsize(c, c.fnret);
|
let scs: i32 = sretretsize(c, c.fnret);
|
||||||
if (scs > 0) {
|
if (scs > 0) {
|
||||||
// `return f();` from a sret callee would silent-
|
// sret return-forwarding (task #9 follow-up to
|
||||||
// miscompile: cgexpr writes inner's result to
|
// #23): `return f();` where outer + inner both
|
||||||
// @sretscr but outer never copies into *@sretarg
|
// return the same >24B struct shape. Outer's
|
||||||
// and never sets RAX. Fail loud per CLAUDE.md
|
// @sretarg already holds its caller's prealloc
|
||||||
// rule 7; the `let r = f(); return r;` workaround
|
// dest; pass it to inner in RDI (set by cgcall
|
||||||
// is already wired and byte-id with cstage.
|
// via c.sretforward), inner writes directly
|
||||||
|
// there, inner's RAX (dest pointer) is already
|
||||||
|
// outer's return value. The trailing MOVQ
|
||||||
|
// @sretarg(BP), AX is redundant after inner's
|
||||||
|
// RET but kept for byte-id symmetry with the
|
||||||
|
// N_IDENT / N_STRUCTLIT arms below.
|
||||||
if (rhs.kind == nkind.N_CALL) {
|
if (rhs.kind == nkind.N_CALL) {
|
||||||
let m: str = "ww: cgreturn: sret return-forwarding for >24B struct not wired (task #23)\n";
|
c.sretforward = 1;
|
||||||
os.write(2, m.ptr, m.len: u64);
|
cgexpr(c, rhs);
|
||||||
os.exit(1);
|
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;
|
||||||
};
|
};
|
||||||
let okrhs: bool = false;
|
let okrhs: bool = false;
|
||||||
if (rhs.kind == nkind.N_IDENT) { okrhs = true; };
|
if (rhs.kind == nkind.N_IDENT) { okrhs = true; };
|
||||||
@@ -18440,10 +18466,17 @@ type cgen = struct {
|
|||||||
// sums c.sretscrsz to pre-reserve.
|
// sums c.sretscrsz to pre-reserve.
|
||||||
// sretscrsz — max sret discard size in this fn (sums during
|
// sretscrsz — max sret discard size in this fn (sums during
|
||||||
// scanlocals, consumed by localadd("@sretscr", ...)).
|
// scanlocals, consumed by localadd("@sretscr", ...)).
|
||||||
|
// sretforward — set by cgreturn `return f();` from an sret callee to
|
||||||
|
// signal cgcall: source RDI for inner from outer's
|
||||||
|
// saved @sretarg (MOVQ) instead of LEAQ'ing a local
|
||||||
|
// dest. Inner writes into outer's caller-prealloc;
|
||||||
|
// inner's RAX (the dest pointer) is already outer's
|
||||||
|
// return value. Cleared after cgcall consumes it.
|
||||||
sretargoff: i32,
|
sretargoff: i32,
|
||||||
sretdestoff: i32,
|
sretdestoff: i32,
|
||||||
sretscroff: i32,
|
sretscroff: i32,
|
||||||
sretscrsz: i32,
|
sretscrsz: i32,
|
||||||
|
sretforward: i32,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Top-level mutable `let` registry. Mirrors cmd/w6c/cgen.c LetVar.
|
// Top-level mutable `let` registry. Mirrors cmd/w6c/cgen.c LetVar.
|
||||||
@@ -18471,6 +18504,7 @@ fn cgeninit(c: *cgen, a: *arena) void = {
|
|||||||
c.sretdestoff = 0;
|
c.sretdestoff = 0;
|
||||||
c.sretscroff = 0;
|
c.sretscroff = 0;
|
||||||
c.sretscrsz = 0;
|
c.sretscrsz = 0;
|
||||||
|
c.sretforward = 0;
|
||||||
// Note: strlit_seq, strlits, ffis are *not* reset here; they
|
// Note: strlit_seq, strlits, ffis are *not* reset here; they
|
||||||
// persist across cgfn calls within one file. cgfile resets them
|
// persist across cgfn calls within one file. cgfile resets them
|
||||||
// at the start of each compilation unit.
|
// at the start of each compilation unit.
|
||||||
|
|||||||
@@ -450,10 +450,17 @@ type cgen = struct {
|
|||||||
// sums c.sretscrsz to pre-reserve.
|
// sums c.sretscrsz to pre-reserve.
|
||||||
// sretscrsz — max sret discard size in this fn (sums during
|
// sretscrsz — max sret discard size in this fn (sums during
|
||||||
// scanlocals, consumed by localadd("@sretscr", ...)).
|
// scanlocals, consumed by localadd("@sretscr", ...)).
|
||||||
|
// sretforward — set by cgreturn `return f();` from an sret callee to
|
||||||
|
// signal cgcall: source RDI for inner from outer's
|
||||||
|
// saved @sretarg (MOVQ) instead of LEAQ'ing a local
|
||||||
|
// dest. Inner writes into outer's caller-prealloc;
|
||||||
|
// inner's RAX (the dest pointer) is already outer's
|
||||||
|
// return value. Cleared after cgcall consumes it.
|
||||||
sretargoff: i32,
|
sretargoff: i32,
|
||||||
sretdestoff: i32,
|
sretdestoff: i32,
|
||||||
sretscroff: i32,
|
sretscroff: i32,
|
||||||
sretscrsz: i32,
|
sretscrsz: i32,
|
||||||
|
sretforward: i32,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Top-level mutable `let` registry. Mirrors cmd/w6c/cgen.c LetVar.
|
// Top-level mutable `let` registry. Mirrors cmd/w6c/cgen.c LetVar.
|
||||||
@@ -481,6 +488,7 @@ fn cgeninit(c: *cgen, a: *arena) void = {
|
|||||||
c.sretdestoff = 0;
|
c.sretdestoff = 0;
|
||||||
c.sretscroff = 0;
|
c.sretscroff = 0;
|
||||||
c.sretscrsz = 0;
|
c.sretscrsz = 0;
|
||||||
|
c.sretforward = 0;
|
||||||
// Note: strlit_seq, strlits, ffis are *not* reset here; they
|
// Note: strlit_seq, strlits, ffis are *not* reset here; they
|
||||||
// persist across cgfn calls within one file. cgfile resets them
|
// persist across cgfn calls within one file. cgfile resets them
|
||||||
// at the start of each compilation unit.
|
// at the start of each compilation unit.
|
||||||
|
|||||||
@@ -3151,10 +3151,24 @@ fn cgcall(c: *cgen, n: *node) void = {
|
|||||||
// sret hidden first-arg (#23): load &dest into RDI AFTER all
|
// sret hidden first-arg (#23): load &dest into RDI AFTER all
|
||||||
// user-arg pops have finished — intidx started at 1 so RDI was
|
// user-arg pops have finished — intidx started at 1 so RDI was
|
||||||
// never written. The CALL emit follows immediately.
|
// never written. The CALL emit follows immediately.
|
||||||
|
//
|
||||||
|
// Forwarding (task #9 follow-up): when outer's `return f();`
|
||||||
|
// forwards through an sret callee, source RDI from outer's
|
||||||
|
// saved @sretarg — inner writes directly into outer's caller-
|
||||||
|
// prealloc dest. No temporary in outer's frame. The @sretscr
|
||||||
|
// slot stays reserved for byte-id with cstage; it goes unused
|
||||||
|
// on the forwarding branch.
|
||||||
if (sretcs > 0) {
|
if (sretcs > 0) {
|
||||||
emitline("\tLEAQ\t");
|
if (c.sretforward != 0) {
|
||||||
emitoff(sretcalloff: i64);
|
emitline("\tMOVQ\t");
|
||||||
emitline("(BP), DI\n");
|
emitoff(c.sretargoff: i64);
|
||||||
|
emitline("(BP), DI\n");
|
||||||
|
c.sretforward = 0;
|
||||||
|
} else {
|
||||||
|
emitline("\tLEAQ\t");
|
||||||
|
emitoff(sretcalloff: i64);
|
||||||
|
emitline("(BP), DI\n");
|
||||||
|
};
|
||||||
};
|
};
|
||||||
if (isfnptrcall) {
|
if (isfnptrcall) {
|
||||||
// Load fn-ptr field value into AX; CALL AX. We emit the
|
// Load fn-ptr field value into AX; CALL AX. We emit the
|
||||||
|
|||||||
@@ -300,16 +300,28 @@ fn cgreturn(c: *cgen, n: *node) void = {
|
|||||||
if (c.sretargoff != 0) {
|
if (c.sretargoff != 0) {
|
||||||
let scs: i32 = sretretsize(c, c.fnret);
|
let scs: i32 = sretretsize(c, c.fnret);
|
||||||
if (scs > 0) {
|
if (scs > 0) {
|
||||||
// `return f();` from a sret callee would silent-
|
// sret return-forwarding (task #9 follow-up to
|
||||||
// miscompile: cgexpr writes inner's result to
|
// #23): `return f();` where outer + inner both
|
||||||
// @sretscr but outer never copies into *@sretarg
|
// return the same >24B struct shape. Outer's
|
||||||
// and never sets RAX. Fail loud per CLAUDE.md
|
// @sretarg already holds its caller's prealloc
|
||||||
// rule 7; the `let r = f(); return r;` workaround
|
// dest; pass it to inner in RDI (set by cgcall
|
||||||
// is already wired and byte-id with cstage.
|
// via c.sretforward), inner writes directly
|
||||||
|
// there, inner's RAX (dest pointer) is already
|
||||||
|
// outer's return value. The trailing MOVQ
|
||||||
|
// @sretarg(BP), AX is redundant after inner's
|
||||||
|
// RET but kept for byte-id symmetry with the
|
||||||
|
// N_IDENT / N_STRUCTLIT arms below.
|
||||||
if (rhs.kind == nkind.N_CALL) {
|
if (rhs.kind == nkind.N_CALL) {
|
||||||
let m: str = "ww: cgreturn: sret return-forwarding for >24B struct not wired (task #23)\n";
|
c.sretforward = 1;
|
||||||
os.write(2, m.ptr, m.len: u64);
|
cgexpr(c, rhs);
|
||||||
os.exit(1);
|
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;
|
||||||
};
|
};
|
||||||
let okrhs: bool = false;
|
let okrhs: bool = false;
|
||||||
if (rhs.kind == nkind.N_IDENT) { okrhs = true; };
|
if (rhs.kind == nkind.N_IDENT) { okrhs = true; };
|
||||||
|
|||||||
@@ -13145,10 +13145,24 @@ fn cgcall(c: *cgen, n: *node) void = {
|
|||||||
// sret hidden first-arg (#23): load &dest into RDI AFTER all
|
// sret hidden first-arg (#23): load &dest into RDI AFTER all
|
||||||
// user-arg pops have finished — intidx started at 1 so RDI was
|
// user-arg pops have finished — intidx started at 1 so RDI was
|
||||||
// never written. The CALL emit follows immediately.
|
// never written. The CALL emit follows immediately.
|
||||||
|
//
|
||||||
|
// Forwarding (task #9 follow-up): when outer's `return f();`
|
||||||
|
// forwards through an sret callee, source RDI from outer's
|
||||||
|
// saved @sretarg — inner writes directly into outer's caller-
|
||||||
|
// prealloc dest. No temporary in outer's frame. The @sretscr
|
||||||
|
// slot stays reserved for byte-id with cstage; it goes unused
|
||||||
|
// on the forwarding branch.
|
||||||
if (sretcs > 0) {
|
if (sretcs > 0) {
|
||||||
emitline("\tLEAQ\t");
|
if (c.sretforward != 0) {
|
||||||
emitoff(sretcalloff: i64);
|
emitline("\tMOVQ\t");
|
||||||
emitline("(BP), DI\n");
|
emitoff(c.sretargoff: i64);
|
||||||
|
emitline("(BP), DI\n");
|
||||||
|
c.sretforward = 0;
|
||||||
|
} else {
|
||||||
|
emitline("\tLEAQ\t");
|
||||||
|
emitoff(sretcalloff: i64);
|
||||||
|
emitline("(BP), DI\n");
|
||||||
|
};
|
||||||
};
|
};
|
||||||
if (isfnptrcall) {
|
if (isfnptrcall) {
|
||||||
// Load fn-ptr field value into AX; CALL AX. We emit the
|
// Load fn-ptr field value into AX; CALL AX. We emit the
|
||||||
@@ -15838,16 +15852,28 @@ fn cgreturn(c: *cgen, n: *node) void = {
|
|||||||
if (c.sretargoff != 0) {
|
if (c.sretargoff != 0) {
|
||||||
let scs: i32 = sretretsize(c, c.fnret);
|
let scs: i32 = sretretsize(c, c.fnret);
|
||||||
if (scs > 0) {
|
if (scs > 0) {
|
||||||
// `return f();` from a sret callee would silent-
|
// sret return-forwarding (task #9 follow-up to
|
||||||
// miscompile: cgexpr writes inner's result to
|
// #23): `return f();` where outer + inner both
|
||||||
// @sretscr but outer never copies into *@sretarg
|
// return the same >24B struct shape. Outer's
|
||||||
// and never sets RAX. Fail loud per CLAUDE.md
|
// @sretarg already holds its caller's prealloc
|
||||||
// rule 7; the `let r = f(); return r;` workaround
|
// dest; pass it to inner in RDI (set by cgcall
|
||||||
// is already wired and byte-id with cstage.
|
// via c.sretforward), inner writes directly
|
||||||
|
// there, inner's RAX (dest pointer) is already
|
||||||
|
// outer's return value. The trailing MOVQ
|
||||||
|
// @sretarg(BP), AX is redundant after inner's
|
||||||
|
// RET but kept for byte-id symmetry with the
|
||||||
|
// N_IDENT / N_STRUCTLIT arms below.
|
||||||
if (rhs.kind == nkind.N_CALL) {
|
if (rhs.kind == nkind.N_CALL) {
|
||||||
let m: str = "ww: cgreturn: sret return-forwarding for >24B struct not wired (task #23)\n";
|
c.sretforward = 1;
|
||||||
os.write(2, m.ptr, m.len: u64);
|
cgexpr(c, rhs);
|
||||||
os.exit(1);
|
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;
|
||||||
};
|
};
|
||||||
let okrhs: bool = false;
|
let okrhs: bool = false;
|
||||||
if (rhs.kind == nkind.N_IDENT) { okrhs = true; };
|
if (rhs.kind == nkind.N_IDENT) { okrhs = true; };
|
||||||
@@ -18440,10 +18466,17 @@ type cgen = struct {
|
|||||||
// sums c.sretscrsz to pre-reserve.
|
// sums c.sretscrsz to pre-reserve.
|
||||||
// sretscrsz — max sret discard size in this fn (sums during
|
// sretscrsz — max sret discard size in this fn (sums during
|
||||||
// scanlocals, consumed by localadd("@sretscr", ...)).
|
// scanlocals, consumed by localadd("@sretscr", ...)).
|
||||||
|
// sretforward — set by cgreturn `return f();` from an sret callee to
|
||||||
|
// signal cgcall: source RDI for inner from outer's
|
||||||
|
// saved @sretarg (MOVQ) instead of LEAQ'ing a local
|
||||||
|
// dest. Inner writes into outer's caller-prealloc;
|
||||||
|
// inner's RAX (the dest pointer) is already outer's
|
||||||
|
// return value. Cleared after cgcall consumes it.
|
||||||
sretargoff: i32,
|
sretargoff: i32,
|
||||||
sretdestoff: i32,
|
sretdestoff: i32,
|
||||||
sretscroff: i32,
|
sretscroff: i32,
|
||||||
sretscrsz: i32,
|
sretscrsz: i32,
|
||||||
|
sretforward: i32,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Top-level mutable `let` registry. Mirrors cmd/w6c/cgen.c LetVar.
|
// Top-level mutable `let` registry. Mirrors cmd/w6c/cgen.c LetVar.
|
||||||
@@ -18471,6 +18504,7 @@ fn cgeninit(c: *cgen, a: *arena) void = {
|
|||||||
c.sretdestoff = 0;
|
c.sretdestoff = 0;
|
||||||
c.sretscroff = 0;
|
c.sretscroff = 0;
|
||||||
c.sretscrsz = 0;
|
c.sretscrsz = 0;
|
||||||
|
c.sretforward = 0;
|
||||||
// Note: strlit_seq, strlits, ffis are *not* reset here; they
|
// Note: strlit_seq, strlits, ffis are *not* reset here; they
|
||||||
// persist across cgfn calls within one file. cgfile resets them
|
// persist across cgfn calls within one file. cgfile resets them
|
||||||
// at the start of each compilation unit.
|
// at the start of each compilation unit.
|
||||||
|
|||||||
@@ -51,7 +51,11 @@ runwait(const char *cmd)
|
|||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
struct row { const char *label; const char *src; };
|
/* fwd: row's mk body is `return inner(...)` — sret return-forwarding
|
||||||
|
* (task #9 follow-up). Additional sentinel: inside mk, the CALL
|
||||||
|
* inner(SB) must be preceded by `MOVQ -K(BP), DI` (the @sretarg
|
||||||
|
* reload), NOT `LEAQ -K(BP), DI` (which would point at a local). */
|
||||||
|
struct row { const char *label; const char *src; int fwd; };
|
||||||
|
|
||||||
/* Each row's mk fn returns a >24B struct; main does a `let r: T = mk(...)`
|
/* Each row's mk fn returns a >24B struct; main does a `let r: T = mk(...)`
|
||||||
* so the receive site is wired and the sret discipline fires. */
|
* so the receive site is wired and the sret discipline fires. */
|
||||||
@@ -62,7 +66,7 @@ static const struct row rows[] = {
|
|||||||
"fn mk() quad = {\n"
|
"fn mk() quad = {\n"
|
||||||
" return quad { a = 1i64, b = 2i64, c = 3i64, d = 4i64 };\n"
|
" return quad { a = 1i64, b = 2i64, c = 3i64, d = 4i64 };\n"
|
||||||
"};\n"
|
"};\n"
|
||||||
"fn main() i32 = { let q: quad = mk(); return 0; };\n" },
|
"fn main() i32 = { let q: quad = mk(); return 0; };\n", 0 },
|
||||||
/* utf8 decoder shape (surfacing case for #23): i64 + []u8. The
|
/* utf8 decoder shape (surfacing case for #23): i64 + []u8. The
|
||||||
* []u8 field's slice layout (ptr/len/cap) crosses the AX/DX/CX
|
* []u8 field's slice layout (ptr/len/cap) crosses the AX/DX/CX
|
||||||
* boundary — the wwstage truncation bug dropped the slice tail. */
|
* boundary — the wwstage truncation bug dropped the slice tail. */
|
||||||
@@ -78,14 +82,45 @@ static const struct row rows[] = {
|
|||||||
" let b: [1]u8;\n"
|
" let b: [1]u8;\n"
|
||||||
" let d: decoder = mk(b[0:1]);\n"
|
" let d: decoder = mk(b[0:1]);\n"
|
||||||
" return 0;\n"
|
" return 0;\n"
|
||||||
"};\n" },
|
"};\n", 0 },
|
||||||
/* 40B five-i64: second size past 24B, exercises @sretscr sizing. */
|
/* 40B five-i64: second size past 24B, exercises @sretscr sizing. */
|
||||||
{ "five_i64",
|
{ "five_i64",
|
||||||
"type five = struct { a: i64, b: i64, c: i64, d: i64, e: i64 };\n"
|
"type five = struct { a: i64, b: i64, c: i64, d: i64, e: i64 };\n"
|
||||||
"fn mk() five = {\n"
|
"fn mk() five = {\n"
|
||||||
" return five { a = 1i64, b = 2i64, c = 3i64, d = 4i64, e = 5i64 };\n"
|
" return five { a = 1i64, b = 2i64, c = 3i64, d = 4i64, e = 5i64 };\n"
|
||||||
"};\n"
|
"};\n"
|
||||||
"fn main() i32 = { let f: five = mk(); return 0; };\n" },
|
"fn main() i32 = { let f: five = mk(); return 0; };\n", 0 },
|
||||||
|
/* Forwarding (task #9 follow-up to #23): `return inner(...);` from
|
||||||
|
* an sret callee. mk reloads its own @sretarg into RDI and tail-
|
||||||
|
* shapes the call into inner; no @sretscr/local materialised, no
|
||||||
|
* struct copy in mk's frame. */
|
||||||
|
{ "forward_quad",
|
||||||
|
"type quad = struct { a: i64, b: i64, c: i64, d: i64 };\n"
|
||||||
|
"fn inner(x: i64) quad = {\n"
|
||||||
|
" return quad { a = x, b = x + 1i64, c = x + 2i64, d = x + 3i64 };\n"
|
||||||
|
"};\n"
|
||||||
|
"fn mk(x: i64) quad = {\n"
|
||||||
|
" return inner(x);\n"
|
||||||
|
"};\n"
|
||||||
|
"fn main() i32 = { let q: quad = mk(10i64); return 0; };\n", 1 },
|
||||||
|
/* Forwarding decoder: argument-bearing inner (slice param) routes
|
||||||
|
* through the same forwarding shape as utf8 iterators. */
|
||||||
|
{ "forward_decoder",
|
||||||
|
"type decoder = struct { offs: i64, src: []u8 };\n"
|
||||||
|
"fn inner(s: []u8) decoder = {\n"
|
||||||
|
" let r: decoder;\n"
|
||||||
|
" r.offs = 0i64;\n"
|
||||||
|
" r.src = s;\n"
|
||||||
|
" return r;\n"
|
||||||
|
"};\n"
|
||||||
|
"fn mk(s: []u8) decoder = {\n"
|
||||||
|
" return inner(s);\n"
|
||||||
|
"};\n"
|
||||||
|
"fn main() i32 = {\n"
|
||||||
|
" let b: [1]u8;\n"
|
||||||
|
" let d: decoder = mk(b[0:1]);\n"
|
||||||
|
" return 0;\n"
|
||||||
|
"};\n", 1 },
|
||||||
};
|
};
|
||||||
|
|
||||||
static int
|
static int
|
||||||
@@ -190,6 +225,48 @@ check_movq_bp_ax_before_ret(const char *path, const struct row *r)
|
|||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* (d) forwarding-specific sentinel (task #9 follow-up): inside mk's
|
||||||
|
* body (between `TEXT mk,` and the first `CALL inner(SB)` after it),
|
||||||
|
* assert the prior line is `MOVQ -K(BP), DI` — the @sretarg reload
|
||||||
|
* pattern — and NOT `LEAQ -K(BP), DI` (which would mean mk allocated
|
||||||
|
* a local dest for the forwarded call, defeating the elision). */
|
||||||
|
static int
|
||||||
|
check_movq_bp_di_before_inner_call(const char *path, const struct row *r)
|
||||||
|
{
|
||||||
|
FILE *f = fopen(path, "rb");
|
||||||
|
if (!f) return -1;
|
||||||
|
char line[1024];
|
||||||
|
char prev[256] = {0};
|
||||||
|
int in_mk = 0;
|
||||||
|
int ok = -1;
|
||||||
|
while (fgets(line, sizeof line, f)) {
|
||||||
|
if (!in_mk) {
|
||||||
|
if (strstr(line, "TEXT mk,")
|
||||||
|
|| strstr(line, "TEXT\tmk,"))
|
||||||
|
in_mk = 1;
|
||||||
|
strncpy(prev, line, sizeof prev - 1);
|
||||||
|
prev[sizeof prev - 1] = '\0';
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (strstr(line, "CALL\tinner(SB)")
|
||||||
|
|| strstr(line, "CALL inner(SB)")) {
|
||||||
|
if (strstr(prev, "MOVQ\t")
|
||||||
|
&& strstr(prev, "(BP), DI")
|
||||||
|
&& !strstr(prev, "LEAQ"))
|
||||||
|
ok = 0;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
strncpy(prev, line, sizeof prev - 1);
|
||||||
|
prev[sizeof prev - 1] = '\0';
|
||||||
|
}
|
||||||
|
fclose(f);
|
||||||
|
if (ok != 0)
|
||||||
|
fprintf(stderr,
|
||||||
|
"row[%s]: MOVQ -K(BP), DI (sret-forward) before"
|
||||||
|
" CALL inner(SB) in mk missing\n", r->label);
|
||||||
|
return ok;
|
||||||
|
}
|
||||||
|
|
||||||
/* (c) caller-side negative-assert: between `CALL mk(SB)` and the
|
/* (c) caller-side negative-assert: between `CALL mk(SB)` and the
|
||||||
* NEXT instruction line, there must be NO `MOVQ AX, -K(BP)` (the
|
* NEXT instruction line, there must be NO `MOVQ AX, -K(BP)` (the
|
||||||
* pre-#23 wwstage truncation pattern). The natural sret receive
|
* pre-#23 wwstage truncation pattern). The natural sret receive
|
||||||
@@ -254,7 +331,7 @@ main(void)
|
|||||||
for (int i = 0; i < n; i++) {
|
for (int i = 0; i < n; i++) {
|
||||||
char cs_path[128], ws_path[128];
|
char cs_path[128], ws_path[128];
|
||||||
|
|
||||||
/* cstage asm + three sentinels. */
|
/* cstage asm + three (or four, fwd) sentinels. */
|
||||||
if (emit_s(w6c, &rows[i], i, cs_path, sizeof cs_path) != 0) {
|
if (emit_s(w6c, &rows[i], i, cs_path, sizeof cs_path) != 0) {
|
||||||
fprintf(stderr, "row[%s]: w6c failed\n", rows[i].label);
|
fprintf(stderr, "row[%s]: w6c failed\n", rows[i].label);
|
||||||
fail++; total++; continue;
|
fail++; total++; continue;
|
||||||
@@ -263,10 +340,15 @@ main(void)
|
|||||||
if (check_leaq_di_before_call(cs_path, &rows[i]) != 0) fail++;
|
if (check_leaq_di_before_call(cs_path, &rows[i]) != 0) fail++;
|
||||||
if (check_movq_bp_ax_before_ret(cs_path, &rows[i]) != 0) fail++;
|
if (check_movq_bp_ax_before_ret(cs_path, &rows[i]) != 0) fail++;
|
||||||
if (check_no_movq_ax_bp_after_call(cs_path, &rows[i]) != 0) fail++;
|
if (check_no_movq_ax_bp_after_call(cs_path, &rows[i]) != 0) fail++;
|
||||||
|
if (rows[i].fwd) {
|
||||||
|
total++;
|
||||||
|
if (check_movq_bp_di_before_inner_call(cs_path,
|
||||||
|
&rows[i]) != 0) fail++;
|
||||||
|
}
|
||||||
|
|
||||||
if (!have_ww) { unlink(cs_path); continue; }
|
if (!have_ww) { unlink(cs_path); continue; }
|
||||||
|
|
||||||
/* wwstage asm + three sentinels. */
|
/* wwstage asm + three (or four, fwd) sentinels. */
|
||||||
if (emit_s(w6c_ww, &rows[i], i, ws_path, sizeof ws_path) != 0) {
|
if (emit_s(w6c_ww, &rows[i], i, ws_path, sizeof ws_path) != 0) {
|
||||||
fprintf(stderr,
|
fprintf(stderr,
|
||||||
"row[%s]: w6c_ww failed\n", rows[i].label);
|
"row[%s]: w6c_ww failed\n", rows[i].label);
|
||||||
@@ -278,6 +360,11 @@ main(void)
|
|||||||
if (check_leaq_di_before_call(ws_path, &rows[i]) != 0) fail++;
|
if (check_leaq_di_before_call(ws_path, &rows[i]) != 0) fail++;
|
||||||
if (check_movq_bp_ax_before_ret(ws_path, &rows[i]) != 0) fail++;
|
if (check_movq_bp_ax_before_ret(ws_path, &rows[i]) != 0) fail++;
|
||||||
if (check_no_movq_ax_bp_after_call(ws_path, &rows[i]) != 0) fail++;
|
if (check_no_movq_ax_bp_after_call(ws_path, &rows[i]) != 0) fail++;
|
||||||
|
if (rows[i].fwd) {
|
||||||
|
total++;
|
||||||
|
if (check_movq_bp_di_before_inner_call(ws_path,
|
||||||
|
&rows[i]) != 0) fail++;
|
||||||
|
}
|
||||||
|
|
||||||
/* Byte-id diff between stages. */
|
/* Byte-id diff between stages. */
|
||||||
total++;
|
total++;
|
||||||
|
|||||||
@@ -176,6 +176,83 @@ static const struct row rows[] = {
|
|||||||
" return 0;\n"
|
" return 0;\n"
|
||||||
"};\n",
|
"};\n",
|
||||||
0 },
|
0 },
|
||||||
|
/* sret return-forwarding (task #9 follow-up to #23): outer fn's
|
||||||
|
* body is `return inner(args...)` where outer and inner both
|
||||||
|
* return the same >24B struct shape. Outer reloads its own
|
||||||
|
* @sretarg into RDI and forwards directly into outer's caller-
|
||||||
|
* prealloc dest — no temporary in outer's frame, no struct copy.
|
||||||
|
* Pre-fix both stages emitted a compile-time fatal at this
|
||||||
|
* shape; the user's only workaround was `let r = inner(...);
|
||||||
|
* return r;` (which materialised an intermediate copy). */
|
||||||
|
{ "forward_simple",
|
||||||
|
"type quad = struct { a: i64, b: i64, c: i64, d: i64 };\n"
|
||||||
|
"fn inner(x: i64) quad = {\n"
|
||||||
|
" return quad { a = x, b = x + 1i64, c = x + 2i64, d = x + 3i64 };\n"
|
||||||
|
"};\n"
|
||||||
|
"fn outer(x: i64) quad = {\n"
|
||||||
|
" return inner(x);\n"
|
||||||
|
"};\n"
|
||||||
|
"export fn main() i32 = {\n"
|
||||||
|
" let q: quad = outer(10i64);\n"
|
||||||
|
" if (q.a != 10i64) { return 1; };\n"
|
||||||
|
" if (q.b != 11i64) { return 2; };\n"
|
||||||
|
" if (q.c != 12i64) { return 3; };\n"
|
||||||
|
" if (q.d != 13i64) { return 4; };\n"
|
||||||
|
" return 0;\n"
|
||||||
|
"};\n",
|
||||||
|
0 },
|
||||||
|
/* Non-trivial inner args: multi-field struct + scalar, exercising
|
||||||
|
* arg-marshalling didn't regress under the forwarding path (sister
|
||||||
|
* concern to the 'sret_with_struct16_arg' row above). Outer
|
||||||
|
* forwards a pair-by-value plus a scalar; inner places fields into
|
||||||
|
* the >24B return shape. */
|
||||||
|
{ "forward_multi_arg",
|
||||||
|
"type pair = struct { x: i64, y: i64 };\n"
|
||||||
|
"type quad = struct { a: i64, b: i64, c: i64, d: i64 };\n"
|
||||||
|
"fn inner(p: pair, k: i64) quad = {\n"
|
||||||
|
" return quad { a = p.x, b = p.y, c = k, d = p.x + p.y + k };\n"
|
||||||
|
"};\n"
|
||||||
|
"fn outer(p: pair, k: i64) quad = {\n"
|
||||||
|
" return inner(p, k);\n"
|
||||||
|
"};\n"
|
||||||
|
"export fn main() i32 = {\n"
|
||||||
|
" let p: pair = pair { x = 4i64, y = 6i64 };\n"
|
||||||
|
" let q: quad = outer(p, 9i64);\n"
|
||||||
|
" if (q.a != 4i64) { return 1; };\n"
|
||||||
|
" if (q.b != 6i64) { return 2; };\n"
|
||||||
|
" if (q.c != 9i64) { return 3; };\n"
|
||||||
|
" if (q.d != 19i64) { return 4; };\n"
|
||||||
|
" return 0;\n"
|
||||||
|
"};\n",
|
||||||
|
0 },
|
||||||
|
/* Slice-payload forwarding (utf8 iterator shape): outer forwards a
|
||||||
|
* decoder { i64, []u8 } through inner; the slice ptr/len/cap must
|
||||||
|
* survive the forward intact via the @sretarg pointer chain. */
|
||||||
|
{ "forward_slice_payload",
|
||||||
|
"type decoder = struct { offs: i64, src: []u8 };\n"
|
||||||
|
"fn inner(s: []u8) decoder = {\n"
|
||||||
|
" let r: decoder;\n"
|
||||||
|
" r.offs = 99i64;\n"
|
||||||
|
" r.src = s;\n"
|
||||||
|
" return r;\n"
|
||||||
|
"};\n"
|
||||||
|
"fn outer(s: []u8) decoder = {\n"
|
||||||
|
" return inner(s);\n"
|
||||||
|
"};\n"
|
||||||
|
"export fn main() i32 = {\n"
|
||||||
|
" let buf: [3]u8;\n"
|
||||||
|
" buf[0] = 0x11u8;\n"
|
||||||
|
" buf[1] = 0x22u8;\n"
|
||||||
|
" buf[2] = 0x33u8;\n"
|
||||||
|
" let d: decoder = outer(buf[0:3]);\n"
|
||||||
|
" if (d.offs != 99i64) { return 1; };\n"
|
||||||
|
" if (d.src.len != 3) { return 2; };\n"
|
||||||
|
" if (d.src[0] != 0x11u8) { return 3; };\n"
|
||||||
|
" if (d.src[1] != 0x22u8) { return 4; };\n"
|
||||||
|
" if (d.src[2] != 0x33u8) { return 5; };\n"
|
||||||
|
" return 0;\n"
|
||||||
|
"};\n",
|
||||||
|
0 },
|
||||||
};
|
};
|
||||||
|
|
||||||
static int
|
static int
|
||||||
|
|||||||
Reference in New Issue
Block a user