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:
2026-05-18 00:15:34 +09:00
parent 793734c1e0
commit dd274315a0
8 changed files with 359 additions and 57 deletions

View File

@@ -51,7 +51,11 @@ runwait(const char *cmd)
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(...)`
* so the receive site is wired and the sret discipline fires. */
@@ -62,7 +66,7 @@ static const struct row rows[] = {
"fn mk() quad = {\n"
" return quad { a = 1i64, b = 2i64, c = 3i64, d = 4i64 };\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
* []u8 field's slice layout (ptr/len/cap) crosses the AX/DX/CX
* boundary — the wwstage truncation bug dropped the slice tail. */
@@ -78,14 +82,45 @@ static const struct row rows[] = {
" let b: [1]u8;\n"
" let d: decoder = mk(b[0:1]);\n"
" return 0;\n"
"};\n" },
"};\n", 0 },
/* 40B five-i64: second size past 24B, exercises @sretscr sizing. */
{ "five_i64",
"type five = struct { a: i64, b: i64, c: i64, d: i64, e: i64 };\n"
"fn mk() five = {\n"
" return five { a = 1i64, b = 2i64, c = 3i64, d = 4i64, e = 5i64 };\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
@@ -190,6 +225,48 @@ check_movq_bp_ax_before_ret(const char *path, const struct row *r)
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
* NEXT instruction line, there must be NO `MOVQ AX, -K(BP)` (the
* pre-#23 wwstage truncation pattern). The natural sret receive
@@ -254,7 +331,7 @@ main(void)
for (int i = 0; i < n; i++) {
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) {
fprintf(stderr, "row[%s]: w6c failed\n", rows[i].label);
fail++; total++; continue;
@@ -263,10 +340,15 @@ main(void)
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_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; }
/* 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) {
fprintf(stderr,
"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_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 (rows[i].fwd) {
total++;
if (check_movq_bp_di_before_inner_call(ws_path,
&rows[i]) != 0) fail++;
}
/* Byte-id diff between stages. */
total++;

View File

@@ -176,6 +176,83 @@ static const struct row rows[] = {
" return 0;\n"
"};\n",
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