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:
2026-05-17 23:17:03 +09:00
parent 6ab865d933
commit 7e0c280691
12 changed files with 1723 additions and 42 deletions

View File

@@ -2991,6 +2991,24 @@ fn cgcall(c: *cgen, n: *node) void = {
};
};
let nargs: i32 = pushargsrev(c, n.list, calleeparams);
// sret call (#23): callee returns plain TY_STRUCT > 24B. The
// dest pointer lands in RDI; start intidx at 1 to skip RDI in
// the user-arg pop loop and emit `LEAQ off(BP), DI` AFTER all
// pops have finished (so they don't clobber RDI). The dest off
// is either the receive site's slot (c.sretdestoff, propagated
// from cglet / cgassign ident) or the per-fn @sretscr discard
// slot reserved by scanlocals.
let sretcs: i32 = callsretsize(c, n);
let sretcalloff: i32 = 0;
if (sretcs > 0) {
if (c.sretdestoff != 0) {
sretcalloff = c.sretdestoff;
c.sretdestoff = 0;
} else {
sretcalloff = localadd(c, "@sretscr",
c.sretscrsz, nil);
};
};
// Pop forward. Float args were pushed as 8 bytes from X0 via
// SUBQ+MOVSD; pop into the XMM stream (X0..X7). Everything else
// pops into the int stream (DI..R9) per the SysV ABI. Walk the
@@ -2999,6 +3017,7 @@ fn cgcall(c: *cgen, n: *node) void = {
// the remaining slots stay on the stack and the callee reads them
// via 16+8*k(BP). Caller-cleanup is emitted after the CALL.
let intidx: i32 = 0;
if (sretcs > 0) { intidx = 1; };
let fpidx: i32 = 0;
let a: *node = n.list;
let popped: i32 = 0;
@@ -3129,6 +3148,14 @@ fn cgcall(c: *cgen, n: *node) void = {
};
};
};
// sret hidden first-arg (#23): load &dest into RDI AFTER all
// user-arg pops have finished — intidx started at 1 so RDI was
// never written. The CALL emit follows immediately.
if (sretcs > 0) {
emitline("\tLEAQ\t");
emitoff(sretcalloff: i64);
emitline("(BP), DI\n");
};
if (isfnptrcall) {
// Load fn-ptr field value into AX; CALL AX. We emit the
// load AFTER the args have been popped (so AX/BX/etc
@@ -5304,6 +5331,21 @@ fn cgassign(c: *cgen, n: *node) void = {
};
if (n.rhs != nil
&& n.rhs.kind == nkind.N_CALL) {
// sret receive (#23): plain
// TY_STRUCT > 24B from a CALL.
// `s` is the prealloc dest; the
// callee writes through hidden RDI
// directly into off(BP). Mirror of
// cglet's sret branch.
if (lcnsz > 24) {
let rscs: i32 = callsretsize(c, n.rhs);
if (rscs > 0) {
c.sretdestoff = off;
cgexpr(c, n.rhs);
c.sretdestoff = 0;
return;
};
};
let lcsz: i32 = lcnsz;
if (lcsz <= 24) {
let tlm: i32 = lcsz - (lcsz / 8) * 8;