diff --git a/Makefile b/Makefile index bf0af3cb..9de6d201 100644 --- a/Makefile +++ b/Makefile @@ -344,6 +344,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_xmod_valglobal_run \ $(BIN)/test_xmod_valglobal_dot_run \ $(BIN)/test_len_strglobal_run \ + $(BIN)/test_tuple_sret_callee \ $(BIN)/test_widen_pad_zero_run \ $(BIN)/test_named_ptr_alias_variant_widen \ $(BIN)/test_single_field_struct_zeroinit \ @@ -840,6 +841,15 @@ $(BIN)/test_len_strglobal_run: test/wcc/797_len_strglobal_run.c \ $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +# #10 Fold A (wide tuple-return / sret, CALLEE side): an over-cap tuple +# return (> 4 GP or > 2 SSE eightbytes) now compiles via sret instead of +# loud-stopping at the SEND. Compile + cs==ww byte-id only — the receive +# stays loud-stopped (Fold B wires the round-trip). Self-contained probes. +$(BIN)/test_tuple_sret_callee: test/wcc/798_tuple_sret_callee.c \ + $(BIN)/ww $(BIN)/w6c $(BIN)/w6c_ww $(BIN)/w6a $(BIN)/w6l \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + # #15: widening a bare *vtable into a NAMED-alias variant (`stream` = # *vtable) of `(file | stream)` must compute the right tag, not default # to tag 0. Both-stage byte-id + runtime, plus a degenerate-ambiguity diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index e3a446bb..52a45bc2 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -159,18 +159,11 @@ type_chase_named(Type *t) return t; } -/* cg_sret_retsize — if `rt` is a plain TY_STRUCT > 24B, return its - * natural size (the sret threshold); else 0. Tagged unions, tuples, - * str, and slices route through their existing register-return ABIs - * regardless of size. Task #23. */ -static int -cg_sret_retsize(Type *rt) -{ - rt = type_chase_named(rt); - if (rt == NULL || rt->kind != TY_STRUCT) return 0; - if ((int)rt->size <= 24) return 0; - return (int)rt->size; -} +/* cg_sret_retsize — sret classifier; defined after the tuple register- + * return helpers (tuple_rseq / tuple_ebytes / fld_isfloat) it consults + * for the over-cap-tuple arm. Forward-declared here for the earlier + * callers (cgcall, fn prologue). Task #23 / #10. */ +static int cg_sret_retsize(Type *rt); static int node_isfloat(Node *n) @@ -242,6 +235,14 @@ static const int tuple_rseq[] = { D_AX, D_DX, D_CX, D_R8 }; * return convergence (#171) is a call-site swap, not a redesign. */ static const int tuple_sse_seq[] = { D_X0, D_X1 }; +/* #10: the register-return-ABI caps — the SINGLE SSoT shared by the sret + * classifier (cg_sret_retsize over-cap-tuple arm) AND every emit/receive + * site (N_RETURN tuple SEND, N_MLET/N_MASSIGN destructure, cgcall guard). + * Classify and emit MUST agree on these, else a tuple gets classified + * sret by one and in-reg by the other → corruption. */ +#define TUPLE_GPCAP ((int)nelem(tuple_rseq)) +#define TUPLE_SSECAP ((int)nelem(tuple_sse_seq)) + static int tuple_ebytes(int wide) { @@ -293,6 +294,41 @@ fld_isfloat(Type *t, int *isf32) return 0; } +/* cg_sret_retsize — sret classification by natural return size: + * - plain TY_STRUCT > 24B → its natural size (the #23 threshold). + * - TY_TUPLE whose SysV register-return footprint exceeds the caps + * (> TUPLE_GPCAP integer eightbytes or > TUPLE_SSECAP float + * eightbytes) → its natural total size, so the callee returns it + * via sret instead of registers (#10). The element footprint walk + * matches the N_RETURN tuple SEND exactly (a float = 1 SSE + * eightbyte, a slice/str its 3-word header, a scalar 1 GP word). + * Everything else (in-cap tuples, tagged unions, str, slices, scalars) + * routes through its register-return ABI → 0. */ +static int +cg_sret_retsize(Type *rt) +{ + rt = type_chase_named(rt); + if (rt == NULL) return 0; + if (rt->kind == TY_STRUCT) + return (int)rt->size <= 24 ? 0 : (int)rt->size; + if (rt->kind == TY_TUPLE) { + int gptotal = 0, ssecount = 0, f32; + for (Tparam *p = rt->params; p; p = p->next) { + Type *pu = type_chase_named(p->type); + int wide = pu && (pu->kind == TY_SLICE + || pu->kind == TY_STR); + if (fld_isfloat(p->type, &f32)) + ssecount++; + else + gptotal += tuple_ebytes(wide); + } + if (gptotal > TUPLE_GPCAP || ssecount > TUPLE_SSECAP) + return (int)rt->size; + return 0; + } + return 0; +} + /* fld_issigned — true iff a sub-word field/element load needs sign * extension (i8 → MOVSBQ, i16 → MOVSWQ, i32 → MOVSXD). Follows NAMED * and ENUM aliases via type_isunsigned, then peels off the unsigned @@ -5843,10 +5879,7 @@ cgexpr(Cg *c, Node *n, Local *locals) /* The producing call already satisfied #164's * return caps; guard anyway (tuple_store indexes * tuple_rseq[4] / tuple_sse_seq[2]). */ - if (gptot > (int)(sizeof tuple_rseq - / sizeof tuple_rseq[0]) - || sstot > (int)(sizeof tuple_sse_seq - / sizeof tuple_sse_seq[0])) + if (gptot > TUPLE_GPCAP || sstot > TUPLE_SSECAP) fatal("tuple arg exceeds return-cursor ABI " "capacity; see #163/#164"); if (cg_tupargscr == 0) { @@ -8687,9 +8720,7 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) * Both rows are loud-stopped at their cap (rule-7, never a * silent collide): INTEGER 4, SSE 2. The SAME class split * drives the receive sites. */ - int gpcap = (int)(sizeof tuple_rseq / sizeof tuple_rseq[0]); - int ssecap = (int)(sizeof tuple_sse_seq - / sizeof tuple_sse_seq[0]); + int ssecap = TUPLE_SSECAP; int gptotal = 0, ssecount = 0, f32; for (Node *e = n->lhs->list; e; e = e->next) { if (fld_isfloat(e->type, &f32)) @@ -8698,14 +8729,49 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) gptotal += tuple_ebytes(node_isstr(e) || node_isslice(e)); } - if (gptotal > gpcap) - fatal("tuple return exceeds integer register-return " - "ABI capacity (%d eightbytes: AX,DX,CX,R8); " - "see return-ABI #10", gpcap); - if (ssecount > ssecap) - fatal("tuple return exceeds SSE register-return ABI " - "capacity (%d eightbytes: X0,X1); " - "see return-ABI #10", ssecap); + if (gptotal > TUPLE_GPCAP || ssecount > ssecap) { + /* #10 Fold A: over-cap tuple returns via sret. The + * prologue wired @sretarg (cg_sret_retsize agrees on + * the caps — the shared SSoT), holding the caller- + * prealloc dest. Store each element through + * *(@sretarg) at its packed layout offset (running + * sum of element sizes — the t.0/t.1 positional + * layout, N_DOT TY_TUPLE arm), each at its natural + * width so a narrow tail doesn't over-MOVQ (#169); + * the dest base is reloaded into DX each step since a + * wide element's cgexpr clobbers AX/BX/CX. Then reuse + * the struct-sret epilogue. The CALL/receive side + * stays loud-stopped (#10 Fold B). */ + int foff = 0; + for (Node *e = n->lhs->list; e; e = e->next) { + int isflt = fld_isfloat(e->type, &f32); + int wide = node_isstr(e) || node_isslice(e); + int esz = e->type ? (int)e->type->size : 8; + cgexpr(c, e, *locals); + ins2(c, A_MOVQ, + amem(D_BP, cg_sret_arg_off), areg(D_DX)); + if (isflt) + ins2(c, f32 ? A_MOVSS : A_MOVSD, + areg(D_X0), amem(D_DX, foff)); + else if (wide) { + ins2(c, A_MOVQ, areg(D_AX), + amem(D_DX, foff + 0)); + ins2(c, A_MOVQ, areg(D_BX), + amem(D_DX, foff + 8)); + ins2(c, A_MOVQ, areg(D_CX), + amem(D_DX, foff + 16)); + } else + ins2(c, fldstoreop(e->type, esz), + areg(D_AX), amem(D_DX, foff)); + foff += esz; + } + 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; + } int fscr = 0; if (ssecount > 0) { if (cg_tupfscr != 0) @@ -8936,9 +9002,8 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) * INTEGER cursor into an 8B slot. Both rows loud-stop at their * cap. */ cgexpr(c, n->rhs, *locals); - int gpcap = (int)(sizeof tuple_rseq / sizeof tuple_rseq[0]); - int ssecap = (int)(sizeof tuple_sse_seq - / sizeof tuple_sse_seq[0]); + int gpcap = TUPLE_GPCAP; + int ssecap = TUPLE_SSECAP; int gptotal = 0, ssetotal = 0, lf32; for (Node *l = n->list; l; l = l->next) { Type *t = l->type; @@ -8995,9 +9060,8 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) Type *rt = n->rhs ? n->rhs->type : NULL; Type *ru = (rt && rt->kind == TY_NAMED) ? rt->under : rt; Tparam *tp0 = (ru && ru->kind == TY_TUPLE) ? ru->params : NULL; - int gpcap = (int)(sizeof tuple_rseq / sizeof tuple_rseq[0]); - int ssecap = (int)(sizeof tuple_sse_seq - / sizeof tuple_sse_seq[0]); + int gpcap = TUPLE_GPCAP; + int ssecap = TUPLE_SSECAP; int gptotal = 0, ssetotal = 0, mf32; for (Tparam *tp = tp0; tp; tp = tp->next) { Type *u = (tp->type && tp->type->kind == TY_NAMED) diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 8d302276..c4e5edba 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -15493,7 +15493,7 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = { }; // The producing call already satisfied #164's return caps; // guard anyway (tupstore indexes [AX,DX,CX,R8] / [X0,X1]). - if (gptot > 4 || sstot > 2) { + if (gptot > TUPLE_GPCAP || sstot > TUPLE_SSECAP) { let msg: str = "tuple arg exceeds return-cursor ABI capacity; see #163/#164\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); @@ -16258,6 +16258,34 @@ export fn sretretsize(c: *cgen, t: *node) i32 = { r = r.lhs; if (r == nil) { return 0; }; }; + 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 = + // 1 SSE eightbyte, a slice/str its 3-word header, a scalar 1 + // GP word. Return the tuple's natural total size (tinfo.size, + // the type table) so the callee returns via sret. Mirrors + // cstage cg_sret_retsize TY_TUPLE arm; TUPLE_GPCAP/TUPLE_SSECAP + // are the shared cap SSoT with the cgreturn SEND emitter. + let ssecap: i32 = TUPLE_SSECAP; + let gptotal: i32 = 0; + let ssecount: i32 = 0; + let pt: *node = r.list; + for (pt != nil) { + let et: *node = pt.lhs; + if (isfloattype(c, et)) { + ssecount = ssecount + 1; + } else { + let wide: bool = isstrtype(c, et) || isslicetype(c, et); + gptotal = gptotal + tupebytes(wide); + }; + pt = pt.next; + }; + if (gptotal > TUPLE_GPCAP || ssecount > ssecap) { + let rti: *tinfo = r.type_: *tinfo; + if (rti != nil) { return rti.size: i32; }; + }; + return 0; + }; if (r.kind != nkind.N_TNAME) { return 0; }; // Primitives / aliased-to-primitives are never sret. if (primsize(r.str) > 0) { return 0; }; @@ -26200,7 +26228,7 @@ fn cgreturn(c: *cgen, n: *node) void = { // loud-stop at their cap (rule-7): INTEGER 4, SSE 2. The SAME // class split drives the receive sites. if (rhs.kind == nkind.N_TUPLE) { - let ssecap: i32 = 2; // X0,X1 per SysV + let ssecap: i32 = TUPLE_SSECAP; // X0,X1 per SysV let gptotal: i32 = 0; let ssecount: i32 = 0; let e: *node = rhs.list; @@ -26213,17 +26241,78 @@ fn cgreturn(c: *cgen, n: *node) void = { }; e = e.next; }; - if (gptotal > 4) { // AX,DX,CX,R8 capacity - // pinned loud-stop, inline like cgen.ww:604 (cstage - // uses fatal(), err.c) — surface, don't corrupt. - let msg: str = "tuple return exceeds integer register-return ABI capacity (4 eightbytes: AX,DX,CX,R8); see return-ABI #10\n"; - os.write(2, msg.ptr, msg.len: u64); - os.exit(1); - }; - if (ssecount > ssecap) { - let msg: str = "tuple return exceeds SSE register-return ABI capacity (2 eightbytes: X0,X1); see return-ABI #10\n"; - os.write(2, msg.ptr, msg.len: u64); - os.exit(1); + if (gptotal > TUPLE_GPCAP || ssecount > ssecap) { + // #10 Fold A: over-cap tuple returns via sret. The + // prologue wired @sretarg (sretretsize agrees on the + // caps — TUPLE_GPCAP/TUPLE_SSECAP, the shared SSoT), + // holding the caller-prealloc dest. Store each element + // through *(@sretarg) + // at its packed layout offset (running sum of element + // sizes from the return-type tuple node — the t.0/t.1 + // positional layout), each at its natural width so a + // narrow tail doesn't over-MOVQ (#169); the dest base is + // reloaded into DX each step since a wide element's + // cgexpr clobbers AX/BX/CX. Then reuse the struct-sret + // epilogue. The CALL/receive side stays loud-stopped + // (#10 Fold B). Byte-identical to cstage cgen.c + // N_RETURN over-cap tuple arm. + let saoff: i32 = localfind(c, "@sretarg"); + let pt: *node = nil; + if (c.fnret != nil) { pt = c.fnret.list; }; + let we: *node = rhs.list; + let foff: i32 = 0; + for (we != nil) { + let isflt: bool = isfloattype(c, we); + let wide: bool = nodeisstr(c, we) || nodeisslice(c, we); + let esz: i32 = 8; + if (pt != nil) { + let eti: *tinfo = pt.lhs.type_: *tinfo; + if (eti != nil) { esz = eti.size: i32; }; + }; + cgexpr(c, we); + emitline("\tMOVQ\t"); + emitoff(saoff: i64); + emitline("(BP), DX\n"); + if (isflt) { + let mov: str = "MOVSD"; + if (isf32type(c, we)) { mov = "MOVSS"; }; + emitline("\t"); + emitline(mov); + emitline("\tX0, "); + emitdispreg(foff: i64, "DX"); + emitline("\n"); + } else { + if (wide) { + emitline("\tMOVQ\tAX, "); + emitdispreg(foff: i64, "DX"); + emitline("\n"); + emitline("\tMOVQ\tBX, "); + emitdispreg((foff + 8): i64, "DX"); + emitline("\n"); + emitline("\tMOVQ\tCX, "); + emitdispreg((foff + 16): i64, "DX"); + emitline("\n"); + } else { + let sop: str = tnodestoreop(c, we, esz); + emitline("\t"); + emitline(sop); + emitline("\tAX, "); + emitdispreg(foff: i64, "DX"); + emitline("\n"); + }; + }; + foff += esz; + we = we.next; + if (pt != nil) { pt = pt.next; }; + }; + emitline("\tMOVQ\t"); + emitoff(saoff: i64); + emitline("(BP), AX\n"); + emitline("\tMOVQ\tBP, SP\n"); + emitline("\tPOPQ\tBP\n"); + emitline("\tRET\n"); + c.lastwasreturn = 1; + return; }; let fscr: i32 = 0; if (ssecount > 0) { @@ -27581,7 +27670,7 @@ fn cgmassign(c: *cgen, n: *node) void = { if (n.rhs != nil) { cgexpr(c, n.rhs); }; - let ssecap: i32 = 2; // X0,X1 per SysV + let ssecap: i32 = TUPLE_SSECAP; // X0,X1 per SysV let gptotal: i32 = 0; let ssetotal: i32 = 0; let l: *node = n.list; @@ -27599,7 +27688,7 @@ fn cgmassign(c: *cgen, n: *node) void = { l = l.next; if (pt != nil) { pt = pt.next; }; }; - if (gptotal > 4) { // AX,DX,CX,R8 capacity + if (gptotal > TUPLE_GPCAP) { // AX,DX,CX,R8 capacity // pinned loud-stop, inline like cgen.ww:604 (cstage uses // fatal(), err.c) — surface, don't corrupt. let msg: str = "tuple destructure exceeds integer register-return ABI capacity (4 eightbytes: AX,DX,CX,R8); see return-ABI #10\n"; @@ -27668,7 +27757,7 @@ fn cgmlet(c: *cgen, n: *node) void = { cgexpr(c, rhs); - let ssecap: i32 = 2; // X0,X1 per SysV + let ssecap: i32 = TUPLE_SSECAP; // X0,X1 per SysV let gptotal: i32 = 0; let ssetotal: i32 = 0; let l: *node = n.list; @@ -27688,7 +27777,7 @@ fn cgmlet(c: *cgen, n: *node) void = { l = l.next; if (pt != nil) { pt = pt.next; }; }; - if (gptotal > 4) { // AX,DX,CX,R8 capacity + if (gptotal > TUPLE_GPCAP) { // AX,DX,CX,R8 capacity // pinned loud-stop, inline like cgen.ww:604 (cstage uses // fatal(), err.c) — surface, don't corrupt. let msg: str = "tuple destructure exceeds integer register-return ABI capacity (4 eightbytes: AX,DX,CX,R8); see return-ABI #10\n"; @@ -29110,6 +29199,15 @@ type enumtype = struct { def LOOP_MAX: i32 = 16; def DEFER_MAX: i32 = 16; +// The SysV register-return-ABI caps — the SINGLE SSoT shared by the sret +// classifier (sretretsize over-cap-tuple arm) AND every emit/receive site +// (cgreturn tuple SEND, cgmlet/cgmassign destructure, cgcall arg guard). +// Classify and emit MUST agree on these, else a tuple gets classified +// sret by one and in-reg by the other -> corruption. Mirrors cstage +// cgen.c TUPLE_GPCAP/TUPLE_SSECAP (#10). +def TUPLE_GPCAP: i32 = 4; // AX,DX,CX,R8 +def TUPLE_SSECAP: i32 = 2; // X0,X1 + type cgen = struct { locals: *local, // atlocals — persistent registry of `@`-prefix scratch slots diff --git a/selfhost/cmd/wcc/cgen.ww b/selfhost/cmd/wcc/cgen.ww index ac69a018..b44d1de4 100644 --- a/selfhost/cmd/wcc/cgen.ww +++ b/selfhost/cmd/wcc/cgen.ww @@ -449,6 +449,15 @@ type enumtype = struct { def LOOP_MAX: i32 = 16; def DEFER_MAX: i32 = 16; +// The SysV register-return-ABI caps — the SINGLE SSoT shared by the sret +// classifier (sretretsize over-cap-tuple arm) AND every emit/receive site +// (cgreturn tuple SEND, cgmlet/cgmassign destructure, cgcall arg guard). +// Classify and emit MUST agree on these, else a tuple gets classified +// sret by one and in-reg by the other -> corruption. Mirrors cstage +// cgen.c TUPLE_GPCAP/TUPLE_SSECAP (#10). +def TUPLE_GPCAP: i32 = 4; // AX,DX,CX,R8 +def TUPLE_SSECAP: i32 = 2; // X0,X1 + type cgen = struct { locals: *local, // atlocals — persistent registry of `@`-prefix scratch slots diff --git a/selfhost/cmd/wcc/cgenstmt.ww b/selfhost/cmd/wcc/cgenstmt.ww index 948f4999..4a8ba6d9 100644 --- a/selfhost/cmd/wcc/cgenstmt.ww +++ b/selfhost/cmd/wcc/cgenstmt.ww @@ -266,7 +266,7 @@ fn cgreturn(c: *cgen, n: *node) void = { // loud-stop at their cap (rule-7): INTEGER 4, SSE 2. The SAME // class split drives the receive sites. if (rhs.kind == nkind.N_TUPLE) { - let ssecap: i32 = 2; // X0,X1 per SysV + let ssecap: i32 = TUPLE_SSECAP; // X0,X1 per SysV let gptotal: i32 = 0; let ssecount: i32 = 0; let e: *node = rhs.list; @@ -279,17 +279,78 @@ fn cgreturn(c: *cgen, n: *node) void = { }; e = e.next; }; - if (gptotal > 4) { // AX,DX,CX,R8 capacity - // pinned loud-stop, inline like cgen.ww:604 (cstage - // uses fatal(), err.c) — surface, don't corrupt. - let msg: str = "tuple return exceeds integer register-return ABI capacity (4 eightbytes: AX,DX,CX,R8); see return-ABI #10\n"; - os.write(2, msg.ptr, msg.len: u64); - os.exit(1); - }; - if (ssecount > ssecap) { - let msg: str = "tuple return exceeds SSE register-return ABI capacity (2 eightbytes: X0,X1); see return-ABI #10\n"; - os.write(2, msg.ptr, msg.len: u64); - os.exit(1); + if (gptotal > TUPLE_GPCAP || ssecount > ssecap) { + // #10 Fold A: over-cap tuple returns via sret. The + // prologue wired @sretarg (sretretsize agrees on the + // caps — TUPLE_GPCAP/TUPLE_SSECAP, the shared SSoT), + // holding the caller-prealloc dest. Store each element + // through *(@sretarg) + // at its packed layout offset (running sum of element + // sizes from the return-type tuple node — the t.0/t.1 + // positional layout), each at its natural width so a + // narrow tail doesn't over-MOVQ (#169); the dest base is + // reloaded into DX each step since a wide element's + // cgexpr clobbers AX/BX/CX. Then reuse the struct-sret + // epilogue. The CALL/receive side stays loud-stopped + // (#10 Fold B). Byte-identical to cstage cgen.c + // N_RETURN over-cap tuple arm. + let saoff: i32 = localfind(c, "@sretarg"); + let pt: *node = nil; + if (c.fnret != nil) { pt = c.fnret.list; }; + let we: *node = rhs.list; + let foff: i32 = 0; + for (we != nil) { + let isflt: bool = isfloattype(c, we); + let wide: bool = nodeisstr(c, we) || nodeisslice(c, we); + let esz: i32 = 8; + if (pt != nil) { + let eti: *tinfo = pt.lhs.type_: *tinfo; + if (eti != nil) { esz = eti.size: i32; }; + }; + cgexpr(c, we); + emitline("\tMOVQ\t"); + emitoff(saoff: i64); + emitline("(BP), DX\n"); + if (isflt) { + let mov: str = "MOVSD"; + if (isf32type(c, we)) { mov = "MOVSS"; }; + emitline("\t"); + emitline(mov); + emitline("\tX0, "); + emitdispreg(foff: i64, "DX"); + emitline("\n"); + } else { + if (wide) { + emitline("\tMOVQ\tAX, "); + emitdispreg(foff: i64, "DX"); + emitline("\n"); + emitline("\tMOVQ\tBX, "); + emitdispreg((foff + 8): i64, "DX"); + emitline("\n"); + emitline("\tMOVQ\tCX, "); + emitdispreg((foff + 16): i64, "DX"); + emitline("\n"); + } else { + let sop: str = tnodestoreop(c, we, esz); + emitline("\t"); + emitline(sop); + emitline("\tAX, "); + emitdispreg(foff: i64, "DX"); + emitline("\n"); + }; + }; + foff += esz; + we = we.next; + if (pt != nil) { pt = pt.next; }; + }; + emitline("\tMOVQ\t"); + emitoff(saoff: i64); + emitline("(BP), AX\n"); + emitline("\tMOVQ\tBP, SP\n"); + emitline("\tPOPQ\tBP\n"); + emitline("\tRET\n"); + c.lastwasreturn = 1; + return; }; let fscr: i32 = 0; if (ssecount > 0) { @@ -1647,7 +1708,7 @@ fn cgmassign(c: *cgen, n: *node) void = { if (n.rhs != nil) { cgexpr(c, n.rhs); }; - let ssecap: i32 = 2; // X0,X1 per SysV + let ssecap: i32 = TUPLE_SSECAP; // X0,X1 per SysV let gptotal: i32 = 0; let ssetotal: i32 = 0; let l: *node = n.list; @@ -1665,7 +1726,7 @@ fn cgmassign(c: *cgen, n: *node) void = { l = l.next; if (pt != nil) { pt = pt.next; }; }; - if (gptotal > 4) { // AX,DX,CX,R8 capacity + if (gptotal > TUPLE_GPCAP) { // AX,DX,CX,R8 capacity // pinned loud-stop, inline like cgen.ww:604 (cstage uses // fatal(), err.c) — surface, don't corrupt. let msg: str = "tuple destructure exceeds integer register-return ABI capacity (4 eightbytes: AX,DX,CX,R8); see return-ABI #10\n"; @@ -1734,7 +1795,7 @@ fn cgmlet(c: *cgen, n: *node) void = { cgexpr(c, rhs); - let ssecap: i32 = 2; // X0,X1 per SysV + let ssecap: i32 = TUPLE_SSECAP; // X0,X1 per SysV let gptotal: i32 = 0; let ssetotal: i32 = 0; let l: *node = n.list; @@ -1754,7 +1815,7 @@ fn cgmlet(c: *cgen, n: *node) void = { l = l.next; if (pt != nil) { pt = pt.next; }; }; - if (gptotal > 4) { // AX,DX,CX,R8 capacity + if (gptotal > TUPLE_GPCAP) { // AX,DX,CX,R8 capacity // pinned loud-stop, inline like cgen.ww:604 (cstage uses // fatal(), err.c) — surface, don't corrupt. let msg: str = "tuple destructure exceeds integer register-return ABI capacity (4 eightbytes: AX,DX,CX,R8); see return-ABI #10\n"; diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index 7be2c28b..f70ed513 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -491,7 +491,7 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = { }; // The producing call already satisfied #164's return caps; // guard anyway (tupstore indexes [AX,DX,CX,R8] / [X0,X1]). - if (gptot > 4 || sstot > 2) { + if (gptot > TUPLE_GPCAP || sstot > TUPLE_SSECAP) { let msg: str = "tuple arg exceeds return-cursor ABI capacity; see #163/#164\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); @@ -1256,6 +1256,34 @@ export fn sretretsize(c: *cgen, t: *node) i32 = { r = r.lhs; if (r == nil) { return 0; }; }; + 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 = + // 1 SSE eightbyte, a slice/str its 3-word header, a scalar 1 + // GP word. Return the tuple's natural total size (tinfo.size, + // the type table) so the callee returns via sret. Mirrors + // cstage cg_sret_retsize TY_TUPLE arm; TUPLE_GPCAP/TUPLE_SSECAP + // are the shared cap SSoT with the cgreturn SEND emitter. + let ssecap: i32 = TUPLE_SSECAP; + let gptotal: i32 = 0; + let ssecount: i32 = 0; + let pt: *node = r.list; + for (pt != nil) { + let et: *node = pt.lhs; + if (isfloattype(c, et)) { + ssecount = ssecount + 1; + } else { + let wide: bool = isstrtype(c, et) || isslicetype(c, et); + gptotal = gptotal + tupebytes(wide); + }; + pt = pt.next; + }; + if (gptotal > TUPLE_GPCAP || ssecount > ssecap) { + let rti: *tinfo = r.type_: *tinfo; + if (rti != nil) { return rti.size: i32; }; + }; + return 0; + }; if (r.kind != nkind.N_TNAME) { return 0; }; // Primitives / aliased-to-primitives are never sret. if (primsize(r.str) > 0) { return 0; }; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 3a7f11c1..69308b63 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -15493,7 +15493,7 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node) i32 = { }; // The producing call already satisfied #164's return caps; // guard anyway (tupstore indexes [AX,DX,CX,R8] / [X0,X1]). - if (gptot > 4 || sstot > 2) { + if (gptot > TUPLE_GPCAP || sstot > TUPLE_SSECAP) { let msg: str = "tuple arg exceeds return-cursor ABI capacity; see #163/#164\n"; os.write(2, msg.ptr, msg.len: u64); os.exit(1); @@ -16258,6 +16258,34 @@ export fn sretretsize(c: *cgen, t: *node) i32 = { r = r.lhs; if (r == nil) { return 0; }; }; + 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 = + // 1 SSE eightbyte, a slice/str its 3-word header, a scalar 1 + // GP word. Return the tuple's natural total size (tinfo.size, + // the type table) so the callee returns via sret. Mirrors + // cstage cg_sret_retsize TY_TUPLE arm; TUPLE_GPCAP/TUPLE_SSECAP + // are the shared cap SSoT with the cgreturn SEND emitter. + let ssecap: i32 = TUPLE_SSECAP; + let gptotal: i32 = 0; + let ssecount: i32 = 0; + let pt: *node = r.list; + for (pt != nil) { + let et: *node = pt.lhs; + if (isfloattype(c, et)) { + ssecount = ssecount + 1; + } else { + let wide: bool = isstrtype(c, et) || isslicetype(c, et); + gptotal = gptotal + tupebytes(wide); + }; + pt = pt.next; + }; + if (gptotal > TUPLE_GPCAP || ssecount > ssecap) { + let rti: *tinfo = r.type_: *tinfo; + if (rti != nil) { return rti.size: i32; }; + }; + return 0; + }; if (r.kind != nkind.N_TNAME) { return 0; }; // Primitives / aliased-to-primitives are never sret. if (primsize(r.str) > 0) { return 0; }; @@ -26200,7 +26228,7 @@ fn cgreturn(c: *cgen, n: *node) void = { // loud-stop at their cap (rule-7): INTEGER 4, SSE 2. The SAME // class split drives the receive sites. if (rhs.kind == nkind.N_TUPLE) { - let ssecap: i32 = 2; // X0,X1 per SysV + let ssecap: i32 = TUPLE_SSECAP; // X0,X1 per SysV let gptotal: i32 = 0; let ssecount: i32 = 0; let e: *node = rhs.list; @@ -26213,17 +26241,78 @@ fn cgreturn(c: *cgen, n: *node) void = { }; e = e.next; }; - if (gptotal > 4) { // AX,DX,CX,R8 capacity - // pinned loud-stop, inline like cgen.ww:604 (cstage - // uses fatal(), err.c) — surface, don't corrupt. - let msg: str = "tuple return exceeds integer register-return ABI capacity (4 eightbytes: AX,DX,CX,R8); see return-ABI #10\n"; - os.write(2, msg.ptr, msg.len: u64); - os.exit(1); - }; - if (ssecount > ssecap) { - let msg: str = "tuple return exceeds SSE register-return ABI capacity (2 eightbytes: X0,X1); see return-ABI #10\n"; - os.write(2, msg.ptr, msg.len: u64); - os.exit(1); + if (gptotal > TUPLE_GPCAP || ssecount > ssecap) { + // #10 Fold A: over-cap tuple returns via sret. The + // prologue wired @sretarg (sretretsize agrees on the + // caps — TUPLE_GPCAP/TUPLE_SSECAP, the shared SSoT), + // holding the caller-prealloc dest. Store each element + // through *(@sretarg) + // at its packed layout offset (running sum of element + // sizes from the return-type tuple node — the t.0/t.1 + // positional layout), each at its natural width so a + // narrow tail doesn't over-MOVQ (#169); the dest base is + // reloaded into DX each step since a wide element's + // cgexpr clobbers AX/BX/CX. Then reuse the struct-sret + // epilogue. The CALL/receive side stays loud-stopped + // (#10 Fold B). Byte-identical to cstage cgen.c + // N_RETURN over-cap tuple arm. + let saoff: i32 = localfind(c, "@sretarg"); + let pt: *node = nil; + if (c.fnret != nil) { pt = c.fnret.list; }; + let we: *node = rhs.list; + let foff: i32 = 0; + for (we != nil) { + let isflt: bool = isfloattype(c, we); + let wide: bool = nodeisstr(c, we) || nodeisslice(c, we); + let esz: i32 = 8; + if (pt != nil) { + let eti: *tinfo = pt.lhs.type_: *tinfo; + if (eti != nil) { esz = eti.size: i32; }; + }; + cgexpr(c, we); + emitline("\tMOVQ\t"); + emitoff(saoff: i64); + emitline("(BP), DX\n"); + if (isflt) { + let mov: str = "MOVSD"; + if (isf32type(c, we)) { mov = "MOVSS"; }; + emitline("\t"); + emitline(mov); + emitline("\tX0, "); + emitdispreg(foff: i64, "DX"); + emitline("\n"); + } else { + if (wide) { + emitline("\tMOVQ\tAX, "); + emitdispreg(foff: i64, "DX"); + emitline("\n"); + emitline("\tMOVQ\tBX, "); + emitdispreg((foff + 8): i64, "DX"); + emitline("\n"); + emitline("\tMOVQ\tCX, "); + emitdispreg((foff + 16): i64, "DX"); + emitline("\n"); + } else { + let sop: str = tnodestoreop(c, we, esz); + emitline("\t"); + emitline(sop); + emitline("\tAX, "); + emitdispreg(foff: i64, "DX"); + emitline("\n"); + }; + }; + foff += esz; + we = we.next; + if (pt != nil) { pt = pt.next; }; + }; + emitline("\tMOVQ\t"); + emitoff(saoff: i64); + emitline("(BP), AX\n"); + emitline("\tMOVQ\tBP, SP\n"); + emitline("\tPOPQ\tBP\n"); + emitline("\tRET\n"); + c.lastwasreturn = 1; + return; }; let fscr: i32 = 0; if (ssecount > 0) { @@ -27581,7 +27670,7 @@ fn cgmassign(c: *cgen, n: *node) void = { if (n.rhs != nil) { cgexpr(c, n.rhs); }; - let ssecap: i32 = 2; // X0,X1 per SysV + let ssecap: i32 = TUPLE_SSECAP; // X0,X1 per SysV let gptotal: i32 = 0; let ssetotal: i32 = 0; let l: *node = n.list; @@ -27599,7 +27688,7 @@ fn cgmassign(c: *cgen, n: *node) void = { l = l.next; if (pt != nil) { pt = pt.next; }; }; - if (gptotal > 4) { // AX,DX,CX,R8 capacity + if (gptotal > TUPLE_GPCAP) { // AX,DX,CX,R8 capacity // pinned loud-stop, inline like cgen.ww:604 (cstage uses // fatal(), err.c) — surface, don't corrupt. let msg: str = "tuple destructure exceeds integer register-return ABI capacity (4 eightbytes: AX,DX,CX,R8); see return-ABI #10\n"; @@ -27668,7 +27757,7 @@ fn cgmlet(c: *cgen, n: *node) void = { cgexpr(c, rhs); - let ssecap: i32 = 2; // X0,X1 per SysV + let ssecap: i32 = TUPLE_SSECAP; // X0,X1 per SysV let gptotal: i32 = 0; let ssetotal: i32 = 0; let l: *node = n.list; @@ -27688,7 +27777,7 @@ fn cgmlet(c: *cgen, n: *node) void = { l = l.next; if (pt != nil) { pt = pt.next; }; }; - if (gptotal > 4) { // AX,DX,CX,R8 capacity + if (gptotal > TUPLE_GPCAP) { // AX,DX,CX,R8 capacity // pinned loud-stop, inline like cgen.ww:604 (cstage uses // fatal(), err.c) — surface, don't corrupt. let msg: str = "tuple destructure exceeds integer register-return ABI capacity (4 eightbytes: AX,DX,CX,R8); see return-ABI #10\n"; @@ -29110,6 +29199,15 @@ type enumtype = struct { def LOOP_MAX: i32 = 16; def DEFER_MAX: i32 = 16; +// The SysV register-return-ABI caps — the SINGLE SSoT shared by the sret +// classifier (sretretsize over-cap-tuple arm) AND every emit/receive site +// (cgreturn tuple SEND, cgmlet/cgmassign destructure, cgcall arg guard). +// Classify and emit MUST agree on these, else a tuple gets classified +// sret by one and in-reg by the other -> corruption. Mirrors cstage +// cgen.c TUPLE_GPCAP/TUPLE_SSECAP (#10). +def TUPLE_GPCAP: i32 = 4; // AX,DX,CX,R8 +def TUPLE_SSECAP: i32 = 2; // X0,X1 + type cgen = struct { locals: *local, // atlocals — persistent registry of `@`-prefix scratch slots diff --git a/test/wcc/798_tuple_sret_callee.c b/test/wcc/798_tuple_sret_callee.c new file mode 100644 index 00000000..4cf3bfdc --- /dev/null +++ b/test/wcc/798_tuple_sret_callee.c @@ -0,0 +1,153 @@ +/* + * 798_tuple_sret_callee — project #10 Fold A (wide tuple-return / sret, + * CALLEE side). An over-capacity tuple return (> 4 integer eightbytes or + * > 2 SSE eightbytes) used to LOUD-STOP at the N_RETURN SEND site + * ("tuple return exceeds ... register-return ABI capacity"). Fold A makes + * the CALLEE emit such a return via the existing sret skeleton: the + * prologue wires @sretarg (the caller-prealloc dest in RDI), and the + * return stores each element through *(@sretarg) at its packed layout + * offset, then returns @sretarg in RAX. + * + * THIS TEST IS COMPILE + BYTE-ID ONLY (no runtime row): + * (a) w6c (cstage) AND w6c_ww (wwstage) must now COMPILE the over-cap + * tuple-returning fn — no loud-stop. The pre-Fold-A behavior was a + * hard error; a green compile here proves the SEND emits sret. + * (b) the two .s outputs must be BYTE-IDENTICAL (rule-10). The classifier + * (cg_sret_retsize / sretretsize) and the SEND emitter share a single + * cap SSoT, so both stages classify + lay out the tuple the same way. + * + * WHY NO RUNTIME ROW: the CALL/receive side stays deliberately loud-stopped + * — an N_MLET/N_MASSIGN destructure of an over-cap tuple still aborts + * ("tuple destructure exceeds ... capacity"), so the over-cap callee is not + * yet usefully callable. The end-to-end round-trip (allocate dest, call, + * read the elements back) arrives with Fold B (#10-B), which wires the + * receive. So this fn is compiled but never called/destructured here. + * + * ROWS exercise the layout arithmetic the SEND must get right: + * - ([]u8, []u8) 6 GP eightbytes, all-wide, offsets 0 / 24. + * - (str, str) 6 GP, str IS []u8 (24B header), offsets 0 / 24. + * - (str, str, i32) 7 GP; the trailing narrow i32 must store MOVL at + * its NATURAL packed offset 48 (#169), not over-MOVQ. + * - (f64, f64, f64) 3 SSE eightbytes > the 2-wide SSE cap. + * + * GATE POLARITY: must stay GREEN. A non-zero compile means the SEND + * loud-stop regressed (Fold A reverted); a byte-id FAIL means cstage and + * wwstage diverged on the sret classification or the element layout + * (rule-10 violation / cap-SSoT drift). + */ +#include +#include +#include + +static int +runwait(const char *cmd) +{ + int rc = system(cmd); + if (rc == -1) return -1; + return rc; +} + +struct row { const char *label; const char *src; }; + +static const struct row rows[] = { + { "two_slices", + "package main;\n" + "export fn f(a: []u8, b: []u8) ([]u8, []u8) = { return (a, b); };\n" }, + { "two_str", + "package main;\n" + "export fn f(a: str, b: str) (str, str) = { return (a, b); };\n" }, + { "str_str_i32", + "package main;\n" + "export fn f(a: str, b: str, n: i32) (str, str, i32) = {\n" + " return (a, b, n);\n" + "};\n" }, + { "three_f64", + "package main;\n" + "export fn f(x: f64, y: f64, z: f64) (f64, f64, f64) = {\n" + " return (x, y, z);\n" + "};\n" }, + { NULL, NULL } +}; + +static int +slurp_eq(const char *a, const char *b) +{ + FILE *fa = fopen(a, "rb"); + FILE *fb = fopen(b, "rb"); + if (!fa || !fb) { if (fa) fclose(fa); if (fb) fclose(fb); return -1; } + int rc = 0; + for (;;) { + int ca = fgetc(fa); + int cb = fgetc(fb); + if (ca != cb) { rc = -1; break; } + if (ca == EOF) break; + } + fclose(fa); fclose(fb); + return rc; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[1024]; + if (bin[0] != '/') { + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + + char w6c[1100], w6c_ww[1100]; + snprintf(w6c, sizeof w6c, "%s/w6c", bin); + snprintf(w6c_ww, sizeof w6c_ww, "%s/w6c_ww", bin); + if (access(w6c_ww, X_OK) != 0) { + fprintf(stderr, "tuple_sret_callee: w6c_ww missing — cannot run " + "the cs==ww byte-id gate\n"); + return 1; + } + + int n = 0, fail = 0; + for (int i = 0; rows[i].src; i++, n++) { + char src[64]; + snprintf(src, sizeof src, "/tmp/wwtsc_%d_%d.ww", getpid(), i); + FILE *f = fopen(src, "wb"); + if (f == NULL) { fail++; continue; } + fputs(rows[i].src, f); + fclose(f); + + char cs_s[64], ws_s[64], cmd[2048]; + snprintf(cs_s, sizeof cs_s, "/tmp/wwtsc_%d_%d_cs.s", getpid(), i); + snprintf(ws_s, sizeof ws_s, "/tmp/wwtsc_%d_%d_ww.s", getpid(), i); + + /* (a) cstage must COMPILE the over-cap tuple callee (no loud-stop). */ + snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", w6c, cs_s, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: w6c loud-stopped (Fold A regressed)\n", + rows[i].label); + fail++; unlink(src); continue; + } + /* (a') wwstage must compile it too. */ + snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", w6c_ww, ws_s, src); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: w6c_ww loud-stopped (Fold A regressed)\n", + rows[i].label); + fail++; unlink(src); unlink(cs_s); continue; + } + /* (b) cs==ww byte-id gate. */ + if (slurp_eq(cs_s, ws_s) != 0) { + fprintf(stderr, "row[%s]: cstage/wwstage .s DIFFER " + "(rule-10 byte-id violation)\n", rows[i].label); + fail++; + } + unlink(src); unlink(cs_s); unlink(ws_s); + } + + if (fail) { + fprintf(stderr, "%d/%d tuple-sret-callee tests failed\n", fail, n); + return 1; + } + printf("tuple_sret_callee: %d/%d ok (compile + cs==ww byte-id)\n", n, n); + return 0; +}