w6c+wwstage: emit over-cap tuple return via sret callee-side (#10 Fold A)

A tuple return whose SysV register-return footprint exceeds the caps
(> 4 integer eightbytes or > 2 SSE eightbytes) previously LOUD-STOPPED
at the N_RETURN SEND. Fold A makes the CALLEE emit such a return through
the existing >24B-struct sret skeleton:

  - classifier (cg_sret_retsize / sretretsize) grows a TY_TUPLE arm:
    walk the element footprint over the SAME caps the SEND uses, and
    return the tuple's natural total size (type table) when over-cap,
    else 0. The gp/sse caps are factored to a single shared SSoT
    (TUPLE_GPCAP / TUPLE_SSECAP — cgen.c macros in cstage, cgen.ww defs
    in wwstage) consumed by the classifier AND every emit/receive site
    (the SEND, the destructure guards, the cgcall arg guard) — so
    classify and emit can't disagree in either stage.
  - the SEND replaces the loud-stop with a write-through: cgexpr each
    element, store it through *(@sretarg) at its packed layout offset
    (the t.0/t.1 positional layout), each at its natural width so a
    narrow tail stores MOVL/MOVB not an over-MOVQ (#169); the dest base
    reloads into DX each step since a wide element clobbers AX/BX/CX.
    Then the existing struct-sret epilogue (MOVQ @sretarg->AX; ret).
  - the prologue already wires @sretarg when the classifier is nonzero.

The CALL/receive side is deliberately untouched: the N_MLET/N_MASSIGN
destructure loud-stops stay, so an over-cap tuple return is not yet
usefully callable. The end-to-end round-trip arrives with Fold B (#10-B).

Symmetric cstage (cmd/w6c/cgen.c) + wwstage (cgen.ww / cgenstmt.ww /
cgenutil.ww); combined.ww amalgams regenerated. Test 798 asserts the
callee now COMPILES (no loud-stop) and w6c vs w6c_ww .s byte-identical
across all-wide, str, narrow-tail, and float-over-cap shapes; no runtime
row (uncallable until Fold B). All 236 pass incl. 990-997 byte-id.
This commit is contained in:
2026-06-01 11:18:44 +09:00
parent fb62aa38f1
commit 19e6b68d03
8 changed files with 605 additions and 84 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -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 (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; };
};
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);
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

View File

@@ -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

View File

@@ -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 (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; };
};
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);
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";

View File

@@ -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; };

View File

@@ -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 (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; };
};
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);
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

View File

@@ -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 <stdio.h>
#include <stdlib.h>
#include <unistd.h>
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;
}