diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index f45a5223..00bd3ed3 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -54,11 +54,18 @@ static int cg_retscr; * 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. */ + * discarded sret return type in the fn. + * cg_sret_forward — set by cgreturn `return f();` from an sret callee + * to signal cgcall: source RDI for inner from outer's + * saved @sretarg (MOVQ) instead of LEAQ'ing a local + * dest. Inner writes into outer's caller-prealloc; + * inner's RAX (the dest pointer) is already outer's + * return value. No temporary in outer's frame. */ static int cg_sret_arg_off; static int cg_sret_dest_off; static int cg_sretscr_off; static int cg_sretscr_sz; +static int cg_sret_forward; /* Per-fn defer stack: pushed in registration order, popped (emitted) * in reverse at each return. */ @@ -4547,10 +4554,27 @@ 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)); + * its int-arg cursor at 1, so RDI was never written. + * + * Forwarding (task #9 follow-up): when outer's `return f();` + * forwards through an sret callee, source RDI from outer's + * saved @sretarg — inner writes directly into outer's + * caller-prealloc dest. No temporary in outer's frame. + * The @sretscr slot was still allocated above for byte-id + * lockstep with wwstage's scanlocals reservation; it goes + * unused on the forwarding branch. */ + if (sret_call_sz > 0) { + if (cg_sret_forward) { + ins2(c, A_MOVQ, + amem(D_BP, cg_sret_arg_off), + areg(D_DI)); + cg_sret_forward = 0; + } else { + 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. */ @@ -6548,18 +6572,29 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) 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. */ + /* sret return-forwarding (task #9 follow-up to #23): + * `return f();` where outer + inner both return the + * same >24B struct shape. Outer's @sretarg already + * holds its caller's prealloc dest; pass it to inner + * in RDI (set by cgcall via cg_sret_forward), inner + * writes directly there, inner's RAX (dest pointer) + * is already outer's return value. The trailing + * MOVQ @sretarg(BP), AX is redundant after inner's + * RET but kept for byte-id symmetry with the + * N_IDENT / N_STRUCTLIT arms below. */ 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)"); + && n->lhs->kind == N_CALL) { + cg_sret_forward = 1; + cgexpr(c, n->lhs, *locals); + 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; + } if (rt && rt->kind == TY_STRUCT && (int)rt->size > 24 && (n->lhs->kind == N_IDENT @@ -7061,6 +7096,7 @@ cgfn(Cg *c, FILE *out, Node *fn) cg_sret_dest_off = 0; cg_sretscr_off = 0; cg_sretscr_sz = 0; + cg_sret_forward = 0; int frame = 0; Local *locals = NULL; diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 0a1012c8..93f10387 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -13145,10 +13145,24 @@ 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. + // + // Forwarding (task #9 follow-up): when outer's `return f();` + // forwards through an sret callee, source RDI from outer's + // saved @sretarg — inner writes directly into outer's caller- + // prealloc dest. No temporary in outer's frame. The @sretscr + // slot stays reserved for byte-id with cstage; it goes unused + // on the forwarding branch. if (sretcs > 0) { - emitline("\tLEAQ\t"); - emitoff(sretcalloff: i64); - emitline("(BP), DI\n"); + if (c.sretforward != 0) { + emitline("\tMOVQ\t"); + emitoff(c.sretargoff: i64); + emitline("(BP), DI\n"); + c.sretforward = 0; + } else { + emitline("\tLEAQ\t"); + emitoff(sretcalloff: i64); + emitline("(BP), DI\n"); + }; }; if (isfnptrcall) { // Load fn-ptr field value into AX; CALL AX. We emit the @@ -15838,16 +15852,28 @@ fn cgreturn(c: *cgen, n: *node) void = { if (c.sretargoff != 0) { let scs: i32 = sretretsize(c, c.fnret); if (scs > 0) { - // `return f();` from a sret callee would silent- - // miscompile: cgexpr writes inner's result to - // @sretscr but outer never copies into *@sretarg - // and never sets RAX. Fail loud per CLAUDE.md - // rule 7; the `let r = f(); return r;` workaround - // is already wired and byte-id with cstage. + // sret return-forwarding (task #9 follow-up to + // #23): `return f();` where outer + inner both + // return the same >24B struct shape. Outer's + // @sretarg already holds its caller's prealloc + // dest; pass it to inner in RDI (set by cgcall + // via c.sretforward), inner writes directly + // there, inner's RAX (dest pointer) is already + // outer's return value. The trailing MOVQ + // @sretarg(BP), AX is redundant after inner's + // RET but kept for byte-id symmetry with the + // N_IDENT / N_STRUCTLIT arms below. if (rhs.kind == nkind.N_CALL) { - let m: str = "ww: cgreturn: sret return-forwarding for >24B struct not wired (task #23)\n"; - os.write(2, m.ptr, m.len: u64); - os.exit(1); + c.sretforward = 1; + cgexpr(c, rhs); + emitline("\tMOVQ\t"); + emitoff(c.sretargoff: i64); + emitline("(BP), AX\n"); + emitline("\tMOVQ\tBP, SP\n"); + emitline("\tPOPQ\tBP\n"); + emitline("\tRET\n"); + c.lastwasreturn = 1; + return; }; let okrhs: bool = false; if (rhs.kind == nkind.N_IDENT) { okrhs = true; }; @@ -18440,10 +18466,17 @@ type cgen = struct { // sums c.sretscrsz to pre-reserve. // sretscrsz — max sret discard size in this fn (sums during // scanlocals, consumed by localadd("@sretscr", ...)). + // sretforward — set by cgreturn `return f();` from an sret callee to + // signal cgcall: source RDI for inner from outer's + // saved @sretarg (MOVQ) instead of LEAQ'ing a local + // dest. Inner writes into outer's caller-prealloc; + // inner's RAX (the dest pointer) is already outer's + // return value. Cleared after cgcall consumes it. sretargoff: i32, sretdestoff: i32, sretscroff: i32, sretscrsz: i32, + sretforward: i32, }; // Top-level mutable `let` registry. Mirrors cmd/w6c/cgen.c LetVar. @@ -18471,6 +18504,7 @@ fn cgeninit(c: *cgen, a: *arena) void = { c.sretdestoff = 0; c.sretscroff = 0; c.sretscrsz = 0; + c.sretforward = 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. diff --git a/selfhost/cmd/wcc/cgen.ww b/selfhost/cmd/wcc/cgen.ww index 85541b4a..15e83f47 100644 --- a/selfhost/cmd/wcc/cgen.ww +++ b/selfhost/cmd/wcc/cgen.ww @@ -450,10 +450,17 @@ type cgen = struct { // sums c.sretscrsz to pre-reserve. // sretscrsz — max sret discard size in this fn (sums during // scanlocals, consumed by localadd("@sretscr", ...)). + // sretforward — set by cgreturn `return f();` from an sret callee to + // signal cgcall: source RDI for inner from outer's + // saved @sretarg (MOVQ) instead of LEAQ'ing a local + // dest. Inner writes into outer's caller-prealloc; + // inner's RAX (the dest pointer) is already outer's + // return value. Cleared after cgcall consumes it. sretargoff: i32, sretdestoff: i32, sretscroff: i32, sretscrsz: i32, + sretforward: i32, }; // Top-level mutable `let` registry. Mirrors cmd/w6c/cgen.c LetVar. @@ -481,6 +488,7 @@ fn cgeninit(c: *cgen, a: *arena) void = { c.sretdestoff = 0; c.sretscroff = 0; c.sretscrsz = 0; + c.sretforward = 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. diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index e8458a81..9c99dd54 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -3151,10 +3151,24 @@ 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. + // + // Forwarding (task #9 follow-up): when outer's `return f();` + // forwards through an sret callee, source RDI from outer's + // saved @sretarg — inner writes directly into outer's caller- + // prealloc dest. No temporary in outer's frame. The @sretscr + // slot stays reserved for byte-id with cstage; it goes unused + // on the forwarding branch. if (sretcs > 0) { - emitline("\tLEAQ\t"); - emitoff(sretcalloff: i64); - emitline("(BP), DI\n"); + if (c.sretforward != 0) { + emitline("\tMOVQ\t"); + emitoff(c.sretargoff: i64); + emitline("(BP), DI\n"); + c.sretforward = 0; + } else { + emitline("\tLEAQ\t"); + emitoff(sretcalloff: i64); + emitline("(BP), DI\n"); + }; }; if (isfnptrcall) { // Load fn-ptr field value into AX; CALL AX. We emit the diff --git a/selfhost/cmd/wcc/cgenstmt.ww b/selfhost/cmd/wcc/cgenstmt.ww index b2648219..38ef7c87 100644 --- a/selfhost/cmd/wcc/cgenstmt.ww +++ b/selfhost/cmd/wcc/cgenstmt.ww @@ -300,16 +300,28 @@ fn cgreturn(c: *cgen, n: *node) void = { if (c.sretargoff != 0) { let scs: i32 = sretretsize(c, c.fnret); if (scs > 0) { - // `return f();` from a sret callee would silent- - // miscompile: cgexpr writes inner's result to - // @sretscr but outer never copies into *@sretarg - // and never sets RAX. Fail loud per CLAUDE.md - // rule 7; the `let r = f(); return r;` workaround - // is already wired and byte-id with cstage. + // sret return-forwarding (task #9 follow-up to + // #23): `return f();` where outer + inner both + // return the same >24B struct shape. Outer's + // @sretarg already holds its caller's prealloc + // dest; pass it to inner in RDI (set by cgcall + // via c.sretforward), inner writes directly + // there, inner's RAX (dest pointer) is already + // outer's return value. The trailing MOVQ + // @sretarg(BP), AX is redundant after inner's + // RET but kept for byte-id symmetry with the + // N_IDENT / N_STRUCTLIT arms below. if (rhs.kind == nkind.N_CALL) { - let m: str = "ww: cgreturn: sret return-forwarding for >24B struct not wired (task #23)\n"; - os.write(2, m.ptr, m.len: u64); - os.exit(1); + c.sretforward = 1; + cgexpr(c, rhs); + emitline("\tMOVQ\t"); + emitoff(c.sretargoff: i64); + emitline("(BP), AX\n"); + emitline("\tMOVQ\tBP, SP\n"); + emitline("\tPOPQ\tBP\n"); + emitline("\tRET\n"); + c.lastwasreturn = 1; + return; }; let okrhs: bool = false; if (rhs.kind == nkind.N_IDENT) { okrhs = true; }; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 3cf9e38f..203fd91a 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -13145,10 +13145,24 @@ 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. + // + // Forwarding (task #9 follow-up): when outer's `return f();` + // forwards through an sret callee, source RDI from outer's + // saved @sretarg — inner writes directly into outer's caller- + // prealloc dest. No temporary in outer's frame. The @sretscr + // slot stays reserved for byte-id with cstage; it goes unused + // on the forwarding branch. if (sretcs > 0) { - emitline("\tLEAQ\t"); - emitoff(sretcalloff: i64); - emitline("(BP), DI\n"); + if (c.sretforward != 0) { + emitline("\tMOVQ\t"); + emitoff(c.sretargoff: i64); + emitline("(BP), DI\n"); + c.sretforward = 0; + } else { + emitline("\tLEAQ\t"); + emitoff(sretcalloff: i64); + emitline("(BP), DI\n"); + }; }; if (isfnptrcall) { // Load fn-ptr field value into AX; CALL AX. We emit the @@ -15838,16 +15852,28 @@ fn cgreturn(c: *cgen, n: *node) void = { if (c.sretargoff != 0) { let scs: i32 = sretretsize(c, c.fnret); if (scs > 0) { - // `return f();` from a sret callee would silent- - // miscompile: cgexpr writes inner's result to - // @sretscr but outer never copies into *@sretarg - // and never sets RAX. Fail loud per CLAUDE.md - // rule 7; the `let r = f(); return r;` workaround - // is already wired and byte-id with cstage. + // sret return-forwarding (task #9 follow-up to + // #23): `return f();` where outer + inner both + // return the same >24B struct shape. Outer's + // @sretarg already holds its caller's prealloc + // dest; pass it to inner in RDI (set by cgcall + // via c.sretforward), inner writes directly + // there, inner's RAX (dest pointer) is already + // outer's return value. The trailing MOVQ + // @sretarg(BP), AX is redundant after inner's + // RET but kept for byte-id symmetry with the + // N_IDENT / N_STRUCTLIT arms below. if (rhs.kind == nkind.N_CALL) { - let m: str = "ww: cgreturn: sret return-forwarding for >24B struct not wired (task #23)\n"; - os.write(2, m.ptr, m.len: u64); - os.exit(1); + c.sretforward = 1; + cgexpr(c, rhs); + emitline("\tMOVQ\t"); + emitoff(c.sretargoff: i64); + emitline("(BP), AX\n"); + emitline("\tMOVQ\tBP, SP\n"); + emitline("\tPOPQ\tBP\n"); + emitline("\tRET\n"); + c.lastwasreturn = 1; + return; }; let okrhs: bool = false; if (rhs.kind == nkind.N_IDENT) { okrhs = true; }; @@ -18440,10 +18466,17 @@ type cgen = struct { // sums c.sretscrsz to pre-reserve. // sretscrsz — max sret discard size in this fn (sums during // scanlocals, consumed by localadd("@sretscr", ...)). + // sretforward — set by cgreturn `return f();` from an sret callee to + // signal cgcall: source RDI for inner from outer's + // saved @sretarg (MOVQ) instead of LEAQ'ing a local + // dest. Inner writes into outer's caller-prealloc; + // inner's RAX (the dest pointer) is already outer's + // return value. Cleared after cgcall consumes it. sretargoff: i32, sretdestoff: i32, sretscroff: i32, sretscrsz: i32, + sretforward: i32, }; // Top-level mutable `let` registry. Mirrors cmd/w6c/cgen.c LetVar. @@ -18471,6 +18504,7 @@ fn cgeninit(c: *cgen, a: *arena) void = { c.sretdestoff = 0; c.sretscroff = 0; c.sretscrsz = 0; + c.sretforward = 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. diff --git a/test/wcc/721_sret_struct_return.c b/test/wcc/721_sret_struct_return.c index d8b66ee5..d9f8e133 100644 --- a/test/wcc/721_sret_struct_return.c +++ b/test/wcc/721_sret_struct_return.c @@ -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++; diff --git a/test/wcc/925_sret_struct_return_run.c b/test/wcc/925_sret_struct_return_run.c index 4e819196..eaaf6dba 100644 --- a/test/wcc/925_sret_struct_return_run.c +++ b/test/wcc/925_sret_struct_return_run.c @@ -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