From 4f3967835e6f8c69631eb60cfef9f181af7917d2 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Thu, 4 Jun 2026 03:43:44 +0900 Subject: [PATCH] w6c+w6c_ww: tagged sret for slot>32B returns (fix #38) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A tagged-union RETURN rides a fixed AX(tag)+DX/CX/R8 cursor (TUPLE_GPCAP eightbytes = 32B slot); wider slots were silently truncated at the return crossing — payload word 4+ built in the callee frame and died there, byte-identical on both stages (gate-blind). Blocks regex fold-2a ((regex | error | nomem) = 64B slot). Classifier: cg_sret_retsize / sretretsize gain a TY_TAGGED arm (<= TUPLE_GPCAP*8 stays register-ABI — the (str|nomem)/(s3|bool) 32B boundary class is pinned unchanged byte-for-byte vs master). Callee: cgreturn writes the slot through *(@sretarg) via the existing widener non-BP base (bare return stores the void tag); exact-type 'return f();' rides the #9 sret-forward. Receive: let/assign/discard reuse the generic #23/#10 sret protocol; the match scrutinee passes its spill slot as the sret dest (tagged-specific, no tuple precedent). This could NOT land as a gate-first interim loud-stop (the planned #38a): lib/errors/errors.ww errno() already returns a 40B (errors.error) slot in-tree — the cgenstmt.ww-documented #222 latent — so a bare gate breaks the build. errno graduates to sret here instead; errnotest pins it at runtime (its cstage run; the wwstage run was already failing at master via an unrelated pre-existing indirect-call arg-classification divergence, reported separately) and test/926's errno-shaped row reads the previously-dropped tail word on both stages. The unwired cursor consumers of an sret-class call result loud-stop (rule 7) rather than read a cursor the callee no longer fills: widening forward/receive ((A|B)->(A|B|C) mem-to-mem tag-remap, filed #40), ?/!/is/as operands, argument position, and the >48B tagged-arg class both stages previously mishandled silently. One-class-one-commit per the #133 carve-out: post-flip those consumers would read AX (now the dest pointer) as the tag — a gates-trailing commit would leave a silently-wrong bisect point, so the flip and its gates are not separable. test/926: 15 rows — 56B regex-shaped round-trips (literal/local/ assign/match-scrutinee/forward/str-variant/multi-call), 40B repro + bare-return-void, the errno-shaped tail-read graduation row, 32B boundary rows pinned register-ABI by asm sentinel, and 3 loud-stop rows pinned as build failures on both stages. --- Makefile | 7 + cmd/w6c/cgen.c | 232 +++++++++++- selfhost/cmd/w6c/main.combined.ww | 309 +++++++++++++++- selfhost/cmd/wcc/cgenexpr.ww | 124 +++++++ selfhost/cmd/wcc/cgenstmt.ww | 121 ++++++- selfhost/cmd/wcc/cgenutil.ww | 64 +++- selfhost/cmd/wwdump/main.combined.ww | 309 +++++++++++++++- test/wcc/926_tagged_sret_run.c | 516 +++++++++++++++++++++++++++ 8 files changed, 1660 insertions(+), 22 deletions(-) create mode 100644 test/wcc/926_tagged_sret_run.c diff --git a/Makefile b/Makefile index 0f8fd119..69e01729 100644 --- a/Makefile +++ b/Makefile @@ -279,6 +279,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_nested_union_widen_run \ $(BIN)/test_sret_struct_return \ $(BIN)/test_sret_struct_return_run \ + $(BIN)/test_tagged_sret_run \ $(BIN)/test_global_sret_run \ $(BIN)/test_sret_narrow_field \ $(BIN)/test_sret_narrow_field_run \ @@ -1164,6 +1165,12 @@ $(BIN)/test_sret_struct_return_run: test/wcc/925_sret_struct_return_run.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_tagged_sret_run: test/wcc/926_tagged_sret_run.c \ + $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ + $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_global_sret_run: test/wcc/940_global_sret_run.c \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index b3bfccb3..cdd48c01 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -322,6 +322,15 @@ cg_sret_retsize(Type *rt) if (rt == NULL) return 0; if (rt->kind == TY_STRUCT) return (int)rt->size <= 24 ? 0 : (int)rt->size; + /* #38: a tagged union rides AX(tag)+DX/CX/R8 = TUPLE_GPCAP + * eightbytes; a wider slot was silently truncated (payload word + * 4+ died in the callee frame). The ≤cap boundary is load-bearing: + * (str|nomem)-shaped 32B slots MUST stay register-ABI or every + * such consumer in the tree flips. Nullable folds to one word. */ + if (rt->kind == TY_TAGGED) { + if (rt->nullable) return 0; + return (int)rt->size <= TUPLE_GPCAP * 8 ? 0 : (int)rt->size; + } /* #267: arrays ride the struct-return ABI — same ≤24 reg / >24 sret * split. Pure-int element arrays only; no float-array-return * consumer exists, so struct_float_class stays struct-only. */ @@ -2163,6 +2172,16 @@ cg_widen_tagged_store(Cg *c, Local **locals_p, Type *dst, Node *src, amem(D_BP, write_off + 8 + k)); } } else { + /* #38b: an sret-classified call result is in + * memory (AX = dest pointer), not the cursor — + * the spill below would store the pointer as + * the payload. Mem-to-mem widen is #40. */ + if (src->kind == N_CALL + && cg_sret_retsize(st) > 0) + fatal("#40: sret-class call result " + "cannot be cursor-widened into a " + "tagged slot (mem-to-mem widen " + "unwired)"); cgexpr(c, src, *locals_p); ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, write_off + 8)); @@ -2196,6 +2215,12 @@ cg_widen_tagged_store(Cg *c, Local **locals_p, Type *dst, Node *src, * variant-widen) so the unconditional store here is * safe even when the source variant has fewer payload * words than the dst slot. */ + /* #38b: an sret-classified call result is in memory + * (AX = dest pointer), not the cursor. #40. */ + if (src->kind == N_CALL && cg_sret_retsize(st) > 0) + fatal("#40: sret-class call result cannot be " + "cursor-widened into a tagged slot " + "(mem-to-mem widen unwired)"); cgexpr(c, src, *locals_p); ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, write_off + 0)); @@ -2345,7 +2370,7 @@ cg_widen_tagged_store(Cg *c, Local **locals_p, Type *dst, Node *src, ? ftype->under : ftype; /* str IS []u8 and a slice is the same 3-word * {ptr,len,cap} header from cgexpr's AX/BX/CX - * (#1/Phase 3). The slice arm rides #38's + * (#1/Phase 3). The slice arm rides #38b's * regex-shaped consumer (slice fields inside a * union-payload struct literal); the prior * str-only gate dropped .len/.cap via the @@ -5626,17 +5651,36 @@ cgexpr(Cg *c, Node *n, Local *locals) /* Plain `r = expr;` where r is a tagged-union local. * Delegates to cg_widen_tagged_store: covers nullable fold, * tagged→tagged (with tag remap), struct payload (ident or - * literal), str payload, and scalar payload. */ + * literal), str payload, and scalar payload. + * + * #38b: an sret-classified tagged CALL result is in memory, + * not the cursor — an exact-type reassign falls through to + * the generic sret receive below; a widening receive needs + * mem-to-mem tag-remap (#40, unwired). */ if (n->lhs && n->lhs->kind == N_IDENT && n->op == TK_ASSIGN && n->lhs->type) { Type *lt = n->lhs->type; Type *lu = (lt && lt->kind == TY_NAMED) ? lt->under : lt; if (lu && lu->kind == TY_TAGGED) { - int off = localfind(locals, n->lhs->str); - if (off == 0) break; - cg_widen_tagged_store(c, &locals, lu, n->rhs, - D_BP, off, (int)lu->size); - break; + int rhs_sret_call = n->rhs + && n->rhs->kind == N_CALL + && cg_sret_retsize(n->rhs->type) > 0; + if (!rhs_sret_call) { + int off = localfind(locals, + n->lhs->str); + if (off == 0) break; + cg_widen_tagged_store(c, &locals, lu, + n->rhs, D_BP, off, (int)lu->size); + break; + } + Type *ru = type_chase_named(n->rhs->type); + if (!(ru == lu || type_eq(n->rhs->type, lt))) + fatal("#40: sret-class call result " + "cannot be widened into a tagged " + "slot (mem-to-mem widen unwired)"); + if (localfind(locals, n->lhs->str) == 0) + fatal("#38b: sret receive into a " + "tagged GLOBAL lvalue unwired"); } } /* Deref-target assignment `*p = v;`. The size of the store is @@ -7020,6 +7064,20 @@ cgexpr(Cg *c, Node *n, Local *locals) args[i], widen_sz[i]); continue; } + /* #38b residual (rule 7): a tagged arg slot past the + * 6-reg arg capacity has no push shape — + * tagged_arg_size returns 0 ("too large") and the + * scalar default silently pushed ONE word. Loud-stop; + * symmetric ww gate in pushargsrev. */ + { + Type *au = type_chase_named(args[i]->type); + if (au && au->kind == TY_TAGGED + && !au->nullable + && tagged_arg_size(args[i]->type) == 0) + fatal("#38b: tagged arg exceeds the " + "register arg capacity (>48B " + "slot) — unwired"); + } cgexpr(c, args[i], locals); Type *tuparg_push = node_tuplearg(args[i]); if (node_isfloat(args[i])) { @@ -7052,6 +7110,15 @@ cgexpr(Cg *c, Node *n, Local *locals) * values into arg-reg[1..]. Nullable (sz=8): * AX holds the pointer, no value-word * registers — push just AX. */ + /* #38b residual (rule 7): an sret-class call + * result is in memory, not the cursor — the + * @aggargscr-style receive-then-push is the + * #40-family follow-up. */ + if (args[i]->kind == N_CALL + && cg_sret_retsize(args[i]->type) > 0) + fatal("#38b: >32B tagged call result " + "as a call argument unwired " + "(#40-family follow-up)"); int sz = tagged_arg_size(args[i]->type); if (sz > 24) ins1(c, A_PUSHQ, areg(D_R8)); @@ -7517,6 +7584,16 @@ cgexpr(Cg *c, Node *n, Local *locals) * spill only that. */ sl_off = localoff(c, &locals, "@match_spill", slot_size, cg_frame); + if (s->kind == N_CALL && cg_sret_retsize(st) > 0) { + /* #38b: sret-classified tagged call — pass the + * scrut slot itself as the sret dest and skip + * the cursor spill; downstream tag dispatch / + * case-let binds already read the slot from + * memory. */ + cg_sret_dest_off = sl_off; + cgexpr(c, s, locals); + cg_sret_dest_off = 0; + } else { cgexpr(c, s, locals); ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, sl_off + 0)); if (!is_nullable) { @@ -7529,6 +7606,7 @@ cgexpr(Cg *c, Node *n, Local *locals) ins2(c, A_MOVQ, areg(D_R8), amem(D_BP, sl_off + 24)); } + } } char *end = mklabel(c, "match_end"); /* Push the end label as the yield target for arm bodies. */ @@ -7648,6 +7726,20 @@ cgexpr(Cg *c, Node *n, Local *locals) * (any non-null), void variant is the error (null). The * enclosing fn's null encoding is the same — RET with AX=0 * if propagating; otherwise leave AX as-is on success. */ + /* #38b residuals (rule 7): the cursor read below cannot see + * an sret-classified call result (AX = dest pointer), and the + * propagate-RET below cannot speak an sret-classified + * enclosing return (the caller reads memory, not the + * cursor). Both are unwired follow-ups of #40's family. */ + if (n->lhs && n->lhs->kind == N_CALL + && cg_sret_retsize(n->lhs->type) > 0) + fatal("#38b: `?` on an sret-class call result " + "unwired (mem-based unwrap is a #40-family " + "follow-up)"); + if (cg_sret_retsize(cg_ret_type) > 0) + fatal("#38b: `?` propagation into a >32B tagged " + "return unwired (sret error-propagate is a " + "#40-family follow-up)"); cgexpr(c, n->lhs, locals); Type *u = n->lhs ? n->lhs->type : NULL; if (u && u->kind == TY_NAMED) u = u->under; @@ -7728,6 +7820,12 @@ cgexpr(Cg *c, Node *n, Local *locals) /* On error variant: exit(1) directly via the syscall. * Nullable: null = error; non-null = success (AX is the * pointer, ready to use). */ + /* #38b residual (rule 7): see the N_TRYPROP twin. */ + if (n->lhs && n->lhs->kind == N_CALL + && cg_sret_retsize(n->lhs->type) > 0) + fatal("#38b: `!` on an sret-class call result " + "unwired (mem-based unwrap is a #40-family " + "follow-up)"); cgexpr(c, n->lhs, locals); Type *u = n->lhs ? n->lhs->type : NULL; if (u && u->kind == TY_NAMED) u = u->under; @@ -7779,6 +7877,13 @@ cgexpr(Cg *c, Node *n, Local *locals) /* `e is T` — Compare scrutinee tag against T's variant index. * Result is bool (0/1) in AX. Nullable: discriminator is * pointer-vs-null, not a tag. */ + /* #38b residual (rule 7): an sret-class call result leaves + * AX = dest pointer, not the tag — mem-based test is a + * #40-family follow-up. */ + if (n->lhs && n->lhs->kind == N_CALL + && cg_sret_retsize(n->lhs->type) > 0) + fatal("#38b: `is` on an sret-class call result " + "unwired (#40-family follow-up)"); cgexpr(c, n->lhs, locals); Type *u = n->lhs ? n->lhs->type : NULL; if (u && u->kind == TY_NAMED) u = u->under; @@ -7835,6 +7940,11 @@ cgexpr(Cg *c, Node *n, Local *locals) } } int slot_size = (u && u->kind == TY_TAGGED) ? (int)u->size : 16; + /* #38b residual (rule 7): the @asrt_spill below reads the + * cursor, which an sret-class call result never fills. */ + if (s && s->kind == N_CALL && cg_sret_retsize(st) > 0) + fatal("#38b: `as` on an sret-class call result " + "unwired (#40-family follow-up)"); int sl_off = 0; if (s && s->kind == N_IDENT && s->str) { sl_off = localfind(locals, s->str); @@ -9327,14 +9437,43 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) } break; } + /* #38b residual (rule 7): `let w: T = f()?;` / `f()!` where f + * returns an sret-classified tagged union — the unwrap would + * need a mem-based read of the sret slot. The N_LET arms + * below have no TRYUNW/TRYPROP shape for a >8B lt, so the + * rhs was SILENTLY dropped (no CALL emitted; wwstage's cglet + * default does cgexpr and hits the cgtryunw/cgtryprop gates — + * this keeps acceptance symmetric, rule 10). */ + if (n->rhs + && (n->rhs->kind == N_TRYUNW || n->rhs->kind == N_TRYPROP) + && n->rhs->lhs && n->rhs->lhs->kind == N_CALL + && cg_sret_retsize(n->rhs->lhs->type) > 0) + fatal("#38b: `?`/`!` on an sret-class call result " + "unwired (mem-based unwrap is a #40-family " + "follow-up)"); /* Tagged-union initialiser. Delegates to cg_widen_tagged_store, * which handles nullable fold, tagged→tagged (with tag remap * when variant indices differ), struct payload (ident or * literal — field-by-field at slot+8+field_off), str payload, - * and scalar payload (with zero-pad to the slot size). */ + * and scalar payload (with zero-pad to the slot size). + * + * #38b: an sret-classified tagged CALL result is in memory, + * not the cursor — an exact-type receive falls through to the + * generic sret receive below (the let's slot IS the dest); a + * widening receive needs mem-to-mem tag-remap (#40, unwired). */ if (n->rhs && lu && lu->kind == TY_TAGGED) { - cg_widen_tagged_store(c, locals, lu, n->rhs, D_BP, off, sz); - break; + int rhs_sret_call = n->rhs->kind == N_CALL + && cg_sret_retsize(n->rhs->type) > 0; + if (!rhs_sret_call) { + cg_widen_tagged_store(c, locals, lu, n->rhs, + D_BP, off, sz); + break; + } + Type *ru = type_chase_named(n->rhs->type); + if (!(ru == lu || type_eq(n->rhs->type, lt))) + fatal("#40: sret-class call result cannot be " + "widened into a tagged slot (mem-to-mem " + "widen unwired)"); } /* Every slice initialiser routes here — fn-return, slice * ident, slice param, and sub-slice `buf[lo:hi]`. cgexpr @@ -9749,6 +9888,27 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) Type *rt = cg_ret_type; if (rt->kind == TY_NAMED) rt = rt->under; if (rt && rt->kind == TY_TAGGED) { + /* #38b: an sret-classified tagged return (slot + * > the AX/DX/CX/R8 cursor) writes the void- + * variant tag through *(@sretarg) and returns + * the dest pointer — the cursor can't carry the + * slot and the caller reads memory. */ + if (cg_sret_retsize(rt) > 0) { + int tag = cg_tag_for_variant(rt, ty_void); + if (tag < 0) tag = 0; + ins2(c, A_MOVQ, + amem(D_BP, cg_sret_arg_off), + areg(D_BX)); + ins2(c, A_MOVQ, aimm(tag), + amem(D_BX, 0)); + 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->nullable) { /* bare `return;` is the void/null * variant: emit AX = 0. */ @@ -9796,6 +9956,58 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) * arm below zeroed the whole value (never packed the * operands). */ int istuple = vu && vu->kind == TY_TUPLE; + /* #38b: sret-classified tagged return (slot > + * the AX/DX/CX/R8 cursor). Three shapes: + * - exact-type N_CALL forward: inner sret's + * straight into outer's caller dest (#9 + * shape, cg_sret_forward). + * - widening from an sret-class tagged source + * ((A|B)→(A|B|C) mem-to-mem tag-remap): + * unwired, loud-stop — #40. + * - everything else: cg_widen_tagged_store + * through *(@sretarg) (the widener already + * speaks non-BP bases, the #34 precedent), + * then return the dest pointer. */ + if (cg_sret_retsize(rt) > 0) { + int sz = (int)rt->size; + if (passthrough) { + /* exact type but a cursor source + * (N_INDEX/N_DOT) can't carry + * >32B — loud-stop (rule 7, + * #38b residual). */ + if (n->lhs->kind != N_CALL) + fatal("#38b: >32B tagged " + "return from a cursor " + "source (kind %d) " + "unsupported", + n->lhs->kind); + cg_sret_forward = 1; + cgexpr(c, n->lhs, *locals); + ins2(c, A_MOVQ, + amem(D_BP, cg_sret_arg_off), + areg(D_AX)); + } else if (istagged + && n->lhs->kind != N_IDENT + && (int)vu->size > TUPLE_GPCAP * 8) { + fatal("#40: widening tagged " + "return-forward of a >32B " + "source needs mem-to-mem " + "tag-remap (unwired)"); + } else { + ins2(c, A_MOVQ, + amem(D_BP, cg_sret_arg_off), + areg(D_BX)); + cg_widen_tagged_store(c, locals, + rt, n->lhs, D_BX, 0, sz); + 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->nullable) { cgexpr(c, n->lhs, *locals); } else if (passthrough) { diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 00555080..2e1790e6 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -15796,6 +15796,27 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = { let nextparam: *node = nil; if (param != nil) { nextparam = param.next; }; let rest: i32 = pushargsrev(c, arg.next, nextparam); + // #38b residual (rule 7): a tagged arg slot past the 6-reg arg + // capacity has no push shape — cstage tagged_arg_size returns 0 + // ("too large") and both stages fell to divergent silent pushes + // (cs one word, ww slot words). Mirrors cstage cgcall's gate. + { + let a48: *tinfo = arg.type_: *tinfo; + for (a48 != nil && a48.kind == tykind.TY_NAMED) { + a48 = a48.under; + }; + if (a48 != nil) { + if (a48.kind == tykind.TY_TAGGED && a48.nullable == 0) { + // sizelint-ok: 6 SysV int arg regs (DI..R9) x + // 8B words = the cstage tagged_arg_size cap. + if (a48.size: i32 > 6 * 8) { + let m48: str = "#38b: tagged arg exceeds the register arg capacity (>48B slot) — unwired\n"; + os.write(2, m48.ptr, m48.len: u64); + os.exit(1); + }; + }; + }; + }; // Implicit widening from a concrete variant to a tagged-union // parameter slot. Skips when the arg is already a tagged local // (line 121's slice-or-tagged shortcut handles that). @@ -16342,6 +16363,15 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = { // tag first. Mirrors cstage at cmd/w6c/cgen.c:4373-4387. let tcs: i32 = taggedcallslot(c, arg); if (tcs > 0) { + // #38b residual (rule 7): an sret-class call result is in + // memory, not the cursor — the @aggargscr-style receive-then- + // push is the #40-family follow-up. Mirrors cstage cgen.c + // cgcall tagged arg-push gate. + if (callsretsize(c, arg) > 0) { + let m38r: str = "#38b: >32B tagged call result as a call argument unwired (#40-family follow-up)\n"; + os.write(2, m38r.ptr, m38r.len: u64); + os.exit(1); + }; if (tcs > 24) { emitline("\tPUSHQ\tR8\n"); }; if (tcs > 16) { emitline("\tPUSHQ\tCX\n"); }; if (tcs > 8) { emitline("\tPUSHQ\tDX\n"); }; @@ -17095,6 +17125,18 @@ export fn sretretsize(c: *cgen, t: *node) i32 = { r = r.lhs; if (r == nil) { return 0; }; }; + // #38: a tagged union rides AX(tag)+DX/CX/R8 = TUPLE_GPCAP + // eightbytes; a wider slot was silently truncated (payload word + // 4+ died in the callee frame). The ≤cap boundary is load-bearing: + // (str|nomem)-shaped 32B slots MUST stay register-ABI or every + // such consumer in the tree flips. Nullable folds to one word. + // Mirrors cstage cg_sret_retsize TY_TAGGED arm. + if (istaggedtype(c, r)) { + if (isnullabletype(r)) { return 0; }; + let tsz38: i32 = slotsize(c, r); + if (tsz38 <= TUPLE_GPCAP * 8) { return 0; }; + return tsz38; + }; if (r.kind == nkind.N_TTUPLE) { // #10: over-cap tuple → sret. Walk the element TYPE nodes // (pt.lhs) over the SAME caps the SEND/receive use; a float = @@ -18653,6 +18695,17 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *tinfo, src: *node, slot_off: i32, slot_s ck += 8; }; } else { + // #38b: an sret-classified call result is in + // memory (AX = dest pointer), not the cursor — + // the spill below would store the pointer as + // the payload. Mem-to-mem widen is #40. + if (src.kind == nkind.N_CALL) { + if (callsretsize(c, src) > 0) { + let m40a: str = "#40: sret-class call result cannot be cursor-widened into a tagged slot (mem-to-mem widen unwired)\n"; + os.write(2, m40a.ptr, m40a.len: u64); + os.exit(1); + }; + }; cgexpr(c, src); emitline("\tMOVQ\tAX, "); emitoff((slot_off + 8): i64); @@ -18716,6 +18769,15 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *tinfo, src: *node, slot_off: i32, slot_s // of tagged element). R8 carries the 4th word for slice-payload // variants (slot 32B). if (rhstaggedabicall(c, src)) { + // #38b: an sret-classified call result is in memory (AX = + // dest pointer), not the cursor. #40. + if (src.kind == nkind.N_CALL) { + if (callsretsize(c, src) > 0) { + let m40b: str = "#40: sret-class call result cannot be cursor-widened into a tagged slot (mem-to-mem widen unwired)\n"; + os.write(2, m40b.ptr, m40b.len: u64); + os.exit(1); + }; + }; cgexpr(c, src); emitline("\tMOVQ\tAX, "); emitoff(slot_off: i64); @@ -18904,7 +18966,7 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *tinfo, src: *node, slot_off: i32, slot_s // str IS []u8 and a slice is the // same 3-word {ptr,len,cap} header // from cgexpr's AX/BX/CX (#1/Phase - // 3). Slice arm rides #38's regex- + // 3). Slice arm rides #38b's regex- // shaped consumer — the prior str- // only gate dropped .len/.cap (#24 // gap's widener twin). @@ -19970,6 +20032,25 @@ fn cgtrytupleshift(c: *cgen, n: *node) bool = { // Success tag = 0 (#216 tracks the legacy/flag-aware success-tag // divergence — out of scope here, success check stays `CMPQ $0`). fn cgtryprop(c: *cgen, n: *node) void = { + // #38b residuals (rule 7): the cursor read below cannot see an + // sret-classified call result (AX = dest pointer), and the + // propagate-RET cannot speak an sret-classified enclosing return + // (the caller reads memory, not the cursor). #40-family follow-ups. + // Mirrors cstage cgen.c N_TRYPROP gates. + if (n.lhs != nil) { + if (n.lhs.kind == nkind.N_CALL) { + if (callsretsize(c, n.lhs) > 0) { + let m38p: str = "#38b: `?` on an sret-class call result unwired (mem-based unwrap is a #40-family follow-up)\n"; + os.write(2, m38p.ptr, m38p.len: u64); + os.exit(1); + }; + }; + }; + if (sretretsize(c, c.fnret) > 0) { + let m38q: str = "#38b: `?` propagation into a >32B tagged return unwired (sret error-propagate is a #40-family follow-up)\n"; + os.write(2, m38q.ptr, m38q.len: u64); + os.exit(1); + }; cgexpr(c, n.lhs); // AX = tag. If non-zero, this is an error; pop frame and RET. let cl: str = mklabel(c, "tryprop_ok"); @@ -20088,6 +20169,16 @@ fn cgtryprop(c: *cgen, n: *node) void = { // cgtryunw — `e!` aborts on the error variant via exit(1). Legacy // semantics (success tag = 0). fn cgtryunw(c: *cgen, n: *node) void = { + // #38b residual (rule 7): see the cgtryprop twin. + if (n.lhs != nil) { + if (n.lhs.kind == nkind.N_CALL) { + if (callsretsize(c, n.lhs) > 0) { + let m38u: str = "#38b: `!` on an sret-class call result unwired (mem-based unwrap is a #40-family follow-up)\n"; + os.write(2, m38u.ptr, m38u.len: u64); + os.exit(1); + }; + }; + }; cgexpr(c, n.lhs); let cl: str = mklabel(c, "tryunw_ok"); emitline("\tCMPQ\t$0, AX\n"); @@ -20157,6 +20248,18 @@ fn cgtypetest(c: *cgen, n: *node) void = { // stored via *i32 in this context — direct assignment of the // local works, indirection through &scrutoff drops sign bits. let lhs: *node = n.lhs; + // #38b residual (rule 7): an sret-class call result leaves AX = + // dest pointer, not the tag — mem-based test is a #40-family + // follow-up. Mirrors cstage cgen.c N_TYPETEST gate. + if (lhs != nil) { + if (lhs.kind == nkind.N_CALL) { + if (callsretsize(c, lhs) > 0) { + let m38t: str = "#38b: `is` on an sret-class call result unwired (#40-family follow-up)\n"; + os.write(2, m38t.ptr, m38t.len: u64); + os.exit(1); + }; + }; + }; let scrutoff: i32 = 0; let scrutt: *node = nil; if (lhs != nil) { @@ -20245,6 +20348,18 @@ fn cgtypeassert(c: *cgen, n: *node) void = { // str → (AX, BX). Mirrors cgmatch's slot-based value load. // Slot resolution inlined; see cgtypetest comment. let lhs: *node = n.lhs; + // #38b residual (rule 7): the spill below reads the cursor, which + // an sret-class call result never fills. Mirrors cstage cgen.c + // N_TYPEASSERT gate. + if (lhs != nil) { + if (lhs.kind == nkind.N_CALL) { + if (callsretsize(c, lhs) > 0) { + let m38a: str = "#38b: `as` on an sret-class call result unwired (#40-family follow-up)\n"; + os.write(2, m38a.ptr, m38a.len: u64); + os.exit(1); + }; + }; + }; let scrutoff: i32 = 0; let scrutt: *node = nil; if (lhs != nil) { @@ -21680,6 +21795,20 @@ fn cgmatch(c: *cgen, n: *node) void = { scrutt = matchscrutt(c, scrut); let spillsz: i32 = matchspillsz(c, scrutt); scrutoff = localalloc(c, "@match_spill", spillsz, nil); + // #38b: sret-classified tagged call scrutinee — pass + // the scrut slot itself as the sret dest and skip the + // cursor spill; downstream tag dispatch / case-let + // binds already read the slot from memory. Mirrors + // cstage cgen.c N_MATCH. + let msret: i32 = 0; + if (scrut.kind == nkind.N_CALL) { + msret = callsretsize(c, scrut); + }; + if (msret > 0) { + c.sretdestoff = scrutoff; + cgexpr(c, scrut); + c.sretdestoff = 0; + } else { cgexpr(c, scrut); emitline("\tMOVQ\tAX, "); emitoff(scrutoff: i64); @@ -21704,6 +21833,7 @@ fn cgmatch(c: *cgen, n: *node) void = { emitline("(BP)\n"); }; }; + }; }; }; let endl: str = mklabel(c, "match_end"); @@ -25041,12 +25171,68 @@ fn cgassign(c: *cgen, n: *node) void = { let lc: *local = localfindnode(c, lhs.str); if (lc != nil) { if (istaggedtype(c, lc.tnode)) { + // #38b: an sret-classified tagged CALL + // result is in memory, not the cursor — + // an exact-type reassign sret's into the + // local's own slot; a widening receive + // needs mem-to-mem tag-remap (#40). + // Mirrors cstage cgen.c N_ASSIGN tagged + // arm + the generic sret receive. + let asret: i32 = 0; + if (n.rhs != nil) { + if (n.rhs.kind == nkind.N_CALL) { + asret = callsretsize(c, n.rhs); + }; + }; + if (asret > 0) { + let aru: *tinfo = n.rhs.type_: *tinfo; + for (aru != nil && aru.kind == tykind.TY_NAMED) { + aru = aru.under; + }; + let alu: *tinfo = lc.tnode.type_: *tinfo; + for (alu != nil && alu.kind == tykind.TY_NAMED) { + alu = alu.under; + }; + let aexact: bool = false; + if (aru != nil && aru == alu) { aexact = true; } + else { + if (typeeq(n.rhs.type_: *tinfo, + lc.tnode.type_: *tinfo)) { + aexact = true; + }; + }; + if (!aexact) { + let m40d: str = "#40: sret-class call result cannot be widened into a tagged slot (mem-to-mem widen unwired)\n"; + os.write(2, m40d.ptr, m40d.len: u64); + os.exit(1); + }; + c.sretdestoff = lc.off; + cgexpr(c, n.rhs); + c.sretdestoff = 0; + return; + }; let lsz: i32 = slotsize(c, lc.tnode); cgwidentaggedstore(c, lc.tnode.type_: *tinfo, n.rhs, "BP", lc.off, lsz); return; }; }; + // #38b: sret receive into a tagged GLOBAL + // lvalue unwired (rule 7; cstage twin fatals). + if (lc == nil && n.rhs != nil) { + if (n.rhs.kind == nkind.N_CALL) { + let gru: *tinfo = lhs.type_: *tinfo; + for (gru != nil && gru.kind == tykind.TY_NAMED) { + gru = gru.under; + }; + if (gru != nil && gru.kind == tykind.TY_TAGGED + && callsretsize(c, n.rhs) > 0) { + let m38g: str = "#38b: sret receive into a tagged GLOBAL lvalue unwired\n"; + os.write(2, m38g.ptr, m38g.len: u64); + os.exit(1); + }; + }; + }; }; }; }; @@ -28934,6 +29120,63 @@ fn cgreturn(c: *cgen, n: *node) void = { }; }; }; + // #38b: sret-classified tagged return (slot > the + // AX/DX/CX/R8 cursor) — write through *(@sretarg) and + // return the dest pointer. Three shapes mirror cstage + // cgen.c N_RETURN #38b: exact-type N_CALL forward + // (c.sretforward), widening from a >32B tagged source + // (#40 loud-stop), everything else through + // cgwidentaggedstore's non-BP base. + if (sretretsize(c, c.fnret) > 0) { + let sa38v: i32 = localfind(c, "@sretarg"); + if (forwardtagged) { + // exact type, but only an N_CALL source + // sret's into outer's dest; a cursor + // source (N_INDEX/N_DOT) can't carry + // >32B (rule 7, #38b residual). + if (rhs.kind != nkind.N_CALL) { + let m38d: str = "#38b: >32B tagged return from a cursor source (N_INDEX/N_DOT) unsupported\n"; + os.write(2, m38d.ptr, m38d.len: u64); + os.exit(1); + }; + c.sretforward = 1; + cgexpr(c, rhs); + emitline("\tMOVQ\t"); + emitoff(sa38v: i64); + emitline("(BP), AX\n"); + emitline("\tMOVQ\tBP, SP\n"); + emitline("\tPOPQ\tBP\n"); + emitline("\tRET\n"); + c.lastwasreturn = 1; + return; + }; + let ru38: *tinfo = rhs.type_: *tinfo; + for (ru38 != nil && ru38.kind == tykind.TY_NAMED) { + ru38 = ru38.under; + }; + if (ru38 != nil) { + if (ru38.kind == tykind.TY_TAGGED + && rhs.kind != nkind.N_IDENT + && ru38.size: i32 > TUPLE_GPCAP * 8) { + let m38e: str = "#40: widening tagged return-forward of a >32B source needs mem-to-mem tag-remap (unwired)\n"; + os.write(2, m38e.ptr, m38e.len: u64); + os.exit(1); + }; + }; + emitline("\tMOVQ\t"); + emitoff(sa38v: i64); + emitline("(BP), BX\n"); + cgwidentaggedstore(c, c.fnret.type_: *tinfo, rhs, + "BX", 0, slotsize(c, c.fnret)); + emitline("\tMOVQ\t"); + emitoff(sa38v: i64); + emitline("(BP), AX\n"); + emitline("\tMOVQ\tBP, SP\n"); + emitline("\tPOPQ\tBP\n"); + emitline("\tRET\n"); + c.lastwasreturn = 1; + return; + }; // Struct payload or tagged-subset return — materialise // the widened value in scratch via cgwidentaggedstore // (handles tag remap and zero pad), then load AX/DX/CX @@ -29709,6 +29952,30 @@ fn cgreturn(c: *cgen, n: *node) void = { // the void variant: emit its tag. Payload is undefined // (void has size 0). Otherwise zero AX for determinism. if (istaggedtype(c, c.fnret)) { + // #38b: an sret-classified tagged return (slot > the + // AX/DX/CX/R8 cursor) writes the void-variant tag + // through *(@sretarg) and returns the dest pointer — + // the cursor can't carry the slot and the caller reads + // memory. Mirrors cstage cgen.c N_RETURN bare arm. + if (sretretsize(c, c.fnret) > 0) { + let sa38: i32 = localfind(c, "@sretarg"); + let vidx38: i32 = voidvariantindex(c.fnret); + if (vidx38 < 0) { vidx38 = 0; }; + emitline("\tMOVQ\t"); + emitoff(sa38: i64); + emitline("(BP), BX\n"); + emitline("\tMOVQ\t$"); + emitint(vidx38: i64); + emitline(", (BX)\n"); + emitline("\tMOVQ\t"); + emitoff(sa38: i64); + emitline("(BP), AX\n"); + emitline("\tMOVQ\tBP, SP\n"); + emitline("\tPOPQ\tBP\n"); + emitline("\tRET\n"); + c.lastwasreturn = 1; + return; + }; if (isnullabletype(c.fnret)) { // null = void variant; AX = 0. emitline("\tMOVQ\t$0, AX\n"); @@ -30126,10 +30393,44 @@ fn cglet(c: *cgen, n: *node) void = { // handles nullable fold, tagged source (ident or AX/DX/CX // ABI call), struct payload (literal/ident), str payload, // scalar payload — with tag remap for tagged-subset widening. + // + // #38b: an sret-classified tagged CALL result is in memory, + // not the cursor — an exact-type receive falls through to the + // generic sret receive below (the let's slot IS the dest); a + // widening receive needs mem-to-mem tag-remap (#40, unwired). + // Mirrors cstage cgen.c N_LET tagged arm. if (istaggedtype(c, tn)) { - cgwidentaggedstore(c, tn.type_: *tinfo, rhs, "BP", off, sz); - c.lastwasreturn = 0; - return; + let letsret: i32 = 0; + if (rhs.kind == nkind.N_CALL) { + letsret = callsretsize(c, rhs); + }; + if (letsret == 0) { + cgwidentaggedstore(c, tn.type_: *tinfo, rhs, + "BP", off, sz); + c.lastwasreturn = 0; + return; + }; + let lru: *tinfo = rhs.type_: *tinfo; + for (lru != nil && lru.kind == tykind.TY_NAMED) { + lru = lru.under; + }; + let llu: *tinfo = tn.type_: *tinfo; + for (llu != nil && llu.kind == tykind.TY_NAMED) { + llu = llu.under; + }; + let exact38: bool = false; + if (lru != nil && lru == llu) { exact38 = true; } + else { + if (typeeq(rhs.type_: *tinfo, tn.type_: *tinfo)) { + exact38 = true; + }; + }; + if (!exact38) { + let m40c: str = "#40: sret-class call result cannot be widened into a tagged slot (mem-to-mem widen unwired)\n"; + os.write(2, m40c.ptr, m40c.len: u64); + os.exit(1); + }; + // fall through to the generic sret receive below. }; // 32B tuple init for `let t: (scalar, str) = call()` / // `let t: (str, scalar) = call()` (#105 / #164/#107). Each diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 06051375..5ca940d4 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -172,6 +172,25 @@ fn cgtrytupleshift(c: *cgen, n: *node) bool = { // Success tag = 0 (#216 tracks the legacy/flag-aware success-tag // divergence — out of scope here, success check stays `CMPQ $0`). fn cgtryprop(c: *cgen, n: *node) void = { + // #38b residuals (rule 7): the cursor read below cannot see an + // sret-classified call result (AX = dest pointer), and the + // propagate-RET cannot speak an sret-classified enclosing return + // (the caller reads memory, not the cursor). #40-family follow-ups. + // Mirrors cstage cgen.c N_TRYPROP gates. + if (n.lhs != nil) { + if (n.lhs.kind == nkind.N_CALL) { + if (callsretsize(c, n.lhs) > 0) { + let m38p: str = "#38b: `?` on an sret-class call result unwired (mem-based unwrap is a #40-family follow-up)\n"; + os.write(2, m38p.ptr, m38p.len: u64); + os.exit(1); + }; + }; + }; + if (sretretsize(c, c.fnret) > 0) { + let m38q: str = "#38b: `?` propagation into a >32B tagged return unwired (sret error-propagate is a #40-family follow-up)\n"; + os.write(2, m38q.ptr, m38q.len: u64); + os.exit(1); + }; cgexpr(c, n.lhs); // AX = tag. If non-zero, this is an error; pop frame and RET. let cl: str = mklabel(c, "tryprop_ok"); @@ -290,6 +309,16 @@ fn cgtryprop(c: *cgen, n: *node) void = { // cgtryunw — `e!` aborts on the error variant via exit(1). Legacy // semantics (success tag = 0). fn cgtryunw(c: *cgen, n: *node) void = { + // #38b residual (rule 7): see the cgtryprop twin. + if (n.lhs != nil) { + if (n.lhs.kind == nkind.N_CALL) { + if (callsretsize(c, n.lhs) > 0) { + let m38u: str = "#38b: `!` on an sret-class call result unwired (mem-based unwrap is a #40-family follow-up)\n"; + os.write(2, m38u.ptr, m38u.len: u64); + os.exit(1); + }; + }; + }; cgexpr(c, n.lhs); let cl: str = mklabel(c, "tryunw_ok"); emitline("\tCMPQ\t$0, AX\n"); @@ -359,6 +388,18 @@ fn cgtypetest(c: *cgen, n: *node) void = { // stored via *i32 in this context — direct assignment of the // local works, indirection through &scrutoff drops sign bits. let lhs: *node = n.lhs; + // #38b residual (rule 7): an sret-class call result leaves AX = + // dest pointer, not the tag — mem-based test is a #40-family + // follow-up. Mirrors cstage cgen.c N_TYPETEST gate. + if (lhs != nil) { + if (lhs.kind == nkind.N_CALL) { + if (callsretsize(c, lhs) > 0) { + let m38t: str = "#38b: `is` on an sret-class call result unwired (#40-family follow-up)\n"; + os.write(2, m38t.ptr, m38t.len: u64); + os.exit(1); + }; + }; + }; let scrutoff: i32 = 0; let scrutt: *node = nil; if (lhs != nil) { @@ -447,6 +488,18 @@ fn cgtypeassert(c: *cgen, n: *node) void = { // str → (AX, BX). Mirrors cgmatch's slot-based value load. // Slot resolution inlined; see cgtypetest comment. let lhs: *node = n.lhs; + // #38b residual (rule 7): the spill below reads the cursor, which + // an sret-class call result never fills. Mirrors cstage cgen.c + // N_TYPEASSERT gate. + if (lhs != nil) { + if (lhs.kind == nkind.N_CALL) { + if (callsretsize(c, lhs) > 0) { + let m38a: str = "#38b: `as` on an sret-class call result unwired (#40-family follow-up)\n"; + os.write(2, m38a.ptr, m38a.len: u64); + os.exit(1); + }; + }; + }; let scrutoff: i32 = 0; let scrutt: *node = nil; if (lhs != nil) { @@ -1882,6 +1935,20 @@ fn cgmatch(c: *cgen, n: *node) void = { scrutt = matchscrutt(c, scrut); let spillsz: i32 = matchspillsz(c, scrutt); scrutoff = localalloc(c, "@match_spill", spillsz, nil); + // #38b: sret-classified tagged call scrutinee — pass + // the scrut slot itself as the sret dest and skip the + // cursor spill; downstream tag dispatch / case-let + // binds already read the slot from memory. Mirrors + // cstage cgen.c N_MATCH. + let msret: i32 = 0; + if (scrut.kind == nkind.N_CALL) { + msret = callsretsize(c, scrut); + }; + if (msret > 0) { + c.sretdestoff = scrutoff; + cgexpr(c, scrut); + c.sretdestoff = 0; + } else { cgexpr(c, scrut); emitline("\tMOVQ\tAX, "); emitoff(scrutoff: i64); @@ -1906,6 +1973,7 @@ fn cgmatch(c: *cgen, n: *node) void = { emitline("(BP)\n"); }; }; + }; }; }; let endl: str = mklabel(c, "match_end"); @@ -5243,12 +5311,68 @@ fn cgassign(c: *cgen, n: *node) void = { let lc: *local = localfindnode(c, lhs.str); if (lc != nil) { if (istaggedtype(c, lc.tnode)) { + // #38b: an sret-classified tagged CALL + // result is in memory, not the cursor — + // an exact-type reassign sret's into the + // local's own slot; a widening receive + // needs mem-to-mem tag-remap (#40). + // Mirrors cstage cgen.c N_ASSIGN tagged + // arm + the generic sret receive. + let asret: i32 = 0; + if (n.rhs != nil) { + if (n.rhs.kind == nkind.N_CALL) { + asret = callsretsize(c, n.rhs); + }; + }; + if (asret > 0) { + let aru: *tinfo = n.rhs.type_: *tinfo; + for (aru != nil && aru.kind == tykind.TY_NAMED) { + aru = aru.under; + }; + let alu: *tinfo = lc.tnode.type_: *tinfo; + for (alu != nil && alu.kind == tykind.TY_NAMED) { + alu = alu.under; + }; + let aexact: bool = false; + if (aru != nil && aru == alu) { aexact = true; } + else { + if (typeeq(n.rhs.type_: *tinfo, + lc.tnode.type_: *tinfo)) { + aexact = true; + }; + }; + if (!aexact) { + let m40d: str = "#40: sret-class call result cannot be widened into a tagged slot (mem-to-mem widen unwired)\n"; + os.write(2, m40d.ptr, m40d.len: u64); + os.exit(1); + }; + c.sretdestoff = lc.off; + cgexpr(c, n.rhs); + c.sretdestoff = 0; + return; + }; let lsz: i32 = slotsize(c, lc.tnode); cgwidentaggedstore(c, lc.tnode.type_: *tinfo, n.rhs, "BP", lc.off, lsz); return; }; }; + // #38b: sret receive into a tagged GLOBAL + // lvalue unwired (rule 7; cstage twin fatals). + if (lc == nil && n.rhs != nil) { + if (n.rhs.kind == nkind.N_CALL) { + let gru: *tinfo = lhs.type_: *tinfo; + for (gru != nil && gru.kind == tykind.TY_NAMED) { + gru = gru.under; + }; + if (gru != nil && gru.kind == tykind.TY_TAGGED + && callsretsize(c, n.rhs) > 0) { + let m38g: str = "#38b: sret receive into a tagged GLOBAL lvalue unwired\n"; + os.write(2, m38g.ptr, m38g.len: u64); + os.exit(1); + }; + }; + }; }; }; }; diff --git a/selfhost/cmd/wcc/cgenstmt.ww b/selfhost/cmd/wcc/cgenstmt.ww index b756e6dd..b1d98078 100644 --- a/selfhost/cmd/wcc/cgenstmt.ww +++ b/selfhost/cmd/wcc/cgenstmt.ww @@ -663,6 +663,63 @@ fn cgreturn(c: *cgen, n: *node) void = { }; }; }; + // #38b: sret-classified tagged return (slot > the + // AX/DX/CX/R8 cursor) — write through *(@sretarg) and + // return the dest pointer. Three shapes mirror cstage + // cgen.c N_RETURN #38b: exact-type N_CALL forward + // (c.sretforward), widening from a >32B tagged source + // (#40 loud-stop), everything else through + // cgwidentaggedstore's non-BP base. + if (sretretsize(c, c.fnret) > 0) { + let sa38v: i32 = localfind(c, "@sretarg"); + if (forwardtagged) { + // exact type, but only an N_CALL source + // sret's into outer's dest; a cursor + // source (N_INDEX/N_DOT) can't carry + // >32B (rule 7, #38b residual). + if (rhs.kind != nkind.N_CALL) { + let m38d: str = "#38b: >32B tagged return from a cursor source (N_INDEX/N_DOT) unsupported\n"; + os.write(2, m38d.ptr, m38d.len: u64); + os.exit(1); + }; + c.sretforward = 1; + cgexpr(c, rhs); + emitline("\tMOVQ\t"); + emitoff(sa38v: i64); + emitline("(BP), AX\n"); + emitline("\tMOVQ\tBP, SP\n"); + emitline("\tPOPQ\tBP\n"); + emitline("\tRET\n"); + c.lastwasreturn = 1; + return; + }; + let ru38: *tinfo = rhs.type_: *tinfo; + for (ru38 != nil && ru38.kind == tykind.TY_NAMED) { + ru38 = ru38.under; + }; + if (ru38 != nil) { + if (ru38.kind == tykind.TY_TAGGED + && rhs.kind != nkind.N_IDENT + && ru38.size: i32 > TUPLE_GPCAP * 8) { + let m38e: str = "#40: widening tagged return-forward of a >32B source needs mem-to-mem tag-remap (unwired)\n"; + os.write(2, m38e.ptr, m38e.len: u64); + os.exit(1); + }; + }; + emitline("\tMOVQ\t"); + emitoff(sa38v: i64); + emitline("(BP), BX\n"); + cgwidentaggedstore(c, c.fnret.type_: *tinfo, rhs, + "BX", 0, slotsize(c, c.fnret)); + emitline("\tMOVQ\t"); + emitoff(sa38v: i64); + emitline("(BP), AX\n"); + emitline("\tMOVQ\tBP, SP\n"); + emitline("\tPOPQ\tBP\n"); + emitline("\tRET\n"); + c.lastwasreturn = 1; + return; + }; // Struct payload or tagged-subset return — materialise // the widened value in scratch via cgwidentaggedstore // (handles tag remap and zero pad), then load AX/DX/CX @@ -1438,6 +1495,30 @@ fn cgreturn(c: *cgen, n: *node) void = { // the void variant: emit its tag. Payload is undefined // (void has size 0). Otherwise zero AX for determinism. if (istaggedtype(c, c.fnret)) { + // #38b: an sret-classified tagged return (slot > the + // AX/DX/CX/R8 cursor) writes the void-variant tag + // through *(@sretarg) and returns the dest pointer — + // the cursor can't carry the slot and the caller reads + // memory. Mirrors cstage cgen.c N_RETURN bare arm. + if (sretretsize(c, c.fnret) > 0) { + let sa38: i32 = localfind(c, "@sretarg"); + let vidx38: i32 = voidvariantindex(c.fnret); + if (vidx38 < 0) { vidx38 = 0; }; + emitline("\tMOVQ\t"); + emitoff(sa38: i64); + emitline("(BP), BX\n"); + emitline("\tMOVQ\t$"); + emitint(vidx38: i64); + emitline(", (BX)\n"); + emitline("\tMOVQ\t"); + emitoff(sa38: i64); + emitline("(BP), AX\n"); + emitline("\tMOVQ\tBP, SP\n"); + emitline("\tPOPQ\tBP\n"); + emitline("\tRET\n"); + c.lastwasreturn = 1; + return; + }; if (isnullabletype(c.fnret)) { // null = void variant; AX = 0. emitline("\tMOVQ\t$0, AX\n"); @@ -1855,10 +1936,44 @@ fn cglet(c: *cgen, n: *node) void = { // handles nullable fold, tagged source (ident or AX/DX/CX // ABI call), struct payload (literal/ident), str payload, // scalar payload — with tag remap for tagged-subset widening. + // + // #38b: an sret-classified tagged CALL result is in memory, + // not the cursor — an exact-type receive falls through to the + // generic sret receive below (the let's slot IS the dest); a + // widening receive needs mem-to-mem tag-remap (#40, unwired). + // Mirrors cstage cgen.c N_LET tagged arm. if (istaggedtype(c, tn)) { - cgwidentaggedstore(c, tn.type_: *tinfo, rhs, "BP", off, sz); - c.lastwasreturn = 0; - return; + let letsret: i32 = 0; + if (rhs.kind == nkind.N_CALL) { + letsret = callsretsize(c, rhs); + }; + if (letsret == 0) { + cgwidentaggedstore(c, tn.type_: *tinfo, rhs, + "BP", off, sz); + c.lastwasreturn = 0; + return; + }; + let lru: *tinfo = rhs.type_: *tinfo; + for (lru != nil && lru.kind == tykind.TY_NAMED) { + lru = lru.under; + }; + let llu: *tinfo = tn.type_: *tinfo; + for (llu != nil && llu.kind == tykind.TY_NAMED) { + llu = llu.under; + }; + let exact38: bool = false; + if (lru != nil && lru == llu) { exact38 = true; } + else { + if (typeeq(rhs.type_: *tinfo, tn.type_: *tinfo)) { + exact38 = true; + }; + }; + if (!exact38) { + let m40c: str = "#40: sret-class call result cannot be widened into a tagged slot (mem-to-mem widen unwired)\n"; + os.write(2, m40c.ptr, m40c.len: u64); + os.exit(1); + }; + // fall through to the generic sret receive below. }; // 32B tuple init for `let t: (scalar, str) = call()` / // `let t: (str, scalar) = call()` (#105 / #164/#107). Each diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index bfc0cea1..fdf8ef47 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -104,6 +104,27 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = { let nextparam: *node = nil; if (param != nil) { nextparam = param.next; }; let rest: i32 = pushargsrev(c, arg.next, nextparam); + // #38b residual (rule 7): a tagged arg slot past the 6-reg arg + // capacity has no push shape — cstage tagged_arg_size returns 0 + // ("too large") and both stages fell to divergent silent pushes + // (cs one word, ww slot words). Mirrors cstage cgcall's gate. + { + let a48: *tinfo = arg.type_: *tinfo; + for (a48 != nil && a48.kind == tykind.TY_NAMED) { + a48 = a48.under; + }; + if (a48 != nil) { + if (a48.kind == tykind.TY_TAGGED && a48.nullable == 0) { + // sizelint-ok: 6 SysV int arg regs (DI..R9) x + // 8B words = the cstage tagged_arg_size cap. + if (a48.size: i32 > 6 * 8) { + let m48: str = "#38b: tagged arg exceeds the register arg capacity (>48B slot) — unwired\n"; + os.write(2, m48.ptr, m48.len: u64); + os.exit(1); + }; + }; + }; + }; // Implicit widening from a concrete variant to a tagged-union // parameter slot. Skips when the arg is already a tagged local // (line 121's slice-or-tagged shortcut handles that). @@ -650,6 +671,15 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = { // tag first. Mirrors cstage at cmd/w6c/cgen.c:4373-4387. let tcs: i32 = taggedcallslot(c, arg); if (tcs > 0) { + // #38b residual (rule 7): an sret-class call result is in + // memory, not the cursor — the @aggargscr-style receive-then- + // push is the #40-family follow-up. Mirrors cstage cgen.c + // cgcall tagged arg-push gate. + if (callsretsize(c, arg) > 0) { + let m38r: str = "#38b: >32B tagged call result as a call argument unwired (#40-family follow-up)\n"; + os.write(2, m38r.ptr, m38r.len: u64); + os.exit(1); + }; if (tcs > 24) { emitline("\tPUSHQ\tR8\n"); }; if (tcs > 16) { emitline("\tPUSHQ\tCX\n"); }; if (tcs > 8) { emitline("\tPUSHQ\tDX\n"); }; @@ -1403,6 +1433,18 @@ export fn sretretsize(c: *cgen, t: *node) i32 = { r = r.lhs; if (r == nil) { return 0; }; }; + // #38: a tagged union rides AX(tag)+DX/CX/R8 = TUPLE_GPCAP + // eightbytes; a wider slot was silently truncated (payload word + // 4+ died in the callee frame). The ≤cap boundary is load-bearing: + // (str|nomem)-shaped 32B slots MUST stay register-ABI or every + // such consumer in the tree flips. Nullable folds to one word. + // Mirrors cstage cg_sret_retsize TY_TAGGED arm. + if (istaggedtype(c, r)) { + if (isnullabletype(r)) { return 0; }; + let tsz38: i32 = slotsize(c, r); + if (tsz38 <= TUPLE_GPCAP * 8) { return 0; }; + return tsz38; + }; if (r.kind == nkind.N_TTUPLE) { // #10: over-cap tuple → sret. Walk the element TYPE nodes // (pt.lhs) over the SAME caps the SEND/receive use; a float = @@ -2961,6 +3003,17 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *tinfo, src: *node, slot_off: i32, slot_s ck += 8; }; } else { + // #38b: an sret-classified call result is in + // memory (AX = dest pointer), not the cursor — + // the spill below would store the pointer as + // the payload. Mem-to-mem widen is #40. + if (src.kind == nkind.N_CALL) { + if (callsretsize(c, src) > 0) { + let m40a: str = "#40: sret-class call result cannot be cursor-widened into a tagged slot (mem-to-mem widen unwired)\n"; + os.write(2, m40a.ptr, m40a.len: u64); + os.exit(1); + }; + }; cgexpr(c, src); emitline("\tMOVQ\tAX, "); emitoff((slot_off + 8): i64); @@ -3024,6 +3077,15 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *tinfo, src: *node, slot_off: i32, slot_s // of tagged element). R8 carries the 4th word for slice-payload // variants (slot 32B). if (rhstaggedabicall(c, src)) { + // #38b: an sret-classified call result is in memory (AX = + // dest pointer), not the cursor. #40. + if (src.kind == nkind.N_CALL) { + if (callsretsize(c, src) > 0) { + let m40b: str = "#40: sret-class call result cannot be cursor-widened into a tagged slot (mem-to-mem widen unwired)\n"; + os.write(2, m40b.ptr, m40b.len: u64); + os.exit(1); + }; + }; cgexpr(c, src); emitline("\tMOVQ\tAX, "); emitoff(slot_off: i64); @@ -3212,7 +3274,7 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *tinfo, src: *node, slot_off: i32, slot_s // str IS []u8 and a slice is the // same 3-word {ptr,len,cap} header // from cgexpr's AX/BX/CX (#1/Phase - // 3). Slice arm rides #38's regex- + // 3). Slice arm rides #38b's regex- // shaped consumer — the prior str- // only gate dropped .len/.cap (#24 // gap's widener twin). diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 2ca63244..f6e88318 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -15796,6 +15796,27 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = { let nextparam: *node = nil; if (param != nil) { nextparam = param.next; }; let rest: i32 = pushargsrev(c, arg.next, nextparam); + // #38b residual (rule 7): a tagged arg slot past the 6-reg arg + // capacity has no push shape — cstage tagged_arg_size returns 0 + // ("too large") and both stages fell to divergent silent pushes + // (cs one word, ww slot words). Mirrors cstage cgcall's gate. + { + let a48: *tinfo = arg.type_: *tinfo; + for (a48 != nil && a48.kind == tykind.TY_NAMED) { + a48 = a48.under; + }; + if (a48 != nil) { + if (a48.kind == tykind.TY_TAGGED && a48.nullable == 0) { + // sizelint-ok: 6 SysV int arg regs (DI..R9) x + // 8B words = the cstage tagged_arg_size cap. + if (a48.size: i32 > 6 * 8) { + let m48: str = "#38b: tagged arg exceeds the register arg capacity (>48B slot) — unwired\n"; + os.write(2, m48.ptr, m48.len: u64); + os.exit(1); + }; + }; + }; + }; // Implicit widening from a concrete variant to a tagged-union // parameter slot. Skips when the arg is already a tagged local // (line 121's slice-or-tagged shortcut handles that). @@ -16342,6 +16363,15 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = { // tag first. Mirrors cstage at cmd/w6c/cgen.c:4373-4387. let tcs: i32 = taggedcallslot(c, arg); if (tcs > 0) { + // #38b residual (rule 7): an sret-class call result is in + // memory, not the cursor — the @aggargscr-style receive-then- + // push is the #40-family follow-up. Mirrors cstage cgen.c + // cgcall tagged arg-push gate. + if (callsretsize(c, arg) > 0) { + let m38r: str = "#38b: >32B tagged call result as a call argument unwired (#40-family follow-up)\n"; + os.write(2, m38r.ptr, m38r.len: u64); + os.exit(1); + }; if (tcs > 24) { emitline("\tPUSHQ\tR8\n"); }; if (tcs > 16) { emitline("\tPUSHQ\tCX\n"); }; if (tcs > 8) { emitline("\tPUSHQ\tDX\n"); }; @@ -17095,6 +17125,18 @@ export fn sretretsize(c: *cgen, t: *node) i32 = { r = r.lhs; if (r == nil) { return 0; }; }; + // #38: a tagged union rides AX(tag)+DX/CX/R8 = TUPLE_GPCAP + // eightbytes; a wider slot was silently truncated (payload word + // 4+ died in the callee frame). The ≤cap boundary is load-bearing: + // (str|nomem)-shaped 32B slots MUST stay register-ABI or every + // such consumer in the tree flips. Nullable folds to one word. + // Mirrors cstage cg_sret_retsize TY_TAGGED arm. + if (istaggedtype(c, r)) { + if (isnullabletype(r)) { return 0; }; + let tsz38: i32 = slotsize(c, r); + if (tsz38 <= TUPLE_GPCAP * 8) { return 0; }; + return tsz38; + }; if (r.kind == nkind.N_TTUPLE) { // #10: over-cap tuple → sret. Walk the element TYPE nodes // (pt.lhs) over the SAME caps the SEND/receive use; a float = @@ -18653,6 +18695,17 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *tinfo, src: *node, slot_off: i32, slot_s ck += 8; }; } else { + // #38b: an sret-classified call result is in + // memory (AX = dest pointer), not the cursor — + // the spill below would store the pointer as + // the payload. Mem-to-mem widen is #40. + if (src.kind == nkind.N_CALL) { + if (callsretsize(c, src) > 0) { + let m40a: str = "#40: sret-class call result cannot be cursor-widened into a tagged slot (mem-to-mem widen unwired)\n"; + os.write(2, m40a.ptr, m40a.len: u64); + os.exit(1); + }; + }; cgexpr(c, src); emitline("\tMOVQ\tAX, "); emitoff((slot_off + 8): i64); @@ -18716,6 +18769,15 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *tinfo, src: *node, slot_off: i32, slot_s // of tagged element). R8 carries the 4th word for slice-payload // variants (slot 32B). if (rhstaggedabicall(c, src)) { + // #38b: an sret-classified call result is in memory (AX = + // dest pointer), not the cursor. #40. + if (src.kind == nkind.N_CALL) { + if (callsretsize(c, src) > 0) { + let m40b: str = "#40: sret-class call result cannot be cursor-widened into a tagged slot (mem-to-mem widen unwired)\n"; + os.write(2, m40b.ptr, m40b.len: u64); + os.exit(1); + }; + }; cgexpr(c, src); emitline("\tMOVQ\tAX, "); emitoff(slot_off: i64); @@ -18904,7 +18966,7 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *tinfo, src: *node, slot_off: i32, slot_s // str IS []u8 and a slice is the // same 3-word {ptr,len,cap} header // from cgexpr's AX/BX/CX (#1/Phase - // 3). Slice arm rides #38's regex- + // 3). Slice arm rides #38b's regex- // shaped consumer — the prior str- // only gate dropped .len/.cap (#24 // gap's widener twin). @@ -19970,6 +20032,25 @@ fn cgtrytupleshift(c: *cgen, n: *node) bool = { // Success tag = 0 (#216 tracks the legacy/flag-aware success-tag // divergence — out of scope here, success check stays `CMPQ $0`). fn cgtryprop(c: *cgen, n: *node) void = { + // #38b residuals (rule 7): the cursor read below cannot see an + // sret-classified call result (AX = dest pointer), and the + // propagate-RET cannot speak an sret-classified enclosing return + // (the caller reads memory, not the cursor). #40-family follow-ups. + // Mirrors cstage cgen.c N_TRYPROP gates. + if (n.lhs != nil) { + if (n.lhs.kind == nkind.N_CALL) { + if (callsretsize(c, n.lhs) > 0) { + let m38p: str = "#38b: `?` on an sret-class call result unwired (mem-based unwrap is a #40-family follow-up)\n"; + os.write(2, m38p.ptr, m38p.len: u64); + os.exit(1); + }; + }; + }; + if (sretretsize(c, c.fnret) > 0) { + let m38q: str = "#38b: `?` propagation into a >32B tagged return unwired (sret error-propagate is a #40-family follow-up)\n"; + os.write(2, m38q.ptr, m38q.len: u64); + os.exit(1); + }; cgexpr(c, n.lhs); // AX = tag. If non-zero, this is an error; pop frame and RET. let cl: str = mklabel(c, "tryprop_ok"); @@ -20088,6 +20169,16 @@ fn cgtryprop(c: *cgen, n: *node) void = { // cgtryunw — `e!` aborts on the error variant via exit(1). Legacy // semantics (success tag = 0). fn cgtryunw(c: *cgen, n: *node) void = { + // #38b residual (rule 7): see the cgtryprop twin. + if (n.lhs != nil) { + if (n.lhs.kind == nkind.N_CALL) { + if (callsretsize(c, n.lhs) > 0) { + let m38u: str = "#38b: `!` on an sret-class call result unwired (mem-based unwrap is a #40-family follow-up)\n"; + os.write(2, m38u.ptr, m38u.len: u64); + os.exit(1); + }; + }; + }; cgexpr(c, n.lhs); let cl: str = mklabel(c, "tryunw_ok"); emitline("\tCMPQ\t$0, AX\n"); @@ -20157,6 +20248,18 @@ fn cgtypetest(c: *cgen, n: *node) void = { // stored via *i32 in this context — direct assignment of the // local works, indirection through &scrutoff drops sign bits. let lhs: *node = n.lhs; + // #38b residual (rule 7): an sret-class call result leaves AX = + // dest pointer, not the tag — mem-based test is a #40-family + // follow-up. Mirrors cstage cgen.c N_TYPETEST gate. + if (lhs != nil) { + if (lhs.kind == nkind.N_CALL) { + if (callsretsize(c, lhs) > 0) { + let m38t: str = "#38b: `is` on an sret-class call result unwired (#40-family follow-up)\n"; + os.write(2, m38t.ptr, m38t.len: u64); + os.exit(1); + }; + }; + }; let scrutoff: i32 = 0; let scrutt: *node = nil; if (lhs != nil) { @@ -20245,6 +20348,18 @@ fn cgtypeassert(c: *cgen, n: *node) void = { // str → (AX, BX). Mirrors cgmatch's slot-based value load. // Slot resolution inlined; see cgtypetest comment. let lhs: *node = n.lhs; + // #38b residual (rule 7): the spill below reads the cursor, which + // an sret-class call result never fills. Mirrors cstage cgen.c + // N_TYPEASSERT gate. + if (lhs != nil) { + if (lhs.kind == nkind.N_CALL) { + if (callsretsize(c, lhs) > 0) { + let m38a: str = "#38b: `as` on an sret-class call result unwired (#40-family follow-up)\n"; + os.write(2, m38a.ptr, m38a.len: u64); + os.exit(1); + }; + }; + }; let scrutoff: i32 = 0; let scrutt: *node = nil; if (lhs != nil) { @@ -21680,6 +21795,20 @@ fn cgmatch(c: *cgen, n: *node) void = { scrutt = matchscrutt(c, scrut); let spillsz: i32 = matchspillsz(c, scrutt); scrutoff = localalloc(c, "@match_spill", spillsz, nil); + // #38b: sret-classified tagged call scrutinee — pass + // the scrut slot itself as the sret dest and skip the + // cursor spill; downstream tag dispatch / case-let + // binds already read the slot from memory. Mirrors + // cstage cgen.c N_MATCH. + let msret: i32 = 0; + if (scrut.kind == nkind.N_CALL) { + msret = callsretsize(c, scrut); + }; + if (msret > 0) { + c.sretdestoff = scrutoff; + cgexpr(c, scrut); + c.sretdestoff = 0; + } else { cgexpr(c, scrut); emitline("\tMOVQ\tAX, "); emitoff(scrutoff: i64); @@ -21704,6 +21833,7 @@ fn cgmatch(c: *cgen, n: *node) void = { emitline("(BP)\n"); }; }; + }; }; }; let endl: str = mklabel(c, "match_end"); @@ -25041,12 +25171,68 @@ fn cgassign(c: *cgen, n: *node) void = { let lc: *local = localfindnode(c, lhs.str); if (lc != nil) { if (istaggedtype(c, lc.tnode)) { + // #38b: an sret-classified tagged CALL + // result is in memory, not the cursor — + // an exact-type reassign sret's into the + // local's own slot; a widening receive + // needs mem-to-mem tag-remap (#40). + // Mirrors cstage cgen.c N_ASSIGN tagged + // arm + the generic sret receive. + let asret: i32 = 0; + if (n.rhs != nil) { + if (n.rhs.kind == nkind.N_CALL) { + asret = callsretsize(c, n.rhs); + }; + }; + if (asret > 0) { + let aru: *tinfo = n.rhs.type_: *tinfo; + for (aru != nil && aru.kind == tykind.TY_NAMED) { + aru = aru.under; + }; + let alu: *tinfo = lc.tnode.type_: *tinfo; + for (alu != nil && alu.kind == tykind.TY_NAMED) { + alu = alu.under; + }; + let aexact: bool = false; + if (aru != nil && aru == alu) { aexact = true; } + else { + if (typeeq(n.rhs.type_: *tinfo, + lc.tnode.type_: *tinfo)) { + aexact = true; + }; + }; + if (!aexact) { + let m40d: str = "#40: sret-class call result cannot be widened into a tagged slot (mem-to-mem widen unwired)\n"; + os.write(2, m40d.ptr, m40d.len: u64); + os.exit(1); + }; + c.sretdestoff = lc.off; + cgexpr(c, n.rhs); + c.sretdestoff = 0; + return; + }; let lsz: i32 = slotsize(c, lc.tnode); cgwidentaggedstore(c, lc.tnode.type_: *tinfo, n.rhs, "BP", lc.off, lsz); return; }; }; + // #38b: sret receive into a tagged GLOBAL + // lvalue unwired (rule 7; cstage twin fatals). + if (lc == nil && n.rhs != nil) { + if (n.rhs.kind == nkind.N_CALL) { + let gru: *tinfo = lhs.type_: *tinfo; + for (gru != nil && gru.kind == tykind.TY_NAMED) { + gru = gru.under; + }; + if (gru != nil && gru.kind == tykind.TY_TAGGED + && callsretsize(c, n.rhs) > 0) { + let m38g: str = "#38b: sret receive into a tagged GLOBAL lvalue unwired\n"; + os.write(2, m38g.ptr, m38g.len: u64); + os.exit(1); + }; + }; + }; }; }; }; @@ -28934,6 +29120,63 @@ fn cgreturn(c: *cgen, n: *node) void = { }; }; }; + // #38b: sret-classified tagged return (slot > the + // AX/DX/CX/R8 cursor) — write through *(@sretarg) and + // return the dest pointer. Three shapes mirror cstage + // cgen.c N_RETURN #38b: exact-type N_CALL forward + // (c.sretforward), widening from a >32B tagged source + // (#40 loud-stop), everything else through + // cgwidentaggedstore's non-BP base. + if (sretretsize(c, c.fnret) > 0) { + let sa38v: i32 = localfind(c, "@sretarg"); + if (forwardtagged) { + // exact type, but only an N_CALL source + // sret's into outer's dest; a cursor + // source (N_INDEX/N_DOT) can't carry + // >32B (rule 7, #38b residual). + if (rhs.kind != nkind.N_CALL) { + let m38d: str = "#38b: >32B tagged return from a cursor source (N_INDEX/N_DOT) unsupported\n"; + os.write(2, m38d.ptr, m38d.len: u64); + os.exit(1); + }; + c.sretforward = 1; + cgexpr(c, rhs); + emitline("\tMOVQ\t"); + emitoff(sa38v: i64); + emitline("(BP), AX\n"); + emitline("\tMOVQ\tBP, SP\n"); + emitline("\tPOPQ\tBP\n"); + emitline("\tRET\n"); + c.lastwasreturn = 1; + return; + }; + let ru38: *tinfo = rhs.type_: *tinfo; + for (ru38 != nil && ru38.kind == tykind.TY_NAMED) { + ru38 = ru38.under; + }; + if (ru38 != nil) { + if (ru38.kind == tykind.TY_TAGGED + && rhs.kind != nkind.N_IDENT + && ru38.size: i32 > TUPLE_GPCAP * 8) { + let m38e: str = "#40: widening tagged return-forward of a >32B source needs mem-to-mem tag-remap (unwired)\n"; + os.write(2, m38e.ptr, m38e.len: u64); + os.exit(1); + }; + }; + emitline("\tMOVQ\t"); + emitoff(sa38v: i64); + emitline("(BP), BX\n"); + cgwidentaggedstore(c, c.fnret.type_: *tinfo, rhs, + "BX", 0, slotsize(c, c.fnret)); + emitline("\tMOVQ\t"); + emitoff(sa38v: i64); + emitline("(BP), AX\n"); + emitline("\tMOVQ\tBP, SP\n"); + emitline("\tPOPQ\tBP\n"); + emitline("\tRET\n"); + c.lastwasreturn = 1; + return; + }; // Struct payload or tagged-subset return — materialise // the widened value in scratch via cgwidentaggedstore // (handles tag remap and zero pad), then load AX/DX/CX @@ -29709,6 +29952,30 @@ fn cgreturn(c: *cgen, n: *node) void = { // the void variant: emit its tag. Payload is undefined // (void has size 0). Otherwise zero AX for determinism. if (istaggedtype(c, c.fnret)) { + // #38b: an sret-classified tagged return (slot > the + // AX/DX/CX/R8 cursor) writes the void-variant tag + // through *(@sretarg) and returns the dest pointer — + // the cursor can't carry the slot and the caller reads + // memory. Mirrors cstage cgen.c N_RETURN bare arm. + if (sretretsize(c, c.fnret) > 0) { + let sa38: i32 = localfind(c, "@sretarg"); + let vidx38: i32 = voidvariantindex(c.fnret); + if (vidx38 < 0) { vidx38 = 0; }; + emitline("\tMOVQ\t"); + emitoff(sa38: i64); + emitline("(BP), BX\n"); + emitline("\tMOVQ\t$"); + emitint(vidx38: i64); + emitline(", (BX)\n"); + emitline("\tMOVQ\t"); + emitoff(sa38: i64); + emitline("(BP), AX\n"); + emitline("\tMOVQ\tBP, SP\n"); + emitline("\tPOPQ\tBP\n"); + emitline("\tRET\n"); + c.lastwasreturn = 1; + return; + }; if (isnullabletype(c.fnret)) { // null = void variant; AX = 0. emitline("\tMOVQ\t$0, AX\n"); @@ -30126,10 +30393,44 @@ fn cglet(c: *cgen, n: *node) void = { // handles nullable fold, tagged source (ident or AX/DX/CX // ABI call), struct payload (literal/ident), str payload, // scalar payload — with tag remap for tagged-subset widening. + // + // #38b: an sret-classified tagged CALL result is in memory, + // not the cursor — an exact-type receive falls through to the + // generic sret receive below (the let's slot IS the dest); a + // widening receive needs mem-to-mem tag-remap (#40, unwired). + // Mirrors cstage cgen.c N_LET tagged arm. if (istaggedtype(c, tn)) { - cgwidentaggedstore(c, tn.type_: *tinfo, rhs, "BP", off, sz); - c.lastwasreturn = 0; - return; + let letsret: i32 = 0; + if (rhs.kind == nkind.N_CALL) { + letsret = callsretsize(c, rhs); + }; + if (letsret == 0) { + cgwidentaggedstore(c, tn.type_: *tinfo, rhs, + "BP", off, sz); + c.lastwasreturn = 0; + return; + }; + let lru: *tinfo = rhs.type_: *tinfo; + for (lru != nil && lru.kind == tykind.TY_NAMED) { + lru = lru.under; + }; + let llu: *tinfo = tn.type_: *tinfo; + for (llu != nil && llu.kind == tykind.TY_NAMED) { + llu = llu.under; + }; + let exact38: bool = false; + if (lru != nil && lru == llu) { exact38 = true; } + else { + if (typeeq(rhs.type_: *tinfo, tn.type_: *tinfo)) { + exact38 = true; + }; + }; + if (!exact38) { + let m40c: str = "#40: sret-class call result cannot be widened into a tagged slot (mem-to-mem widen unwired)\n"; + os.write(2, m40c.ptr, m40c.len: u64); + os.exit(1); + }; + // fall through to the generic sret receive below. }; // 32B tuple init for `let t: (scalar, str) = call()` / // `let t: (str, scalar) = call()` (#105 / #164/#107). Each diff --git a/test/wcc/926_tagged_sret_run.c b/test/wcc/926_tagged_sret_run.c new file mode 100644 index 00000000..37d25d06 --- /dev/null +++ b/test/wcc/926_tagged_sret_run.c @@ -0,0 +1,516 @@ +/* + * 926_tagged_sret_run — tagged-union sret returns (#38b): a tagged + * RETURN whose slot exceeds the AX/DX/CX/R8 register cursor + * (TUPLE_GPCAP eightbytes = 32B; tag + 3 payload words) routes + * through the SysV sret discipline instead of silently truncating + * payload word 4+ in the callee frame. + * + * Pre-#38 both stages emitted the SAME truncating cursor loads + * (gate-blind byte-id): the regex-shaped 56B payload (64B slot) + * died at the return crossing while locals, args, and the widener's + * memory stores were all correct. errors.errno's 40B (errors.error) + * slot was the latent in-tree instance — benign only because no + * consumer read opaque_data word 2. + * + * Three checks per row: + * - byte-id: w6c vs w6c_ww .s must be identical (rule 10). + * - sret-presence: rows flagged `sret` must emit the hidden-RDI + * `LEAQ (BP), DI` caller dest; boundary rows (slot EXACTLY + * 32B — the (str|nomem)/(s3|bool) class) must NOT — an + * off-by-one in the classifier flips every (T|nomem) consumer + * in the tree (ken's top #38 risk). + * - runtime: build via the ww / ww_ww drivers and run; the exit + * code pins every payload word INCLUDING the last (the + * truncation signature). + * Rows flagged `buildfail` must be REJECTED by both stages (the + * rule-7 loud-stops over the unwired #40-family shapes: widening + * sret forward, `?`/arg-position consumption of an sret-class call + * result). + */ +#include +#include +#include +#include +#include +#include + +static int +runwait(const char *cmd) +{ + int rc = system(cmd); + if (rc == -1) return -1; + if (WIFEXITED(rc)) return WEXITSTATUS(rc); + return -1; +} + +struct row { + const char *label; + const char *src; + int want; /* expected exit code (run rows) */ + int sret; /* 1: .s must contain LEAQ..DI; 0: must not */ + int buildfail; /* 1: both stages must reject (loud-stop) */ +}; + +/* Shared regex-shaped prelude: 56B payload (two slices + i64) inside + * a 3-variant union = 64B slot, the lib/regex compile() return shape + * that surfaced #38. */ +#define WIDE_TYPES \ + "type oops = !str;\n" \ + "type nomem = !void;\n" \ + "type wide = struct { xs: []u8, ys: []u8, n: i64 };\n" + +#define WIDE_MAIN_CHECK \ + " case let e: oops => return 13;\n" \ + " case nomem => return 14;\n" \ + " };\n" \ + " return 0;\n" \ + "};\n" + +static const struct row rows[] = { + /* Literal source; every field checked, n (the LAST payload + * word, dead pre-#38) checked FIRST so its loss is exit 1. */ + { "wide_lit_roundtrip", + WIDE_TYPES + "fn mk(b: []u8, n: i64) (wide | oops | nomem) = {\n" + " return wide { xs = b, ys = b, n = n };\n" + "};\n" + "export fn main() i32 = {\n" + " let buf: [4]u8 = [9u8, 8u8, 7u8, 6u8];\n" + " match (mk(buf[0:4], 42)) {\n" + " case let w: wide => {\n" + " if (w.n != 42) { return 1; };\n" + " if (w.xs.len != 4) { return 2; };\n" + " if (w.ys.len != 4) { return 3; };\n" + " if (w.xs[3] != 6u8) { return 4; };\n" + " };\n" + WIDE_MAIN_CHECK, + 0, 1, 0 }, + /* Local-ident source (the widener's mem-to-mem struct arm). */ + { "wide_local_roundtrip", + WIDE_TYPES + "fn mk(b: []u8, n: i64) (wide | oops | nomem) = {\n" + " let w: wide = wide { xs = b, ys = b, n = n };\n" + " return w;\n" + "};\n" + "export fn main() i32 = {\n" + " let buf: [4]u8 = [1u8, 2u8, 3u8, 4u8];\n" + " let r: (wide | oops | nomem) = mk(buf[0:4], 7);\n" + " match (r) {\n" + " case let w: wide => {\n" + " if (w.n != 7) { return 1; };\n" + " if (w.ys[0] != 1u8) { return 2; };\n" + " };\n" + WIDE_MAIN_CHECK, + 0, 1, 0 }, + /* let-receive then ASSIGN-receive into the same slot, with + * frame canaries (locals around the receives must survive). */ + { "wide_assign_receive", + WIDE_TYPES + "fn mk(b: []u8, n: i64) (wide | oops | nomem) = {\n" + " return wide { xs = b, ys = b, n = n };\n" + "};\n" + "export fn main() i32 = {\n" + " let canary1: i64 = 111;\n" + " let buf: [4]u8 = [1u8, 2u8, 3u8, 4u8];\n" + " let r: (wide | oops | nomem) = mk(buf[0:4], 5);\n" + " let canary2: i64 = 222;\n" + " r = mk(buf[0:4], 6);\n" + " if (canary1 != 111) { return 21; };\n" + " if (canary2 != 222) { return 22; };\n" + " match (r) {\n" + " case let w: wide => { if (w.n != 6) { return 1; }; };\n" + WIDE_MAIN_CHECK, + 0, 1, 0 }, + /* match-scrutinee receive — the tagged-specific arm with no + * tuple precedent: the scrut slot itself is the sret dest. */ + { "wide_match_scrutinee", + WIDE_TYPES + "fn mk(b: []u8, n: i64) (wide | oops | nomem) = {\n" + " return wide { xs = b, ys = b, n = n };\n" + "};\n" + "export fn main() i32 = {\n" + " let buf: [4]u8 = [1u8, 2u8, 3u8, 4u8];\n" + " match (mk(buf[0:4], 9)) {\n" + " case let w: wide => { if (w.n != 9) { return 1; }; };\n" + WIDE_MAIN_CHECK, + 0, 1, 0 }, + /* Exact-type return-forward: inner sret's straight into + * outer's caller dest (cg_sret_forward / c.sretforward). */ + { "wide_forward_exact", + WIDE_TYPES + "fn inner(b: []u8, n: i64) (wide | oops | nomem) = {\n" + " return wide { xs = b, ys = b, n = n };\n" + "};\n" + "fn outer(b: []u8, n: i64) (wide | oops | nomem) = {\n" + " return inner(b, n);\n" + "};\n" + "export fn main() i32 = {\n" + " let buf: [4]u8 = [1u8, 2u8, 3u8, 4u8];\n" + " match (outer(buf[0:4], 11)) {\n" + " case let w: wide => { if (w.n != 11) { return 1; }; };\n" + WIDE_MAIN_CHECK, + 0, 1, 0 }, + /* str-payload error variant through the 64B slot (the widener + * str arm writing *(@sretarg)). */ + { "wide_str_error_variant", + WIDE_TYPES + "fn mk(b: []u8, n: i64) (wide | oops | nomem) = {\n" + " if (n < 0) { return \"neg\": oops; };\n" + " return wide { xs = b, ys = b, n = n };\n" + "};\n" + "export fn main() i32 = {\n" + " let buf: [4]u8 = [1u8, 2u8, 3u8, 4u8];\n" + " match (mk(buf[0:4], -1)) {\n" + " case let w: wide => return 1;\n" + " case let e: oops => {\n" + " if ((e: str).len != 3) { return 2; };\n" + " };\n" + " case nomem => return 3;\n" + " };\n" + " return 0;\n" + "};\n", + 0, 1, 0 }, + /* Two wide results live at once: distinct @match-independent + * receive slots, no shared-scratch clobber. */ + { "wide_multicall", + WIDE_TYPES + "fn mk(b: []u8, n: i64) (wide | oops | nomem) = {\n" + " return wide { xs = b, ys = b, n = n };\n" + "};\n" + "export fn main() i32 = {\n" + " let buf: [4]u8 = [1u8, 2u8, 3u8, 4u8];\n" + " let r1: (wide | oops | nomem) = mk(buf[0:4], 100);\n" + " let r2: (wide | oops | nomem) = mk(buf[0:4], 200);\n" + " match (r1) {\n" + " case let w: wide => { if (w.n != 100) { return 1; }; };\n" + " case let e: oops => return 13;\n" + " case nomem => return 14;\n" + " };\n" + " match (r2) {\n" + " case let w: wide => { if (w.n != 200) { return 2; }; };\n" + WIDE_MAIN_CHECK, + 0, 1, 0 }, + /* Bare `return;` (void variant) through a 40B slot: the tag + * must land in *(@sretarg), not AX. */ + { "wide_bare_return_void", + "type s4 = struct { a: i64, b: i64, c: i64, d: i64 };\n" + "fn maybe(n: i64) (s4 | void) = {\n" + " if (n == 0) { return; };\n" + " return s4 { a = 1, b = 2, c = 3, d = n };\n" + "};\n" + "export fn main() i32 = {\n" + " match (maybe(0)) {\n" + " case let v: s4 => return 1;\n" + " case void => { };\n" + " };\n" + " match (maybe(5)) {\n" + " case let v: s4 => { if (v.d != 5) { return 2; }; };\n" + " case void => return 3;\n" + " };\n" + " return 0;\n" + "};\n", + 0, 1, 0 }, + /* The #38 repro shape: 32B struct payload = 40B slot, literal + * AND local sources (was exit 2 / silent d-drop at master). */ + { "wide_s4_repro", + "type s4 = struct { a: i64, b: i64, c: i64, d: i64 };\n" + "fn taglit4() (s4 | bool) = {\n" + " return s4 { a = 1, b = 2, c = 3, d = 4 };\n" + "};\n" + "fn taglocal4() (s4 | bool) = {\n" + " let v: s4 = s4 { a = 1, b = 2, c = 3, d = 4 };\n" + " return v;\n" + "};\n" + "export fn main() i32 = {\n" + " match (taglit4()) {\n" + " case let r: s4 => { if (r.d != 4) { return 2; }; };\n" + " case let b: bool => return 3;\n" + " };\n" + " match (taglocal4()) {\n" + " case let r: s4 => { if (r.d != 4) { return 4; }; };\n" + " case let b: bool => return 5;\n" + " };\n" + " return 0;\n" + "};\n", + 0, 1, 0 }, + /* errno-shaped graduation row: errors.error's opaque_ variant is + * struct { strerror *fn (8B), data [3]u64 (24B) } = 32B payload = + * 40B slot — the LIVE in-tree #38 instance that truncated + * benignly-by-luck at master (data[2] was never read). This row + * READS the previously-dropped tail word first, pinning the + * errors.errno sret graduation. Local-ident return source, like + * errno's `return err;`. */ + { "errno_shaped_tail_read", + "type opq = struct { h: i64, data: [3]u64 };\n" + "fn wrap(e: i64) (opq | bool) = {\n" + " let o: opq;\n" + " o.h = 7;\n" + " o.data[0] = e: u64;\n" + " o.data[1] = 1111u64;\n" + " o.data[2] = 2222u64;\n" + " return o;\n" + "};\n" + "export fn main() i32 = {\n" + " match (wrap(5)) {\n" + " case let o: opq => {\n" + " if (o.data[2] != 2222u64) { return 1; };\n" + " if (o.data[1] != 1111u64) { return 2; };\n" + " if (o.data[0] != 5u64) { return 3; };\n" + " if (o.h != 7) { return 4; };\n" + " };\n" + " case let b: bool => return 5;\n" + " };\n" + " return 0;\n" + "};\n", + 0, 1, 0 }, + /* BOUNDARY: 24B struct payload = EXACTLY 32B slot — must stay + * register-ABI (sret==0 pins no hidden-RDI LEAQ in the .s). */ + { "boundary_s3_register", + "type s3 = struct { a: i64, b: i64, c: i64 };\n" + "fn mk(ok: bool) (s3 | bool) = {\n" + " if (!ok) { return false; };\n" + " return s3 { a = 7, b = 8, c = 9 };\n" + "};\n" + "export fn main() i32 = {\n" + " match (mk(true)) {\n" + " case let v: s3 => { if (v.c != 9) { return 1; }; };\n" + " case let b: bool => return 2;\n" + " };\n" + " match (mk(false)) {\n" + " case let v: s3 => return 3;\n" + " case let b: bool => { if (b) { return 4; }; };\n" + " };\n" + " return 0;\n" + "};\n", + 0, 0, 0 }, + /* BOUNDARY: (str|nomem) — the 32B-slot class fmt/io/strconv + * return everywhere; flipping it to sret breaks the tree. */ + { "boundary_str_nomem_register", + "type nomem = !void;\n" + "fn pick(ok: bool) (str | nomem) = {\n" + " if (ok) { return \"hello\"; };\n" + " let nm: nomem;\n" + " return nm;\n" + "};\n" + "export fn main() i32 = {\n" + " match (pick(true)) {\n" + " case let s: str => { if (s.len != 5) { return 1; }; };\n" + " case nomem => return 2;\n" + " };\n" + " match (pick(false)) {\n" + " case let s: str => return 3;\n" + " case nomem => { };\n" + " };\n" + " return 0;\n" + "};\n", + 0, 0, 0 }, + /* LOUD-STOP: widening return-forward of an sret-class source + * ((wide|oops) -> (wide|oops|nomem)) needs the mem-to-mem + * tag-remap — unwired, filed #40. Must NOT compile. */ + { "fail_widening_forward", + WIDE_TYPES + "fn inner(b: []u8) (wide | oops) = {\n" + " return wide { xs = b, ys = b, n = 1 };\n" + "};\n" + "fn outer(b: []u8) (wide | oops | nomem) = {\n" + " return inner(b);\n" + "};\n" + "export fn main() i32 = {\n" + " let buf: [4]u8 = [1u8, 2u8, 3u8, 4u8];\n" + " match (outer(buf[0:4])) {\n" + " case let w: wide => return 0;\n" + WIDE_MAIN_CHECK, + 0, 0, 1 }, + /* LOUD-STOP: `!` consuming an sret-class call result reads the + * cursor the callee never filled — #40-family follow-up. */ + { "fail_tryunw_wide", + WIDE_TYPES + "fn mk(b: []u8) (wide | oops | nomem) = {\n" + " return wide { xs = b, ys = b, n = 1 };\n" + "};\n" + "export fn main() i32 = {\n" + " let buf: [4]u8 = [1u8, 2u8, 3u8, 4u8];\n" + " let w: wide = mk(buf[0:4])!;\n" + " if (w.n != 1) { return 1; };\n" + " return 0;\n" + "};\n", + 0, 0, 1 }, + /* LOUD-STOP: an sret-class tagged call result in argument + * position (cursor push of a memory result). */ + { "fail_wide_call_arg", + WIDE_TYPES + "fn mk(b: []u8) (wide | oops | nomem) = {\n" + " return wide { xs = b, ys = b, n = 1 };\n" + "};\n" + "fn use(r: (wide | oops | nomem)) i64 = {\n" + " match (r) {\n" + " case let w: wide => return w.n;\n" + " case let e: oops => return -1;\n" + " case nomem => return -2;\n" + " };\n" + " return -3;\n" + "};\n" + "export fn main() i32 = {\n" + " let buf: [4]u8 = [1u8, 2u8, 3u8, 4u8];\n" + " if (use(mk(buf[0:4])) != 1) { return 1; };\n" + " return 0;\n" + "};\n", + 0, 0, 1 }, +}; + +static const char *g_bin; + +/* compile one row with `tool` (w6c or w6c_ww) into outpath; returns + * the tool's exit code. */ +static int +compile_s(const char *tool, const char *src, const char *outpath) +{ + char cmd[1024]; + snprintf(cmd, sizeof cmd, "%s/%s %s > %s 2>/dev/null", + g_bin, tool, src, outpath); + return runwait(cmd); +} + +static int +file_eq(const char *a, const char *b) +{ + char cmd[1024]; + snprintf(cmd, sizeof cmd, "cmp -s %s %s", a, b); + return runwait(cmd) == 0; +} + +static int +file_has(const char *path, const char *needle) +{ + FILE *f = fopen(path, "rb"); + if (!f) return 0; + static char buf[1 << 20]; + size_t n = fread(buf, 1, sizeof buf - 1, f); + fclose(f); + buf[n] = '\0'; + return strstr(buf, needle) != NULL; +} + +static int +run_driver(const char *driver, const char *src, const char *label) +{ + char tmpdir[128], cmd[1024]; + snprintf(tmpdir, sizeof tmpdir, "/tmp/tsret_%d_d", getpid()); + mkdir(tmpdir, 0755); + snprintf(cmd, sizeof cmd, "cd %s && %s/%s build %s >/dev/null 2>&1", + tmpdir, g_bin, driver, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: build via %s failed\n", + label, driver); + return -1; + } + const char *base = strrchr(src, '/'); + base = base ? base + 1 : src; + char outbin[256]; + snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base); + char *dot = strrchr(outbin, '.'); + if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; + int got = runwait(outbin); + unlink(outbin); + rmdir(tmpdir); + return got; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + static char absbin[512]; + if (bin[0] != '/') { + char cwd[256]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + g_bin = bin; + + int n = (int)(sizeof rows / sizeof rows[0]); + int total = 0, fail = 0; + for (int i = 0; i < n; i++) { + const struct row *r = &rows[i]; + char src[128], cs_s[128], ww_s[128]; + snprintf(src, sizeof src, "/tmp/tsret_%d_%d.ww", + getpid(), i); + snprintf(cs_s, sizeof cs_s, "/tmp/tsret_%d_%d_cs.s", + getpid(), i); + snprintf(ww_s, sizeof ww_s, "/tmp/tsret_%d_%d_ww.s", + getpid(), i); + FILE *f = fopen(src, "wb"); + if (!f) return 1; + fputs("package main;\n\n", f); + fputs(r->src, f); + fclose(f); + + int cs_rc = compile_s("w6c", src, cs_s); + int ww_rc = compile_s("w6c_ww", src, ww_s); + if (r->buildfail) { + total++; + if (cs_rc == 0 || ww_rc == 0) { + fprintf(stderr, "FAIL row[%s]: loud-stop " + "expected, cstage rc=%d wwstage rc=%d\n", + r->label, cs_rc, ww_rc); + fail++; + } + unlink(src); unlink(cs_s); unlink(ww_s); + continue; + } + total++; + if (cs_rc != 0 || ww_rc != 0) { + fprintf(stderr, "FAIL row[%s]: compile rc cs=%d " + "ww=%d\n", r->label, cs_rc, ww_rc); + fail++; + unlink(src); unlink(cs_s); unlink(ww_s); + continue; + } + if (!file_eq(cs_s, ww_s)) { + fprintf(stderr, "FAIL row[%s]: cs != ww .s\n", + r->label); + fail++; + } + /* hidden-RDI dest: present iff the row is sret-class. + * The boundary rows pin the 32B class stays register. */ + int has_di = file_has(cs_s, "(BP), DI\n"); + if (r->sret && !has_di) { + fprintf(stderr, "FAIL row[%s]: expected sret " + "hidden-RDI LEAQ, none emitted\n", r->label); + fail++; + } + if (!r->sret && has_di) { + fprintf(stderr, "FAIL row[%s]: 32B-slot row " + "flipped to sret (classifier boundary " + "off-by-one)\n", r->label); + fail++; + } + int got_cs = run_driver("ww", src, r->label); + if (got_cs != r->want) { + fprintf(stderr, "FAIL row[%s] cstage: want %d " + "got %d\n", r->label, r->want, got_cs); + fail++; + } + char wwdrv[600]; + snprintf(wwdrv, sizeof wwdrv, "%s/ww_ww", g_bin); + if (access(wwdrv, X_OK) == 0) { + int got_ww = run_driver("ww_ww", src, r->label); + if (got_ww != r->want) { + fprintf(stderr, "FAIL row[%s] wwstage: " + "want %d got %d\n", + r->label, r->want, got_ww); + fail++; + } + } + unlink(src); unlink(cs_s); unlink(ww_s); + } + if (fail) { + fprintf(stderr, "tagged_sret_run: %d/%d rows failed\n", + fail, total); + return 1; + } + printf("tagged_sret_run: %d rows ok\n", total); + return 0; +}