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:
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user