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

@@ -431,6 +431,29 @@ type cgen = struct {
// later emit reuses. Mirrors c.tagscrsz pattern (#38) but tracks
// offset, not size (per-fn return type is fixed, so size is too).
retscroff: i32,
// System V AMD64 sret discipline (#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.
//
// sretargoff — callee-side @sretarg slot (8B, holds saved RDI).
// Set in cgfn prologue when the fn's return type
// triggers sret. 0 means N/A.
// sretdestoff — caller-side dest BP offset, propagated from a
// receive site (cglet / cgassign ident) to the
// nested cgexpr → cgcall so the call emits
// `LEAQ off(BP), DI` instead of allocating a
// scratch. 0 means no receiver wired.
// sretscroff — per-fn @sretscr discard slot, used by sret CALLs
// whose result has no named receiver. Single-slot
// SSoT mirroring c.retscroff; the scanlocals walk
// sums c.sretscrsz to pre-reserve.
// sretscrsz — max sret discard size in this fn (sums during
// scanlocals, consumed by localadd("@sretscr", ...)).
sretargoff: i32,
sretdestoff: i32,
sretscroff: i32,
sretscrsz: i32,
};
// Top-level mutable `let` registry. Mirrors cmd/w6c/cgen.c LetVar.
@@ -454,6 +477,10 @@ fn cgeninit(c: *cgen, a: *arena) void = {
c.varargseq = 0;
c.tagscrsz = 0;
c.retscroff = 0;
c.sretargoff = 0;
c.sretdestoff = 0;
c.sretscroff = 0;
c.sretscrsz = 0;
// Note: strlit_seq, strlits, ffis are *not* reset here; they
// persist across cgfn calls within one file. cgfile resets them
// at the start of each compilation unit.
@@ -537,6 +564,23 @@ fn localadd(c: *cgen, name: str, sz: i32, tnode: *node) i32 = {
c.retscroff = off;
return off;
};
// @sretarg / @sretscr (#23): same single-slot SSoT
// pattern as @retscr. @sretarg holds the saved hidden
// RDI for sret callees (8B, set once per fn at the
// prologue); @sretscr is the caller-side discard slot
// for sret CALLs whose result is dropped.
if (streq(name, "@sretarg")) {
if (c.sretargoff != 0) { return c.sretargoff; };
let off: i32 = localalloc(c, name, sz, tnode);
c.sretargoff = off;
return off;
};
if (streq(name, "@sretscr")) {
if (c.sretscroff != 0) { return c.sretscroff; };
let off: i32 = localalloc(c, name, sz, tnode);
c.sretscroff = off;
return off;
};
let cur: *local = c.locals;
for (cur != nil) {
let cn: str = cur.name;