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:
212
cmd/w6c/cgen.c
212
cmd/w6c/cgen.c
@@ -39,6 +39,26 @@ static int *cg_frame;
|
||||
* semantics for synthetic scratches). 0 means "not yet allocated";
|
||||
* negative offsets returned by local_alloc are the live value. */
|
||||
static int cg_retscr;
|
||||
/* System V AMD64 sret discipline (task #23). Plain TY_STRUCT returns
|
||||
* with size > 24B are passed via a hidden first-arg pointer (RDI) to
|
||||
* a caller-prealloc dest; the callee writes through that pointer and
|
||||
* returns it in RAX. Tagged returns (slot ≤ 32B in AX/DX/CX/R8) and
|
||||
* tuples (16/24B in AX/DX/CX) keep their existing register-return ABI.
|
||||
*
|
||||
* cg_sret_arg_off — callee-side @sretarg slot (8B, holds saved RDI).
|
||||
* Set in cgfn prologue when ret > 24B plain struct.
|
||||
* cg_sret_dest_off — caller-side dest offset, propagated from a receive
|
||||
* site (N_LET / N_ASSIGN ident) to the nested N_CALL
|
||||
* so the call emits `LEAQ off(BP), RDI` instead of
|
||||
* allocating a scratch. 0 means no receiver wired.
|
||||
* cg_sretscr_off — per-fn @sretscr discard slot for sret CALLs whose
|
||||
* result is dropped (no named receiver). Single-slot
|
||||
* SSoT mirroring cg_retscr. Sized to the largest
|
||||
* discarded sret return type in the fn. */
|
||||
static int cg_sret_arg_off;
|
||||
static int cg_sret_dest_off;
|
||||
static int cg_sretscr_off;
|
||||
static int cg_sretscr_sz;
|
||||
|
||||
/* Per-fn defer stack: pushed in registration order, popped (emitted)
|
||||
* in reverse at each return. */
|
||||
@@ -71,6 +91,20 @@ cg_isfloat(Type *t)
|
||||
|| t->kind == TY_UNTYPED_FLOAT;
|
||||
}
|
||||
|
||||
/* cg_sret_retsize — if `rt` is a plain TY_STRUCT > 24B, return its
|
||||
* natural size (the sret threshold); else 0. Tagged unions, tuples,
|
||||
* str, and slices route through their existing register-return ABIs
|
||||
* regardless of size. Task #23. */
|
||||
static int
|
||||
cg_sret_retsize(Type *rt)
|
||||
{
|
||||
if (rt == NULL) return 0;
|
||||
if (rt->kind == TY_NAMED) rt = rt->under;
|
||||
if (rt == NULL || rt->kind != TY_STRUCT) return 0;
|
||||
if ((int)rt->size <= 24) return 0;
|
||||
return (int)rt->size;
|
||||
}
|
||||
|
||||
static int
|
||||
node_isfloat(Node *n)
|
||||
{
|
||||
@@ -3629,6 +3663,21 @@ cgexpr(Cg *c, Node *n, Local *locals)
|
||||
}
|
||||
break;
|
||||
}
|
||||
/* sret receive (#23): `s = f();` where s is a struct
|
||||
* local >24B. s's slot IS the caller-prealloc dest;
|
||||
* the callee writes through hidden RDI. Mirrors the
|
||||
* cglet branch above. */
|
||||
if (lu && lu->kind == TY_STRUCT && (int)lu->size > 24
|
||||
&& n->rhs && n->rhs->kind == N_CALL
|
||||
&& n->op == TK_ASSIGN) {
|
||||
int off = localfind(locals, n->lhs->str);
|
||||
if (off != 0) {
|
||||
cg_sret_dest_off = off;
|
||||
cgexpr(c, n->rhs, locals);
|
||||
cg_sret_dest_off = 0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
/* Struct local reassignment: `s = expr;` where s is
|
||||
* a TY_STRUCT local of size <=24B. Two rhs shapes,
|
||||
* mirroring cglet's N_STRUCTLIT and the call-result
|
||||
@@ -4389,11 +4438,48 @@ cgexpr(Cg *c, Node *n, Local *locals)
|
||||
ins1(c, A_PUSHQ, areg(D_AX));
|
||||
}
|
||||
}
|
||||
/* sret discipline (#23): callee returns plain TY_STRUCT
|
||||
* > 24B. Reserve RDI for the hidden dest-pointer arg by
|
||||
* starting the int-arg cursor at 1 and emit the LEAQ AFTER
|
||||
* the pop loop (so the pops don't clobber RDI). The dest
|
||||
* slot is either the receiver's own slot (cg_sret_dest_off,
|
||||
* propagated from N_LET / N_ASSIGN ident receive) or a
|
||||
* per-fn @sretscr discard slot. Sized at the receive site
|
||||
* or here for discards.
|
||||
*
|
||||
* Stack alignment is unaffected because pushargsrev/pops
|
||||
* left RDI free — we never popped a user arg into it. */
|
||||
int sret_call_sz = 0;
|
||||
int sret_call_off = 0;
|
||||
{
|
||||
Type *ret = (cu && cu->kind == TY_FN)
|
||||
? cu->ret : NULL;
|
||||
sret_call_sz = cg_sret_retsize(ret);
|
||||
}
|
||||
if (sret_call_sz > 0) {
|
||||
/* Always pre-allocate @sretscr at the first sret CALL
|
||||
* regardless of whether cg_sret_dest_off is set — keeps
|
||||
* cstage's frame in lockstep with wwstage's
|
||||
* scanlocals-based reservation. Single-slot SSoT (cg_
|
||||
* sretscr_off) mirrors @retscr / @tagscr conventions. */
|
||||
if (cg_sretscr_off == 0) {
|
||||
cg_sretscr_off = local_alloc(c,
|
||||
&locals, "@sretscr",
|
||||
sret_call_sz, cg_frame);
|
||||
cg_sretscr_sz = sret_call_sz;
|
||||
}
|
||||
if (cg_sret_dest_off != 0) {
|
||||
sret_call_off = cg_sret_dest_off;
|
||||
cg_sret_dest_off = 0;
|
||||
} else {
|
||||
sret_call_off = cg_sretscr_off;
|
||||
}
|
||||
}
|
||||
/* pop forward into the right register class. Args that
|
||||
* don't fit in regs stay on the stack and are reached by
|
||||
* the callee via positive offsets from BP. The caller is
|
||||
* responsible for cleaning them up after CALL. */
|
||||
int ii = 0, fi = 0, stackslots = 0;
|
||||
int ii = (sret_call_sz > 0) ? 1 : 0, fi = 0, stackslots = 0;
|
||||
for (int i = 0; i < argcount; i++) {
|
||||
if (widen[i]) {
|
||||
/* Pop widened tagged slot into arg-register
|
||||
@@ -4459,6 +4545,12 @@ cgexpr(Cg *c, Node *n, Local *locals)
|
||||
}
|
||||
}
|
||||
}
|
||||
/* sret hidden first-arg (#23): load &dest into RDI AFTER
|
||||
* all user-arg pops have finished — the pop loop started
|
||||
* 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),
|
||||
areg(D_DI));
|
||||
/* SysV: variadic callees require AL to hold the count of
|
||||
* XMM regs used in the variable portion. We don't pass
|
||||
* floats yet, so AL=0 covers every case we emit. */
|
||||
@@ -6132,6 +6224,18 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
|
||||
cg_structlit_fill_bp(c, locals, lu, n->rhs, off);
|
||||
break;
|
||||
}
|
||||
/* sret receive (#23): plain TY_STRUCT >24B. The let's own
|
||||
* slot IS the caller-prealloc dest; the call writes
|
||||
* through hidden RDI directly into our slot, no AX/DX/CX
|
||||
* shuffle. Set cg_sret_dest_off so the nested cgexpr →
|
||||
* N_CALL path emits `LEAQ off(BP), RDI` before CALL. */
|
||||
if (n->rhs && n->rhs->kind == N_CALL && lu
|
||||
&& lu->kind == TY_STRUCT && sz > 24) {
|
||||
cg_sret_dest_off = off;
|
||||
cgexpr(c, n->rhs, *locals);
|
||||
cg_sret_dest_off = 0;
|
||||
break;
|
||||
}
|
||||
/* 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], zero-
|
||||
@@ -6435,10 +6539,91 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
|
||||
break;
|
||||
}
|
||||
}
|
||||
/* sret return (#23): plain TY_STRUCT >24B. Callee writes
|
||||
* the value through `*(@sretarg)` (the caller-prealloc
|
||||
* dest passed in RDI at entry; saved to @sretarg in the
|
||||
* prologue), then loads @sretarg into RAX and rets — the
|
||||
* SysV sret discipline of "return the pointer". No
|
||||
* AX/DX/CX shuffle, no scratch slot beyond @sretarg. */
|
||||
if (n->lhs && cg_ret_type && cg_sret_arg_off != 0) {
|
||||
Type *rt = cg_ret_type;
|
||||
if (rt->kind == TY_NAMED) rt = rt->under;
|
||||
/* `return f();` from a sret callee falls through the
|
||||
* arm below (rhs is N_CALL, not N_IDENT/N_STRUCTLIT)
|
||||
* and would silent-miscompile: cgexpr places inner's
|
||||
* result in @sretscr but outer never copies into
|
||||
* *@sretarg and never sets RAX. Fail loud per
|
||||
* CLAUDE.md rule 7; the workaround `let r = f();
|
||||
* return r;` is already wired and correct. */
|
||||
if (rt && rt->kind == TY_STRUCT
|
||||
&& (int)rt->size > 24
|
||||
&& n->lhs->kind == N_CALL)
|
||||
fatal("cgreturn: sret return-forwarding "
|
||||
"for >24B struct not wired (task #23)");
|
||||
if (rt && rt->kind == TY_STRUCT
|
||||
&& (int)rt->size > 24
|
||||
&& (n->lhs->kind == N_IDENT
|
||||
|| n->lhs->kind == N_STRUCTLIT)) {
|
||||
int sz = (int)rt->size;
|
||||
if (n->lhs->kind == N_STRUCTLIT) {
|
||||
/* Delegate to the shared *-relative
|
||||
* fill helper. Same store sequence the
|
||||
* ≤24B path emits, but the base reg is
|
||||
* reloaded from @sretarg(BP) before each
|
||||
* field store. Mirrors DST_PTR_LOCAL
|
||||
* usage at N_ASSIGN N_DOT via_ptr. */
|
||||
cg_structlit_fill(c, locals, rt,
|
||||
n->lhs, DST_PTR_LOCAL,
|
||||
cg_sret_arg_off, NULL, 0);
|
||||
} else {
|
||||
/* N_IDENT: word-copy from rhs slot to
|
||||
* *(@sretarg). Whole 8B words via MOVQ;
|
||||
* trailing partial words via MOVL/MOVB
|
||||
* so the read stays inside the source
|
||||
* slot's declared size. */
|
||||
int rhsoff = localfind(*locals,
|
||||
n->lhs->str);
|
||||
ins2(c, A_MOVQ,
|
||||
amem(D_BP, cg_sret_arg_off),
|
||||
areg(D_BX));
|
||||
int k = 0;
|
||||
while (k + 8 <= sz) {
|
||||
ins2(c, A_MOVQ,
|
||||
amem(D_BP, rhsoff + k),
|
||||
areg(D_AX));
|
||||
ins2(c, A_MOVQ, areg(D_AX),
|
||||
amem(D_BX, k));
|
||||
k += 8;
|
||||
}
|
||||
while (k + 4 <= sz) {
|
||||
ins2(c, A_MOVL,
|
||||
amem(D_BP, rhsoff + k),
|
||||
areg(D_AX));
|
||||
ins2(c, A_MOVL, areg(D_AX),
|
||||
amem(D_BX, k));
|
||||
k += 4;
|
||||
}
|
||||
while (k < sz) {
|
||||
ins2(c, A_MOVB,
|
||||
amem(D_BP, rhsoff + k),
|
||||
areg(D_AX));
|
||||
ins2(c, A_MOVB, areg(D_AX),
|
||||
amem(D_BX, k));
|
||||
k += 1;
|
||||
}
|
||||
}
|
||||
/* sret return: RAX = dest pointer. */
|
||||
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;
|
||||
}
|
||||
}
|
||||
/* Whole-struct return for sizes ≤24B. ABI: AX=bytes[0..7],
|
||||
* DX=bytes[8..15], CX=bytes[16..23]. Sizes >24B are not
|
||||
* wired (sret deferred); they fall through to the scalar
|
||||
* path below and return only AX. Materialise rhs into a
|
||||
* DX=bytes[8..15], CX=bytes[16..23]. Sizes >24B route
|
||||
* through the sret arm above. Materialise rhs into a
|
||||
* zero-padded 24B scratch slot, then emit AX/DX/CX loads
|
||||
* unconditionally so the instruction shape is constant
|
||||
* regardless of declared struct size. The receive side
|
||||
@@ -6872,6 +7057,10 @@ cgfn(Cg *c, FILE *out, Node *fn)
|
||||
nloops = 0;
|
||||
cg_ret_type = fn->type ? fn->type->ret : NULL;
|
||||
cg_retscr = 0;
|
||||
cg_sret_arg_off = 0;
|
||||
cg_sret_dest_off = 0;
|
||||
cg_sretscr_off = 0;
|
||||
cg_sretscr_sz = 0;
|
||||
|
||||
int frame = 0;
|
||||
Local *locals = NULL;
|
||||
@@ -6893,10 +7082,23 @@ cgfn(Cg *c, FILE *out, Node *fn)
|
||||
subsp->to = areg(D_SP);
|
||||
emit(c, subsp);
|
||||
|
||||
/* sret discipline (#23): plain TY_STRUCT return > 24B consumes
|
||||
* RDI as a hidden first-arg dest pointer. Spill it to @sretarg
|
||||
* before the user-param loop so cgreturn can write through it,
|
||||
* and start the user-arg register counter at 1 to shift every
|
||||
* declared arg right by one (SI/DX/CX/R8/R9/+stack). */
|
||||
if (cg_sret_retsize(cg_ret_type) > 0) {
|
||||
cg_sret_arg_off = local_alloc(c, &locals, "@sretarg",
|
||||
8, &frame);
|
||||
ins2(c, A_MOVQ, areg(D_DI),
|
||||
amem(D_BP, cg_sret_arg_off));
|
||||
}
|
||||
|
||||
/* spill incoming arg registers to local slots. Slice params
|
||||
* occupy 24 bytes; float params land in XMM0..7 (counted
|
||||
* separately from integer DI/SI/DX/CX/R8/R9). */
|
||||
int argi = 0, fargi = 0;
|
||||
int argi = (cg_sret_arg_off != 0) ? 1 : 0;
|
||||
int fargi = 0;
|
||||
Tparam *tp = fn->type ? fn->type->params : NULL;
|
||||
for (Node *p = fn->list; p; p = p->next) {
|
||||
if (p->str == NULL || strcmp(p->str, "...") == 0) {
|
||||
|
||||
Reference in New Issue
Block a user