diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 8452556a..7999e94e 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -39,6 +39,13 @@ static int *cg_frame; * semantics for synthetic scratches). 0 means "not yet allocated"; * negative offsets returned by local_alloc are the live value. */ static int cg_retscr; +/* Per-fn @tupfscr offset (single-slot SSoT). A multi-float tuple return + * (#164/#107) spills each float out of X0 to this scratch as the L→R + * element walk clobbers X0, then reloads X0/X1 by SSE index after the + * integer POPQ dance. Sized to the SSE register cap (X0,X1). Mirrors the + * @retscr single-slot convention + wwstage's `@tupfscr` '@'-prefix dedup; + * 0 means "not yet allocated". */ +static int cg_tupfscr; /* Per-fn @-prefix scratch SSoT (task #26, follow-up to #15-cstage's * @retscr). Pre-#26 each site allocated a labelseq-stamped fresh slot * per call (mklabel "tagbase" / "tagscr" / "argscr" / "idxscr"); the @@ -192,6 +199,17 @@ node_isslice(Node *n) * create_unpack_bindings element walk (ref/harec/src/check.c:1354-1416). */ static const int tuple_rseq[] = { D_AX, D_DX, D_CX, D_R8 }; +/* #164 (#107): SysV dual register-class return. A tuple (and, per #171, + * a struct) return places each element by SysV class — a float rides the + * SSE row [X0,X1], everything else the INTEGER row [AX,DX,CX,R8] + * (tuple_rseq) — with the two rows advancing on INDEPENDENT counters, so + * a float lands in the next XMM regardless of its positional slot + * (ref/qbe/amd64/sysv.c retr L95-108, retreg={{RAX,RDX},{XMM0,XMM1}}). + * ww extends the INTEGER row to 4 eightbytes; the SSE row keeps SysV's 2. + * tuple_store is the shared per-element receive lowering so the struct- + * return convergence (#171) is a call-site swap, not a redesign. */ +static const int tuple_sse_seq[] = { D_X0, D_X1 }; + static int tuple_ebytes(int wide) { @@ -1242,6 +1260,36 @@ ins1(Cg *c, int op, Adr to) emit(c, p); } +/* tuple_store — store one received tuple element at BP-relative `off` + * from its SysV-class register. A slice/str rides its 3-word + * {ptr,len,cap} header from the INTEGER cursor tuple_rseq[gp..]; a float + * rides tuple_sse_seq[sse] via MOVSD/MOVSS (#105 single-float widened to + * the SSE cursor for #164/#107 multi-float); a scalar rides one INTEGER + * word from tuple_rseq[gp]. The caller owns the dual cursor (validated + + * advanced); this just emits the store. Shared by N_LET/N_MLET/N_MASSIGN + * and, per #171, struct unpack — mirrors wwstage cgenstmt.ww tupstore. */ +static void +tuple_store(Cg *c, Type *t, int wide, int gp, int sse, int off) +{ + int f32 = 0; + + if (wide) { + ins2(c, A_MOVQ, areg(tuple_rseq[gp + 0]), + amem(D_BP, off + 0)); /* .ptr */ + ins2(c, A_MOVQ, areg(tuple_rseq[gp + 1]), + amem(D_BP, off + 8)); /* .len */ + ins2(c, A_MOVQ, areg(tuple_rseq[gp + 2]), + amem(D_BP, off + 16)); /* .cap */ + return; + } + if (fld_isfloat(t, &f32)) { + ins2(c, f32 ? A_MOVSS : A_MOVSD, areg(tuple_sse_seq[sse]), + amem(D_BP, off)); + return; + } + ins2(c, A_MOVQ, areg(tuple_rseq[gp]), amem(D_BP, off)); +} + static void ins0(Cg *c, int op) { @@ -7326,68 +7374,38 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, off + 16)); break; } - /* 2-tuple initialiser from a function call. An integer word - * rides its tuple_rseq[] reg (AX, DX); a single f64/f32 word - * rides X0, the SSE return reg — the RETURN leaves the float - * in X0 and pushes garbage through that word's integer slot, - * so a blanket MOVQ-from-integer spill stores garbage and the - * #103-FACE-Z field read (MOVSD-from-slot) reads it (#105). - * Spill each word from its real class. Multi-float tuples - * collide on X0 at the RETURN (#107), out of scope here. */ - if (n->rhs && lu && lu->kind == TY_TUPLE && sz == 16) { - Tparam *p0 = lu->params; - Tparam *p1 = p0 ? p0->next : NULL; - int f0_f32 = 0, f1_f32 = 0; - int e0_f = p0 && fld_isfloat(p0->type, &f0_f32); - int e1_f = p1 && fld_isfloat(p1->type, &f1_f32); + /* Tuple initialiser from a function call (#105 / #164/#107), + * 16B (two eightbytes) or 32B (scalar/float + slice/str header). + * Each element rides its SysV class: a float its SSE cursor reg + * (X0,X1 = tuple_sse_seq), an integer/ptr word its INTEGER cursor + * reg (tuple_rseq), a slice/str its 3-word {ptr,len,cap} header + * over consecutive INTEGER cursor regs — INDEPENDENT counters, + * so the RETURN leaves floats in X0/X1 and integer words in + * AX/DX/CX/R8. A blanket MOVQ spill would store garbage where a + * float rode and the #103-FACE-Z field read (MOVSD-from-slot) + * would see it. tuple_store routes each element from its real + * class into its positional slot (eoff steps by the element's + * slot size: a slice/str takes its 24B header); the same split + * drives the destructure / reassign sites. */ + if (n->rhs && lu && lu->kind == TY_TUPLE + && (sz == 16 || sz == 32)) { cgexpr(c, n->rhs, *locals); - if (e0_f) - ins2(c, f0_f32 ? A_MOVSS : A_MOVSD, - areg(D_X0), amem(D_BP, off + 0)); - else - ins2(c, A_MOVQ, areg(tuple_rseq[0]), - amem(D_BP, off + 0)); - if (e1_f) - ins2(c, f1_f32 ? A_MOVSS : A_MOVSD, - areg(D_X0), amem(D_BP, off + 8)); - else - ins2(c, A_MOVQ, areg(tuple_rseq[1]), - amem(D_BP, off + 8)); - break; - } - /* 32B tuple initialiser for `(scalar, str)` / `(str, scalar)`. - * Per the AX:DX:CX:R8 return convention: AX = scalar elem, - * DX = str.ptr, CX = str.len, R8 = str.cap. The slot is laid - * out positionally (str takes 24B at its position), so we route - * each register to the slot dictated by the element's type, not - * by AX/DX position. str IS []u8 (24B) → 32B tuple (#1/Phase 3, - * task #5). */ - if (n->rhs && lu && lu->kind == TY_TUPLE && sz == 32) { - Tparam *p0 = lu->params; - Tparam *p1 = p0 ? p0->next : NULL; - Type *t0 = p0 ? p0->type : NULL; - Type *t1 = p1 ? p1->type : NULL; - Type *u0 = (t0 && t0->kind == TY_NAMED) ? t0->under : t0; - Type *u1 = (t1 && t1->kind == TY_NAMED) ? t1->under : t1; - int e0_str = u0 && u0->kind == TY_STR; - int e1_str = u1 && u1->kind == TY_STR; - if (e0_str ^ e1_str) { - cgexpr(c, n->rhs, *locals); - if (e0_str) { - /* layout: str@+0 (24B), scalar@+24. */ - ins2(c, A_MOVQ, areg(D_DX), amem(D_BP, off + 0)); - ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, off + 8)); - ins2(c, A_MOVQ, areg(D_R8), amem(D_BP, off + 16)); - ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 24)); - } else { - /* layout: scalar@+0 (8B), str@+8 (24B). */ - ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, off + 0)); - ins2(c, A_MOVQ, areg(D_DX), amem(D_BP, off + 8)); - ins2(c, A_MOVQ, areg(D_CX), amem(D_BP, off + 16)); - ins2(c, A_MOVQ, areg(D_R8), amem(D_BP, off + 24)); - } - break; + int gpcur = 0, ssecur = 0, eoff = 0, ef32; + for (Tparam *p = lu->params; p; p = p->next) { + Type *pu = (p->type && p->type->kind == TY_NAMED) + ? p->type->under : p->type; + int wide = pu && (pu->kind == TY_SLICE + || pu->kind == TY_STR); + int isflt = fld_isfloat(p->type, &ef32); + tuple_store(c, p->type, wide, gpcur, ssecur, + off + eoff); + if (isflt) + ssecur++; + else + gpcur += tuple_ebytes(wide); + eoff += wide ? (int)pu->size : 8; } + break; } /* Tagged-union initialiser. Delegates to cg_widen_tagged_store, * which handles nullable fold, tagged→tagged (with tag remap @@ -8050,37 +8068,79 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) break; } if (n->lhs && n->lhs->kind == N_TUPLE) { - /* #83: positional per-element register-return. Walk the - * tuple's elements (harec create_unpack_bindings, - * ref/harec/src/check.c:1354-1416); each rides consecutive - * eightbytes over tuple_rseq[]. A slice/str rides its 3-word - * {ptr,len,cap} header (ref/hare/rt/ensure.ha:4-8), cgexpr - * leaving it in (AX,BX,CX); a scalar rides 1 word in AX. - * Spill each element's word(s) L→R, then pop into the - * cursor's registers in reverse so positional slot i lands in - * tuple_rseq[i] — (scalar,str) keeps the historical AX + - * DX,CX,R8 layout, and the SAME cursor drives the receive - * sites. Over-capacity is a loud stop (return-ABI #10), never - * a silent drop. */ - int cap = (int)(sizeof tuple_rseq / sizeof tuple_rseq[0]); - int total = 0; - for (Node *e = n->lhs->list; e; e = e->next) - total += tuple_ebytes(node_isstr(e) || node_isslice(e)); - if (total > cap) - fatal("tuple return exceeds register-return ABI " - "capacity (%d eightbytes); see return-ABI #10", - cap); + /* #83 / #164 (#107): positional register-return over a SysV + * dual class cursor. Each element rides its SysV class + * (harec create_unpack_bindings, ref/harec/src/check.c:1354- + * 1416): a float takes one SSE eightbyte (X0,X1 = tuple_sse_ + * seq), everything else INTEGER eightbytes over tuple_rseq — + * a slice/str its 3-word {ptr,len,cap} header (ref/hare/rt/ + * ensure.ha:4-8) cgexpr leaves in (AX,BX,CX), a scalar 1 word + * in AX. Integer words spill L→R to the stack and pop into the + * INTEGER cursor in reverse so positional slot i lands in + * tuple_rseq[i] (byte-id with #83 when no float is present). + * Each float must spill X0 to @tupfscr as we walk, because a + * later element's cgexpr clobbers X0; after the integer pops + * the saved floats reload into X0/X1 by SSE index — INDEPENDENT + * of the integer cursor (ref/qbe/amd64/sysv.c retr L95-108). + * 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 gptotal = 0, ssecount = 0, f32; for (Node *e = n->lhs->list; e; e = e->next) { - int wide = node_isstr(e) || node_isslice(e); - cgexpr(c, e, *locals); /* scalar=AX; slice/str=AX,BX,CX */ + if (fld_isfloat(e->type, &f32)) + ssecount++; + else + 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); + int fscr = 0; + if (ssecount > 0) { + if (cg_tupfscr != 0) + fscr = cg_tupfscr; + else { + fscr = local_alloc(c, locals, "@tupfscr", + ssecap * 8, cg_frame); + cg_tupfscr = fscr; + } + } + int sseidx = 0; + for (Node *e = n->lhs->list; e; e = e->next) { + int isflt = fld_isfloat(e->type, &f32); + cgexpr(c, e, *locals); /* scalar=AX; slice/str=AX,BX,CX; float=X0 */ + if (isflt) { + ins2(c, f32 ? A_MOVSS : A_MOVSD, areg(D_X0), + amem(D_BP, fscr + sseidx * 8)); + sseidx++; + continue; + } ins1(c, A_PUSHQ, areg(D_AX)); /* scalar / .ptr */ - if (wide) { + if (node_isstr(e) || node_isslice(e)) { ins1(c, A_PUSHQ, areg(D_BX)); /* .len */ ins1(c, A_PUSHQ, areg(D_CX)); /* .cap */ } } - for (int i = total - 1; i >= 0; i--) + for (int i = gptotal - 1; i >= 0; i--) ins1(c, A_POPQ, areg(tuple_rseq[i])); + int j = 0; + for (Node *e = n->lhs->list; e; e = e->next) { + if (!fld_isfloat(e->type, &f32)) + continue; + ins2(c, f32 ? A_MOVSS : A_MOVSD, + amem(D_BP, fscr + j * 8), + areg(tuple_sse_seq[j])); + j++; + } } else if (n->lhs) { cgexpr(c, n->lhs, *locals); } else { @@ -8263,55 +8323,51 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) break; } case N_MLET: { - /* #83: positional per-element destructure store. The rhs left - * each tuple element in the register-return cursor (see N_RETURN - * / harec create_unpack_bindings, ref/harec/src/check.c:1354-1416); - * walk the bindings over the SAME cursor and store each at its - * own width — a slice/str's 3-word {ptr,len,cap} header - * (ref/hare/rt/ensure.ha:4-8) into a header-sized slot (sized - * from u->size so #1 propagates), a scalar's 1 word into an 8B - * slot. Over-capacity is a loud stop, not a silent drop. */ + /* #83 / #164 (#107): positional per-element destructure store. + * The rhs left each tuple element in its SysV-class register + * (see N_RETURN / harec create_unpack_bindings, ref/harec/src/ + * check.c:1354-1416); walk the bindings over the SAME dual + * cursor and store each at its own width — a slice/str's 3-word + * {ptr,len,cap} header (ref/hare/rt/ensure.ha:4-8) into a + * header-sized slot (sized from u->size so #1 propagates), a + * float from X0/X1 (SSE cursor), a scalar's 1 word from the + * INTEGER cursor into an 8B slot. Both rows loud-stop at their + * cap. */ cgexpr(c, n->rhs, *locals); - int cap = (int)(sizeof tuple_rseq / sizeof tuple_rseq[0]); - int total = 0; + int gpcap = (int)(sizeof tuple_rseq / sizeof tuple_rseq[0]); + int ssecap = (int)(sizeof tuple_sse_seq + / sizeof tuple_sse_seq[0]); + int gptotal = 0, ssetotal = 0, lf32; for (Node *l = n->list; l; l = l->next) { Type *t = l->type; Type *u = (t && t->kind == TY_NAMED) ? t->under : t; - int wide = u && (u->kind == TY_SLICE || u->kind == TY_STR); - total += tuple_ebytes(wide); + if (fld_isfloat(t, &lf32)) + ssetotal++; + else + gptotal += tuple_ebytes(u && (u->kind == TY_SLICE + || u->kind == TY_STR)); } - if (total > cap) - fatal("tuple destructure exceeds register-return ABI " - "capacity (%d eightbytes); see return-ABI #10", cap); - int cur = 0; + if (gptotal > gpcap) + fatal("tuple destructure exceeds integer register-return " + "ABI capacity (%d eightbytes: AX,DX,CX,R8); " + "see return-ABI #10", gpcap); + if (ssetotal > ssecap) + fatal("tuple destructure exceeds SSE register-return ABI " + "capacity (%d eightbytes: X0,X1); see return-ABI #10", + ssecap); + int gpcur = 0, ssecur = 0; for (Node *l = n->list; l; l = l->next) { Type *t = l->type; Type *u = (t && t->kind == TY_NAMED) ? t->under : t; int wide = u && (u->kind == TY_SLICE || u->kind == TY_STR); + int isflt = fld_isfloat(t, &lf32); int sz = wide ? (int)u->size : 8; int off = localoff(c, locals, l->str, sz, frame); - if (wide) { - ins2(c, A_MOVQ, areg(tuple_rseq[cur + 0]), - amem(D_BP, off + 0)); /* .ptr */ - ins2(c, A_MOVQ, areg(tuple_rseq[cur + 1]), - amem(D_BP, off + 8)); /* .len */ - ins2(c, A_MOVQ, areg(tuple_rseq[cur + 2]), - amem(D_BP, off + 16)); /* .cap */ - } else { - /* #105: an f64/f32 element rides X0, not its - * integer cursor reg — MOVSD/MOVSS it, else - * the slot gets garbage and the FACE-Z field - * read sees it. X0 survives the reg->mem - * stores. Single-float scope; #107 is multi. */ - int e_f32 = 0; - if (fld_isfloat(t, &e_f32)) - ins2(c, e_f32 ? A_MOVSS : A_MOVSD, - areg(D_X0), amem(D_BP, off)); - else - ins2(c, A_MOVQ, areg(tuple_rseq[cur]), - amem(D_BP, off)); - } - cur += tuple_ebytes(wide); + tuple_store(c, t, wide, gpcur, ssecur, off); + if (isflt) + ssecur++; + else + gpcur += tuple_ebytes(wide); } break; } @@ -8337,47 +8393,44 @@ 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 cap = (int)(sizeof tuple_rseq / sizeof tuple_rseq[0]); - int total = 0; + int gpcap = (int)(sizeof tuple_rseq / sizeof tuple_rseq[0]); + int ssecap = (int)(sizeof tuple_sse_seq + / sizeof tuple_sse_seq[0]); + int gptotal = 0, ssetotal = 0, mf32; for (Tparam *tp = tp0; tp; tp = tp->next) { Type *u = (tp->type && tp->type->kind == TY_NAMED) ? tp->type->under : tp->type; - total += tuple_ebytes(u && (u->kind == TY_SLICE - || u->kind == TY_STR)); + if (fld_isfloat(tp->type, &mf32)) + ssetotal++; + else + gptotal += tuple_ebytes(u && (u->kind == TY_SLICE + || u->kind == TY_STR)); } - if (total > cap) - fatal("tuple destructure exceeds register-return ABI " - "capacity (%d eightbytes); see return-ABI #10", cap); - int cur = 0; + if (gptotal > gpcap) + fatal("tuple destructure exceeds integer register-return " + "ABI capacity (%d eightbytes: AX,DX,CX,R8); " + "see return-ABI #10", gpcap); + if (ssetotal > ssecap) + fatal("tuple destructure exceeds SSE register-return ABI " + "capacity (%d eightbytes: X0,X1); see return-ABI #10", + ssecap); + int gpcur = 0, ssecur = 0; Tparam *tp = tp0; for (Node *l = n->list; l; l = l->next) { Type *et = tp ? tp->type : NULL; Type *u = (et && et->kind == TY_NAMED) ? et->under : et; int wide = u && (u->kind == TY_SLICE || u->kind == TY_STR); + int isflt = fld_isfloat(et, &mf32); int off = (l->kind == N_IDENT) ? localfind(*locals, l->str) : 0; - if (off != 0) { - if (wide) { - ins2(c, A_MOVQ, areg(tuple_rseq[cur + 0]), - amem(D_BP, off + 0)); /* .ptr */ - ins2(c, A_MOVQ, areg(tuple_rseq[cur + 1]), - amem(D_BP, off + 8)); /* .len */ - ins2(c, A_MOVQ, areg(tuple_rseq[cur + 2]), - amem(D_BP, off + 16)); /* .cap */ - } else { - /* #105: f64/f32 element rides X0 (SSE), - * not its integer cursor reg — see N_MLET. */ - int e_f32 = 0; - if (fld_isfloat(et, &e_f32)) - ins2(c, e_f32 ? A_MOVSS : A_MOVSD, - areg(D_X0), amem(D_BP, off)); - else - ins2(c, A_MOVQ, - areg(tuple_rseq[cur]), - amem(D_BP, off)); - } - } - cur += tuple_ebytes(wide); + /* harec `_` (off==0): skip the store but CONSUME the + * cursor slot so the next element stays aligned. */ + if (off != 0) + tuple_store(c, et, wide, gpcur, ssecur, off); + if (isflt) + ssecur++; + else + gpcur += tuple_ebytes(wide); if (tp) tp = tp->next; } break; @@ -8459,6 +8512,7 @@ cgfn(Cg *c, FILE *out, Node *fn) nloops = 0; cg_ret_type = fn->type ? fn->type->ret : NULL; cg_retscr = 0; + cg_tupfscr = 0; cg_tagbase = 0; cg_tagbase_sz = 0; cg_tagscr = 0; diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index f86f3d77..f3c274fc 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -24189,6 +24189,17 @@ fn tupreg(i: i32) str = { return "R8"; }; +// #164 (#107): SSE half of the SysV dual register-class return. A float +// element rides the SSE row [X0,X1] on a counter INDEPENDENT of the +// INTEGER row tupreg — a float lands in the next XMM regardless of its +// positional slot (ref/qbe/amd64/sysv.c retr L95-108, retreg={{RAX,RDX}, +// {XMM0,XMM1}}). SysV caps SSE returns at 2 eightbytes. Mirror of cstage +// tuple_sse_seq (cmd/w6c/cgen.c). +fn tupsse(i: i32) str = { + if (i == 0) { return "X0"; }; + return "X1"; +}; + fn tupebytes(wide: bool) i32 = { if (wide) { return (tyslicesize() / 8i64): i32; }; return 1; @@ -24229,31 +24240,34 @@ fn rettupleof(c: *cgen, rhs: *node) *node = { // tupstore — store the tuple element at register-cursor `cur` into the // BP-relative slot at `off`. A slice/str stores its 3-word {ptr,len,cap} // header (ref/hare/rt/ensure.ha:4-8) at off/+8/+16 from consecutive -// cursor registers; a scalar stores 1 word. Byte-identical to the cstage -// N_MLET/N_MASSIGN store (cmd/w6c/cgen.c). -fn tupstore(c: *cgen, cur: i32, off: i32, wide: bool, tn: *node) void = { +// INTEGER cursor registers; a float rides the SSE cursor (X0,X1); a +// scalar stores 1 INTEGER word. The caller owns the dual cursor +// (validated + advanced). Byte-identical to the cstage tuple_store +// (cmd/w6c/cgen.c). +fn tupstore(c: *cgen, gpcur: i32, ssecur: i32, off: i32, wide: bool, tn: *node) void = { if (wide) { emitline("\tMOVQ\t"); - emitline(tupreg(cur + 0)); + emitline(tupreg(gpcur + 0)); emitline(", "); emitoff(off: i64); emitline("(BP)\n"); emitline("\tMOVQ\t"); - emitline(tupreg(cur + 1)); + emitline(tupreg(gpcur + 1)); emitline(", "); emitoff((off + 8): i64); emitline("(BP)\n"); emitline("\tMOVQ\t"); - emitline(tupreg(cur + 2)); + emitline(tupreg(gpcur + 2)); emitline(", "); emitoff((off + 16): i64); emitline("(BP)\n"); return; }; - // #105: an f64/f32 element rides X0 (the SSE return reg), not its - // integer cursor reg — MOVSD/MOVSS it, else the slot gets garbage and - // the FACE-Z field read sees it. X0 survives the reg->mem stores. - // Single-float scope; multi-float collides on X0 at RETURN (#107). + // #105 / #164 (#107): an f64/f32 element rides the SSE cursor reg + // (X0,X1 = tupsse), not its INTEGER cursor reg — MOVSD/MOVSS it, else + // the slot gets garbage and the FACE-Z field read sees it. The SSE + // regs survive the reg->mem stores. SSE-idx0=X0 keeps the #105 + // single-float byte-id; idx1=X1 is the #107 multi-float extension. if (isfloattype(c, tn)) { // #121 (Package B) RESIDUAL sibling-evidence guard, pin form. // In destructure mode tn IS the tuple-element-type-AST node @@ -24286,13 +24300,15 @@ fn tupstore(c: *cgen, cur: i32, off: i32, wide: bool, tn: *node) void = { if (isf32type(c, tn)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); - emitline("\tX0, "); + emitline("\t"); + emitline(tupsse(ssecur)); + emitline(", "); emitoff(off: i64); emitline("(BP)\n"); return; }; emitline("\tMOVQ\t"); - emitline(tupreg(cur)); + emitline(tupreg(gpcur)); emitline(", "); emitoff(off: i64); emitline("(BP)\n"); @@ -24302,49 +24318,98 @@ fn cgreturn(c: *cgen, n: *node) void = { rundefers(c); let rhs: *node = n.lhs; if (rhs != nil) { - // #83: positional per-element register-return (harec - // create_unpack_bindings, ref/harec/src/check.c:1354-1416). Each - // element rides consecutive eightbytes over [AX,DX,CX,R8] - // (tupreg); a slice/str rides its 3-word {ptr,len,cap} header - // (ref/hare/rt/ensure.ha:4-8), cgexpr leaving it in (AX,BX,CX); a - // scalar rides 1 word in AX. Spill each element L->R, then pop - // into the cursor's registers in reverse so positional slot i - // lands in tupreg(i) — (scalar,str) keeps the historical AX + - // DX,CX,R8. The SAME cursor drives the receive sites. Over- - // capacity is a loud stop (return-ABI #10), never a silent drop. + // #83 / #164 (#107): positional register-return over a SysV + // dual class cursor (harec create_unpack_bindings, ref/harec/src/ + // check.c:1354-1416). A float takes one SSE eightbyte (X0,X1 = + // tupsse), everything else INTEGER eightbytes over [AX,DX,CX,R8] + // (tupreg) — a slice/str its 3-word {ptr,len,cap} header + // (ref/hare/rt/ensure.ha:4-8) cgexpr leaves in (AX,BX,CX), a + // scalar 1 word in AX. Integer words spill L->R to the stack and + // pop into the INTEGER cursor in reverse so positional slot i + // lands in tupreg(i) (byte-id with #83 when no float is present). + // Each float must spill X0 to @tupfscr as we walk, since a later + // element's cgexpr clobbers X0; after the integer pops the saved + // floats reload into X0/X1 by SSE index — INDEPENDENT of the + // INTEGER cursor (ref/qbe/amd64/sysv.c retr L95-108). Both rows + // 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 total: i32 = 0; + let ssecap: i32 = 2; // X0,X1 per SysV + let gptotal: i32 = 0; + let ssecount: i32 = 0; let e: *node = rhs.list; for (e != nil) { - let wide: bool = nodeisstr(c, e) || nodeisslice(c, e); - total = total + tupebytes(wide); - e = e.next; - }; - if (total > 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 register-return ABI capacity (4 eightbytes); see return-ABI #10\n"; - os.write(2, msg.ptr, msg.len: u64); - os.exit(1); - }; - e = rhs.list; - for (e != nil) { - let wide: bool = nodeisstr(c, e) || nodeisslice(c, e); - cgexpr(c, e); - emitline("\tPUSHQ\tAX\n"); // scalar / .ptr - if (wide) { - emitline("\tPUSHQ\tBX\n"); // .len - emitline("\tPUSHQ\tCX\n"); // .cap + if (isfloattype(c, e)) { + ssecount = ssecount + 1; + } else { + let wide: bool = nodeisstr(c, e) || nodeisslice(c, e); + gptotal = gptotal + tupebytes(wide); }; e = e.next; }; - let i: i32 = total - 1; + 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); + }; + let fscr: i32 = 0; + if (ssecount > 0) { + fscr = localadd(c, "@tupfscr", ssecap * 8, nil); + }; + let sseidx: i32 = 0; + e = rhs.list; + for (e != nil) { + let isflt: bool = isfloattype(c, e); + cgexpr(c, e); + if (isflt) { + let mov: str = "MOVSD"; + if (isf32type(c, e)) { mov = "MOVSS"; }; + emitline("\t"); + emitline(mov); + emitline("\tX0, "); + emitoff((fscr + sseidx * 8): i64); + emitline("(BP)\n"); + sseidx = sseidx + 1; + } else { + emitline("\tPUSHQ\tAX\n"); // scalar / .ptr + if (nodeisstr(c, e) || nodeisslice(c, e)) { + emitline("\tPUSHQ\tBX\n"); // .len + emitline("\tPUSHQ\tCX\n"); // .cap + }; + }; + e = e.next; + }; + let i: i32 = gptotal - 1; for (i >= 0) { emitline("\tPOPQ\t"); emitline(tupreg(i)); emitline("\n"); i = i - 1; }; + let j: i32 = 0; + e = rhs.list; + for (e != nil) { + if (isfloattype(c, e)) { + let mov: str = "MOVSD"; + if (isf32type(c, e)) { mov = "MOVSS"; }; + emitline("\t"); + emitline(mov); + emitline("\t"); + emitoff((fscr + j * 8): i64); + emitline("(BP), "); + emitline(tupsse(j)); + emitline("\n"); + j = j + 1; + }; + e = e.next; + }; emitline("\tMOVQ\tBP, SP\n"); emitline("\tPOPQ\tBP\n"); emitline("\tRET\n"); @@ -24943,12 +25008,15 @@ fn cglet(c: *cgen, n: *node) void = { return; }; // 32B tuple init for `let t: (scalar, str) = call()` / - // `let t: (str, scalar) = call()`. Per the AX:DX:CX:R8 return - // convention: AX = scalar elem, DX = str.ptr, CX = str.len, - // R8 = str.cap. Layout is positional (str takes 24B at its - // position), so we route each register to the slot dictated by - // element type, not by AX/DX position. str IS []u8 (24B) → 32B - // tuple (#1/Phase 3, task #5). + // `let t: (str, scalar) = call()` (#105 / #164/#107). Each + // element rides its SysV class — a float its SSE cursor reg + // (X0,X1 = tupsse), an integer/ptr word its INTEGER cursor reg + // (tupreg), a slice/str its 3-word {ptr,len,cap} header over + // consecutive INTEGER cursor regs — on INDEPENDENT counters. + // tupstore routes each element from its real class into its + // positional slot (eoff steps by the element's slot size: a + // slice/str takes its 24B header). str IS []u8 (24B) → 32B tuple + // (#1/Phase 3, task #5). Mirror of the cstage unified branch. if (n.lhs != nil) { if (n.lhs.kind == nkind.N_TTUPLE) { let p0: *node = n.lhs.list; @@ -24958,38 +25026,36 @@ fn cglet(c: *cgen, n: *node) void = { let p1t: *node = nil; if (p0 != nil) { p0t = p0.lhs; }; if (p1 != nil) { p1t = p1.lhs; }; - let s0_is_str: bool = isstrtype(c, p0t); - let s1_is_str: bool = isstrtype(c, p1t); + let s0_is_str: bool = isstrtype(c, p0t) + || isslicetype(c, p0t); + let s1_is_str: bool = isstrtype(c, p1t) + || isslicetype(c, p1t); if (p0 != nil) { if (p1 != nil) { if (s0_is_str != s1_is_str) { cgexpr(c, rhs); - if (s0_is_str) { - emitline("\tMOVQ\tDX, "); - emitoff(off: i64); - emitline("(BP)\n"); - emitline("\tMOVQ\tCX, "); - emitoff((off + 8): i64); - emitline("(BP)\n"); - emitline("\tMOVQ\tR8, "); - emitoff((off + 16): i64); - emitline("(BP)\n"); - emitline("\tMOVQ\tAX, "); - emitoff((off + 24): i64); - emitline("(BP)\n"); - } else { - emitline("\tMOVQ\tAX, "); - emitoff(off: i64); - emitline("(BP)\n"); - emitline("\tMOVQ\tDX, "); - emitoff((off + 8): i64); - emitline("(BP)\n"); - emitline("\tMOVQ\tCX, "); - emitoff((off + 16): i64); - emitline("(BP)\n"); - emitline("\tMOVQ\tR8, "); - emitoff((off + 24): i64); - emitline("(BP)\n"); + let gpcur: i32 = 0; + let ssecur: i32 = 0; + let eoff: i32 = 0; + let q: *node = n.lhs.list; + for (q != nil) { + let qt: *node = q.lhs; + let isflt: bool = isfloattype(c, qt); + let wide: bool = isstrtype(c, qt) + || isslicetype(c, qt); + tupstore(c, gpcur, ssecur, + off + eoff, wide, qt); + if (isflt) { + ssecur = ssecur + 1; + } else { + gpcur = gpcur + tupebytes(wide); + }; + if (wide) { + eoff = eoff + (tyslicesize(): i32); + } else { + eoff = eoff + 8; + }; + q = q.next; }; c.lastwasreturn = 0; return; @@ -24998,54 +25064,35 @@ fn cglet(c: *cgen, n: *node) void = { }; }; }; - // 16B tuple init from a function call. An integer word rides - // its integer cursor reg (AX, DX); a single f64/f32 word rides - // X0, the SSE return reg — the RETURN leaves the float in X0 - // and pushes garbage through that word's integer slot, so a - // blanket MOVQ-from-integer spill stores garbage and the #103- - // FACE-Z field read (MOVSD-from-slot) reads it (#105). Spill - // each word from its real class. Multi-float tuples collide on - // X0 at the RETURN (#107), out of scope. Mirror of cstage - // cgen.c. Without this branch a 16B tuple receive (any element - // mix) fell to the generic single-word store below and dropped - // word1 — silent loss of t.1 (#102). + // 16B tuple init from a function call (#105 / #164/#107). Each + // eightbyte rides its SysV class: a float its SSE cursor reg + // (X0,X1 = tupsse), an integer/ptr word its INTEGER cursor reg + // (AX,DX = tupreg), INDEPENDENT counters — the RETURN leaves + // floats in X0/X1 and integer words in AX/DX, so a blanket MOVQ + // spill would store garbage where a float rode and the #103- + // FACE-Z field read (MOVSD-from-slot) would see it. tupstore + // routes each word from its real class; the same split drives + // the destructure / reassign sites. Without this branch a 16B + // tuple receive fell to the generic single-word store below and + // dropped word1 — silent loss of t.1 (#102). let rt16: *node = rettupleof(c, rhs); if (rt16 != nil && sz == 16) { - let q0: *node = rt16.list; - let q1: *node = nil; - if (q0 != nil) { q1 = q0.next; }; - let q0t: *node = nil; - let q1t: *node = nil; - if (q0 != nil) { q0t = q0.lhs; }; - if (q1 != nil) { q1t = q1.lhs; }; - let e0f: bool = isfloattype(c, q0t); - let e1f: bool = isfloattype(c, q1t); cgexpr(c, rhs); - if (e0f) { - let mov: str = "MOVSD"; - if (isf32type(c, q0t)) { mov = "MOVSS"; }; - emitline("\t"); - emitline(mov); - emitline("\tX0, "); - emitoff(off: i64); - emitline("(BP)\n"); - } else { - emitline("\tMOVQ\tAX, "); - emitoff(off: i64); - emitline("(BP)\n"); - }; - if (e1f) { - let mov: str = "MOVSD"; - if (isf32type(c, q1t)) { mov = "MOVSS"; }; - emitline("\t"); - emitline(mov); - emitline("\tX0, "); - emitoff((off + 8): i64); - emitline("(BP)\n"); - } else { - emitline("\tMOVQ\tDX, "); - emitoff((off + 8): i64); - emitline("(BP)\n"); + let gpcur: i32 = 0; + let ssecur: i32 = 0; + let eoff: i32 = 0; + let q: *node = rt16.list; + for (q != nil) { + let qt: *node = q.lhs; + let isflt: bool = isfloattype(c, qt); + tupstore(c, gpcur, ssecur, off + eoff, false, qt); + if (isflt) { + ssecur = ssecur + 1; + } else { + gpcur = gpcur + 1; + }; + eoff = eoff + 8; + q = q.next; }; c.lastwasreturn = 0; return; @@ -25517,40 +25564,59 @@ fn cgmassign(c: *cgen, n: *node) void = { if (n.rhs != nil) { cgexpr(c, n.rhs); }; - let total: i32 = 0; + let ssecap: i32 = 2; // X0,X1 per SysV + let gptotal: i32 = 0; + let ssetotal: i32 = 0; let l: *node = n.list; let pt: *node = nil; if (rettuple != nil) { pt = rettuple.list; }; for (l != nil) { let tn: *node = nil; if (pt != nil) { tn = pt.lhs; }; - let wide: bool = isstrtype(c, tn) || isslicetype(c, tn); - total = total + tupebytes(wide); + if (isfloattype(c, tn)) { + ssetotal = ssetotal + 1; + } else { + let wide: bool = isstrtype(c, tn) || isslicetype(c, tn); + gptotal = gptotal + tupebytes(wide); + }; l = l.next; if (pt != nil) { pt = pt.next; }; }; - if (total > 4) { // AX,DX,CX,R8 capacity + 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 destructure exceeds register-return ABI capacity (4 eightbytes); see return-ABI #10\n"; + let msg: str = "tuple destructure 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 (ssetotal > ssecap) { + let msg: str = "tuple destructure 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); }; - let cur: i32 = 0; + let gpcur: i32 = 0; + let ssecur: i32 = 0; l = n.list; pt = nil; if (rettuple != nil) { pt = rettuple.list; }; for (l != nil) { let tn: *node = nil; if (pt != nil) { tn = pt.lhs; }; + let isflt: bool = isfloattype(c, tn); let wide: bool = isstrtype(c, tn) || isslicetype(c, tn); let off: i32 = 0; if (l.kind == nkind.N_IDENT) { off = localfind(c, l.str); }; + // harec `_` (off==0): skip the store but CONSUME the cursor + // slot so the next element stays aligned. if (off != 0) { - tupstore(c, cur, off, wide, tn); + tupstore(c, gpcur, ssecur, off, wide, tn); + }; + if (isflt) { + ssecur = ssecur + 1; + } else { + gpcur = gpcur + tupebytes(wide); }; - cur = cur + tupebytes(wide); l = l.next; if (pt != nil) { pt = pt.next; }; }; @@ -25585,7 +25651,9 @@ fn cgmlet(c: *cgen, n: *node) void = { cgexpr(c, rhs); - let total: i32 = 0; + let ssecap: i32 = 2; // X0,X1 per SysV + let gptotal: i32 = 0; + let ssetotal: i32 = 0; let l: *node = n.list; let pt: *node = nil; if (rettuple != nil) { pt = rettuple.list; }; @@ -25594,20 +25662,30 @@ fn cgmlet(c: *cgen, n: *node) void = { if (tn == nil) { if (pt != nil) { tn = pt.lhs; }; }; - let wide: bool = isstrtype(c, tn) || isslicetype(c, tn); - total = total + tupebytes(wide); + if (isfloattype(c, tn)) { + ssetotal = ssetotal + 1; + } else { + let wide: bool = isstrtype(c, tn) || isslicetype(c, tn); + gptotal = gptotal + tupebytes(wide); + }; l = l.next; if (pt != nil) { pt = pt.next; }; }; - if (total > 4) { // AX,DX,CX,R8 capacity + 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 destructure exceeds register-return ABI capacity (4 eightbytes); see return-ABI #10\n"; + let msg: str = "tuple destructure 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 (ssetotal > ssecap) { + let msg: str = "tuple destructure 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); }; - let cur: i32 = 0; + let gpcur: i32 = 0; + let ssecur: i32 = 0; l = n.list; pt = nil; if (rettuple != nil) { pt = rettuple.list; }; @@ -25616,12 +25694,17 @@ fn cgmlet(c: *cgen, n: *node) void = { if (tn == nil) { if (pt != nil) { tn = pt.lhs; }; }; + let isflt: bool = isfloattype(c, tn); let wide: bool = isstrtype(c, tn) || isslicetype(c, tn); let sz: i32 = 8; if (wide) { sz = tyslicesize(): i32; }; let off: i32 = localadd(c, l.str, sz, tn); - tupstore(c, cur, off, wide, tn); - cur = cur + tupebytes(wide); + tupstore(c, gpcur, ssecur, off, wide, tn); + if (isflt) { + ssecur = ssecur + 1; + } else { + gpcur = gpcur + tupebytes(wide); + }; l = l.next; if (pt != nil) { pt = pt.next; }; }; diff --git a/selfhost/cmd/wcc/cgenstmt.ww b/selfhost/cmd/wcc/cgenstmt.ww index 80ce9173..7845f4c2 100644 --- a/selfhost/cmd/wcc/cgenstmt.ww +++ b/selfhost/cmd/wcc/cgenstmt.ww @@ -121,6 +121,17 @@ fn tupreg(i: i32) str = { return "R8"; }; +// #164 (#107): SSE half of the SysV dual register-class return. A float +// element rides the SSE row [X0,X1] on a counter INDEPENDENT of the +// INTEGER row tupreg — a float lands in the next XMM regardless of its +// positional slot (ref/qbe/amd64/sysv.c retr L95-108, retreg={{RAX,RDX}, +// {XMM0,XMM1}}). SysV caps SSE returns at 2 eightbytes. Mirror of cstage +// tuple_sse_seq (cmd/w6c/cgen.c). +fn tupsse(i: i32) str = { + if (i == 0) { return "X0"; }; + return "X1"; +}; + fn tupebytes(wide: bool) i32 = { if (wide) { return (tyslicesize() / 8i64): i32; }; return 1; @@ -161,31 +172,34 @@ fn rettupleof(c: *cgen, rhs: *node) *node = { // tupstore — store the tuple element at register-cursor `cur` into the // BP-relative slot at `off`. A slice/str stores its 3-word {ptr,len,cap} // header (ref/hare/rt/ensure.ha:4-8) at off/+8/+16 from consecutive -// cursor registers; a scalar stores 1 word. Byte-identical to the cstage -// N_MLET/N_MASSIGN store (cmd/w6c/cgen.c). -fn tupstore(c: *cgen, cur: i32, off: i32, wide: bool, tn: *node) void = { +// INTEGER cursor registers; a float rides the SSE cursor (X0,X1); a +// scalar stores 1 INTEGER word. The caller owns the dual cursor +// (validated + advanced). Byte-identical to the cstage tuple_store +// (cmd/w6c/cgen.c). +fn tupstore(c: *cgen, gpcur: i32, ssecur: i32, off: i32, wide: bool, tn: *node) void = { if (wide) { emitline("\tMOVQ\t"); - emitline(tupreg(cur + 0)); + emitline(tupreg(gpcur + 0)); emitline(", "); emitoff(off: i64); emitline("(BP)\n"); emitline("\tMOVQ\t"); - emitline(tupreg(cur + 1)); + emitline(tupreg(gpcur + 1)); emitline(", "); emitoff((off + 8): i64); emitline("(BP)\n"); emitline("\tMOVQ\t"); - emitline(tupreg(cur + 2)); + emitline(tupreg(gpcur + 2)); emitline(", "); emitoff((off + 16): i64); emitline("(BP)\n"); return; }; - // #105: an f64/f32 element rides X0 (the SSE return reg), not its - // integer cursor reg — MOVSD/MOVSS it, else the slot gets garbage and - // the FACE-Z field read sees it. X0 survives the reg->mem stores. - // Single-float scope; multi-float collides on X0 at RETURN (#107). + // #105 / #164 (#107): an f64/f32 element rides the SSE cursor reg + // (X0,X1 = tupsse), not its INTEGER cursor reg — MOVSD/MOVSS it, else + // the slot gets garbage and the FACE-Z field read sees it. The SSE + // regs survive the reg->mem stores. SSE-idx0=X0 keeps the #105 + // single-float byte-id; idx1=X1 is the #107 multi-float extension. if (isfloattype(c, tn)) { // #121 (Package B) RESIDUAL sibling-evidence guard, pin form. // In destructure mode tn IS the tuple-element-type-AST node @@ -218,13 +232,15 @@ fn tupstore(c: *cgen, cur: i32, off: i32, wide: bool, tn: *node) void = { if (isf32type(c, tn)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); - emitline("\tX0, "); + emitline("\t"); + emitline(tupsse(ssecur)); + emitline(", "); emitoff(off: i64); emitline("(BP)\n"); return; }; emitline("\tMOVQ\t"); - emitline(tupreg(cur)); + emitline(tupreg(gpcur)); emitline(", "); emitoff(off: i64); emitline("(BP)\n"); @@ -234,49 +250,98 @@ fn cgreturn(c: *cgen, n: *node) void = { rundefers(c); let rhs: *node = n.lhs; if (rhs != nil) { - // #83: positional per-element register-return (harec - // create_unpack_bindings, ref/harec/src/check.c:1354-1416). Each - // element rides consecutive eightbytes over [AX,DX,CX,R8] - // (tupreg); a slice/str rides its 3-word {ptr,len,cap} header - // (ref/hare/rt/ensure.ha:4-8), cgexpr leaving it in (AX,BX,CX); a - // scalar rides 1 word in AX. Spill each element L->R, then pop - // into the cursor's registers in reverse so positional slot i - // lands in tupreg(i) — (scalar,str) keeps the historical AX + - // DX,CX,R8. The SAME cursor drives the receive sites. Over- - // capacity is a loud stop (return-ABI #10), never a silent drop. + // #83 / #164 (#107): positional register-return over a SysV + // dual class cursor (harec create_unpack_bindings, ref/harec/src/ + // check.c:1354-1416). A float takes one SSE eightbyte (X0,X1 = + // tupsse), everything else INTEGER eightbytes over [AX,DX,CX,R8] + // (tupreg) — a slice/str its 3-word {ptr,len,cap} header + // (ref/hare/rt/ensure.ha:4-8) cgexpr leaves in (AX,BX,CX), a + // scalar 1 word in AX. Integer words spill L->R to the stack and + // pop into the INTEGER cursor in reverse so positional slot i + // lands in tupreg(i) (byte-id with #83 when no float is present). + // Each float must spill X0 to @tupfscr as we walk, since a later + // element's cgexpr clobbers X0; after the integer pops the saved + // floats reload into X0/X1 by SSE index — INDEPENDENT of the + // INTEGER cursor (ref/qbe/amd64/sysv.c retr L95-108). Both rows + // 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 total: i32 = 0; + let ssecap: i32 = 2; // X0,X1 per SysV + let gptotal: i32 = 0; + let ssecount: i32 = 0; let e: *node = rhs.list; for (e != nil) { - let wide: bool = nodeisstr(c, e) || nodeisslice(c, e); - total = total + tupebytes(wide); - e = e.next; - }; - if (total > 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 register-return ABI capacity (4 eightbytes); see return-ABI #10\n"; - os.write(2, msg.ptr, msg.len: u64); - os.exit(1); - }; - e = rhs.list; - for (e != nil) { - let wide: bool = nodeisstr(c, e) || nodeisslice(c, e); - cgexpr(c, e); - emitline("\tPUSHQ\tAX\n"); // scalar / .ptr - if (wide) { - emitline("\tPUSHQ\tBX\n"); // .len - emitline("\tPUSHQ\tCX\n"); // .cap + if (isfloattype(c, e)) { + ssecount = ssecount + 1; + } else { + let wide: bool = nodeisstr(c, e) || nodeisslice(c, e); + gptotal = gptotal + tupebytes(wide); }; e = e.next; }; - let i: i32 = total - 1; + 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); + }; + let fscr: i32 = 0; + if (ssecount > 0) { + fscr = localadd(c, "@tupfscr", ssecap * 8, nil); + }; + let sseidx: i32 = 0; + e = rhs.list; + for (e != nil) { + let isflt: bool = isfloattype(c, e); + cgexpr(c, e); + if (isflt) { + let mov: str = "MOVSD"; + if (isf32type(c, e)) { mov = "MOVSS"; }; + emitline("\t"); + emitline(mov); + emitline("\tX0, "); + emitoff((fscr + sseidx * 8): i64); + emitline("(BP)\n"); + sseidx = sseidx + 1; + } else { + emitline("\tPUSHQ\tAX\n"); // scalar / .ptr + if (nodeisstr(c, e) || nodeisslice(c, e)) { + emitline("\tPUSHQ\tBX\n"); // .len + emitline("\tPUSHQ\tCX\n"); // .cap + }; + }; + e = e.next; + }; + let i: i32 = gptotal - 1; for (i >= 0) { emitline("\tPOPQ\t"); emitline(tupreg(i)); emitline("\n"); i = i - 1; }; + let j: i32 = 0; + e = rhs.list; + for (e != nil) { + if (isfloattype(c, e)) { + let mov: str = "MOVSD"; + if (isf32type(c, e)) { mov = "MOVSS"; }; + emitline("\t"); + emitline(mov); + emitline("\t"); + emitoff((fscr + j * 8): i64); + emitline("(BP), "); + emitline(tupsse(j)); + emitline("\n"); + j = j + 1; + }; + e = e.next; + }; emitline("\tMOVQ\tBP, SP\n"); emitline("\tPOPQ\tBP\n"); emitline("\tRET\n"); @@ -875,12 +940,15 @@ fn cglet(c: *cgen, n: *node) void = { return; }; // 32B tuple init for `let t: (scalar, str) = call()` / - // `let t: (str, scalar) = call()`. Per the AX:DX:CX:R8 return - // convention: AX = scalar elem, DX = str.ptr, CX = str.len, - // R8 = str.cap. Layout is positional (str takes 24B at its - // position), so we route each register to the slot dictated by - // element type, not by AX/DX position. str IS []u8 (24B) → 32B - // tuple (#1/Phase 3, task #5). + // `let t: (str, scalar) = call()` (#105 / #164/#107). Each + // element rides its SysV class — a float its SSE cursor reg + // (X0,X1 = tupsse), an integer/ptr word its INTEGER cursor reg + // (tupreg), a slice/str its 3-word {ptr,len,cap} header over + // consecutive INTEGER cursor regs — on INDEPENDENT counters. + // tupstore routes each element from its real class into its + // positional slot (eoff steps by the element's slot size: a + // slice/str takes its 24B header). str IS []u8 (24B) → 32B tuple + // (#1/Phase 3, task #5). Mirror of the cstage unified branch. if (n.lhs != nil) { if (n.lhs.kind == nkind.N_TTUPLE) { let p0: *node = n.lhs.list; @@ -890,38 +958,36 @@ fn cglet(c: *cgen, n: *node) void = { let p1t: *node = nil; if (p0 != nil) { p0t = p0.lhs; }; if (p1 != nil) { p1t = p1.lhs; }; - let s0_is_str: bool = isstrtype(c, p0t); - let s1_is_str: bool = isstrtype(c, p1t); + let s0_is_str: bool = isstrtype(c, p0t) + || isslicetype(c, p0t); + let s1_is_str: bool = isstrtype(c, p1t) + || isslicetype(c, p1t); if (p0 != nil) { if (p1 != nil) { if (s0_is_str != s1_is_str) { cgexpr(c, rhs); - if (s0_is_str) { - emitline("\tMOVQ\tDX, "); - emitoff(off: i64); - emitline("(BP)\n"); - emitline("\tMOVQ\tCX, "); - emitoff((off + 8): i64); - emitline("(BP)\n"); - emitline("\tMOVQ\tR8, "); - emitoff((off + 16): i64); - emitline("(BP)\n"); - emitline("\tMOVQ\tAX, "); - emitoff((off + 24): i64); - emitline("(BP)\n"); - } else { - emitline("\tMOVQ\tAX, "); - emitoff(off: i64); - emitline("(BP)\n"); - emitline("\tMOVQ\tDX, "); - emitoff((off + 8): i64); - emitline("(BP)\n"); - emitline("\tMOVQ\tCX, "); - emitoff((off + 16): i64); - emitline("(BP)\n"); - emitline("\tMOVQ\tR8, "); - emitoff((off + 24): i64); - emitline("(BP)\n"); + let gpcur: i32 = 0; + let ssecur: i32 = 0; + let eoff: i32 = 0; + let q: *node = n.lhs.list; + for (q != nil) { + let qt: *node = q.lhs; + let isflt: bool = isfloattype(c, qt); + let wide: bool = isstrtype(c, qt) + || isslicetype(c, qt); + tupstore(c, gpcur, ssecur, + off + eoff, wide, qt); + if (isflt) { + ssecur = ssecur + 1; + } else { + gpcur = gpcur + tupebytes(wide); + }; + if (wide) { + eoff = eoff + (tyslicesize(): i32); + } else { + eoff = eoff + 8; + }; + q = q.next; }; c.lastwasreturn = 0; return; @@ -930,54 +996,35 @@ fn cglet(c: *cgen, n: *node) void = { }; }; }; - // 16B tuple init from a function call. An integer word rides - // its integer cursor reg (AX, DX); a single f64/f32 word rides - // X0, the SSE return reg — the RETURN leaves the float in X0 - // and pushes garbage through that word's integer slot, so a - // blanket MOVQ-from-integer spill stores garbage and the #103- - // FACE-Z field read (MOVSD-from-slot) reads it (#105). Spill - // each word from its real class. Multi-float tuples collide on - // X0 at the RETURN (#107), out of scope. Mirror of cstage - // cgen.c. Without this branch a 16B tuple receive (any element - // mix) fell to the generic single-word store below and dropped - // word1 — silent loss of t.1 (#102). + // 16B tuple init from a function call (#105 / #164/#107). Each + // eightbyte rides its SysV class: a float its SSE cursor reg + // (X0,X1 = tupsse), an integer/ptr word its INTEGER cursor reg + // (AX,DX = tupreg), INDEPENDENT counters — the RETURN leaves + // floats in X0/X1 and integer words in AX/DX, so a blanket MOVQ + // spill would store garbage where a float rode and the #103- + // FACE-Z field read (MOVSD-from-slot) would see it. tupstore + // routes each word from its real class; the same split drives + // the destructure / reassign sites. Without this branch a 16B + // tuple receive fell to the generic single-word store below and + // dropped word1 — silent loss of t.1 (#102). let rt16: *node = rettupleof(c, rhs); if (rt16 != nil && sz == 16) { - let q0: *node = rt16.list; - let q1: *node = nil; - if (q0 != nil) { q1 = q0.next; }; - let q0t: *node = nil; - let q1t: *node = nil; - if (q0 != nil) { q0t = q0.lhs; }; - if (q1 != nil) { q1t = q1.lhs; }; - let e0f: bool = isfloattype(c, q0t); - let e1f: bool = isfloattype(c, q1t); cgexpr(c, rhs); - if (e0f) { - let mov: str = "MOVSD"; - if (isf32type(c, q0t)) { mov = "MOVSS"; }; - emitline("\t"); - emitline(mov); - emitline("\tX0, "); - emitoff(off: i64); - emitline("(BP)\n"); - } else { - emitline("\tMOVQ\tAX, "); - emitoff(off: i64); - emitline("(BP)\n"); - }; - if (e1f) { - let mov: str = "MOVSD"; - if (isf32type(c, q1t)) { mov = "MOVSS"; }; - emitline("\t"); - emitline(mov); - emitline("\tX0, "); - emitoff((off + 8): i64); - emitline("(BP)\n"); - } else { - emitline("\tMOVQ\tDX, "); - emitoff((off + 8): i64); - emitline("(BP)\n"); + let gpcur: i32 = 0; + let ssecur: i32 = 0; + let eoff: i32 = 0; + let q: *node = rt16.list; + for (q != nil) { + let qt: *node = q.lhs; + let isflt: bool = isfloattype(c, qt); + tupstore(c, gpcur, ssecur, off + eoff, false, qt); + if (isflt) { + ssecur = ssecur + 1; + } else { + gpcur = gpcur + 1; + }; + eoff = eoff + 8; + q = q.next; }; c.lastwasreturn = 0; return; @@ -1449,40 +1496,59 @@ fn cgmassign(c: *cgen, n: *node) void = { if (n.rhs != nil) { cgexpr(c, n.rhs); }; - let total: i32 = 0; + let ssecap: i32 = 2; // X0,X1 per SysV + let gptotal: i32 = 0; + let ssetotal: i32 = 0; let l: *node = n.list; let pt: *node = nil; if (rettuple != nil) { pt = rettuple.list; }; for (l != nil) { let tn: *node = nil; if (pt != nil) { tn = pt.lhs; }; - let wide: bool = isstrtype(c, tn) || isslicetype(c, tn); - total = total + tupebytes(wide); + if (isfloattype(c, tn)) { + ssetotal = ssetotal + 1; + } else { + let wide: bool = isstrtype(c, tn) || isslicetype(c, tn); + gptotal = gptotal + tupebytes(wide); + }; l = l.next; if (pt != nil) { pt = pt.next; }; }; - if (total > 4) { // AX,DX,CX,R8 capacity + 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 destructure exceeds register-return ABI capacity (4 eightbytes); see return-ABI #10\n"; + let msg: str = "tuple destructure 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 (ssetotal > ssecap) { + let msg: str = "tuple destructure 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); }; - let cur: i32 = 0; + let gpcur: i32 = 0; + let ssecur: i32 = 0; l = n.list; pt = nil; if (rettuple != nil) { pt = rettuple.list; }; for (l != nil) { let tn: *node = nil; if (pt != nil) { tn = pt.lhs; }; + let isflt: bool = isfloattype(c, tn); let wide: bool = isstrtype(c, tn) || isslicetype(c, tn); let off: i32 = 0; if (l.kind == nkind.N_IDENT) { off = localfind(c, l.str); }; + // harec `_` (off==0): skip the store but CONSUME the cursor + // slot so the next element stays aligned. if (off != 0) { - tupstore(c, cur, off, wide, tn); + tupstore(c, gpcur, ssecur, off, wide, tn); + }; + if (isflt) { + ssecur = ssecur + 1; + } else { + gpcur = gpcur + tupebytes(wide); }; - cur = cur + tupebytes(wide); l = l.next; if (pt != nil) { pt = pt.next; }; }; @@ -1517,7 +1583,9 @@ fn cgmlet(c: *cgen, n: *node) void = { cgexpr(c, rhs); - let total: i32 = 0; + let ssecap: i32 = 2; // X0,X1 per SysV + let gptotal: i32 = 0; + let ssetotal: i32 = 0; let l: *node = n.list; let pt: *node = nil; if (rettuple != nil) { pt = rettuple.list; }; @@ -1526,20 +1594,30 @@ fn cgmlet(c: *cgen, n: *node) void = { if (tn == nil) { if (pt != nil) { tn = pt.lhs; }; }; - let wide: bool = isstrtype(c, tn) || isslicetype(c, tn); - total = total + tupebytes(wide); + if (isfloattype(c, tn)) { + ssetotal = ssetotal + 1; + } else { + let wide: bool = isstrtype(c, tn) || isslicetype(c, tn); + gptotal = gptotal + tupebytes(wide); + }; l = l.next; if (pt != nil) { pt = pt.next; }; }; - if (total > 4) { // AX,DX,CX,R8 capacity + 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 destructure exceeds register-return ABI capacity (4 eightbytes); see return-ABI #10\n"; + let msg: str = "tuple destructure 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 (ssetotal > ssecap) { + let msg: str = "tuple destructure 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); }; - let cur: i32 = 0; + let gpcur: i32 = 0; + let ssecur: i32 = 0; l = n.list; pt = nil; if (rettuple != nil) { pt = rettuple.list; }; @@ -1548,12 +1626,17 @@ fn cgmlet(c: *cgen, n: *node) void = { if (tn == nil) { if (pt != nil) { tn = pt.lhs; }; }; + let isflt: bool = isfloattype(c, tn); let wide: bool = isstrtype(c, tn) || isslicetype(c, tn); let sz: i32 = 8; if (wide) { sz = tyslicesize(): i32; }; let off: i32 = localadd(c, l.str, sz, tn); - tupstore(c, cur, off, wide, tn); - cur = cur + tupebytes(wide); + tupstore(c, gpcur, ssecur, off, wide, tn); + if (isflt) { + ssecur = ssecur + 1; + } else { + gpcur = gpcur + tupebytes(wide); + }; l = l.next; if (pt != nil) { pt = pt.next; }; }; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 6f2acc51..cfe77ff2 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -24189,6 +24189,17 @@ fn tupreg(i: i32) str = { return "R8"; }; +// #164 (#107): SSE half of the SysV dual register-class return. A float +// element rides the SSE row [X0,X1] on a counter INDEPENDENT of the +// INTEGER row tupreg — a float lands in the next XMM regardless of its +// positional slot (ref/qbe/amd64/sysv.c retr L95-108, retreg={{RAX,RDX}, +// {XMM0,XMM1}}). SysV caps SSE returns at 2 eightbytes. Mirror of cstage +// tuple_sse_seq (cmd/w6c/cgen.c). +fn tupsse(i: i32) str = { + if (i == 0) { return "X0"; }; + return "X1"; +}; + fn tupebytes(wide: bool) i32 = { if (wide) { return (tyslicesize() / 8i64): i32; }; return 1; @@ -24229,31 +24240,34 @@ fn rettupleof(c: *cgen, rhs: *node) *node = { // tupstore — store the tuple element at register-cursor `cur` into the // BP-relative slot at `off`. A slice/str stores its 3-word {ptr,len,cap} // header (ref/hare/rt/ensure.ha:4-8) at off/+8/+16 from consecutive -// cursor registers; a scalar stores 1 word. Byte-identical to the cstage -// N_MLET/N_MASSIGN store (cmd/w6c/cgen.c). -fn tupstore(c: *cgen, cur: i32, off: i32, wide: bool, tn: *node) void = { +// INTEGER cursor registers; a float rides the SSE cursor (X0,X1); a +// scalar stores 1 INTEGER word. The caller owns the dual cursor +// (validated + advanced). Byte-identical to the cstage tuple_store +// (cmd/w6c/cgen.c). +fn tupstore(c: *cgen, gpcur: i32, ssecur: i32, off: i32, wide: bool, tn: *node) void = { if (wide) { emitline("\tMOVQ\t"); - emitline(tupreg(cur + 0)); + emitline(tupreg(gpcur + 0)); emitline(", "); emitoff(off: i64); emitline("(BP)\n"); emitline("\tMOVQ\t"); - emitline(tupreg(cur + 1)); + emitline(tupreg(gpcur + 1)); emitline(", "); emitoff((off + 8): i64); emitline("(BP)\n"); emitline("\tMOVQ\t"); - emitline(tupreg(cur + 2)); + emitline(tupreg(gpcur + 2)); emitline(", "); emitoff((off + 16): i64); emitline("(BP)\n"); return; }; - // #105: an f64/f32 element rides X0 (the SSE return reg), not its - // integer cursor reg — MOVSD/MOVSS it, else the slot gets garbage and - // the FACE-Z field read sees it. X0 survives the reg->mem stores. - // Single-float scope; multi-float collides on X0 at RETURN (#107). + // #105 / #164 (#107): an f64/f32 element rides the SSE cursor reg + // (X0,X1 = tupsse), not its INTEGER cursor reg — MOVSD/MOVSS it, else + // the slot gets garbage and the FACE-Z field read sees it. The SSE + // regs survive the reg->mem stores. SSE-idx0=X0 keeps the #105 + // single-float byte-id; idx1=X1 is the #107 multi-float extension. if (isfloattype(c, tn)) { // #121 (Package B) RESIDUAL sibling-evidence guard, pin form. // In destructure mode tn IS the tuple-element-type-AST node @@ -24286,13 +24300,15 @@ fn tupstore(c: *cgen, cur: i32, off: i32, wide: bool, tn: *node) void = { if (isf32type(c, tn)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); - emitline("\tX0, "); + emitline("\t"); + emitline(tupsse(ssecur)); + emitline(", "); emitoff(off: i64); emitline("(BP)\n"); return; }; emitline("\tMOVQ\t"); - emitline(tupreg(cur)); + emitline(tupreg(gpcur)); emitline(", "); emitoff(off: i64); emitline("(BP)\n"); @@ -24302,49 +24318,98 @@ fn cgreturn(c: *cgen, n: *node) void = { rundefers(c); let rhs: *node = n.lhs; if (rhs != nil) { - // #83: positional per-element register-return (harec - // create_unpack_bindings, ref/harec/src/check.c:1354-1416). Each - // element rides consecutive eightbytes over [AX,DX,CX,R8] - // (tupreg); a slice/str rides its 3-word {ptr,len,cap} header - // (ref/hare/rt/ensure.ha:4-8), cgexpr leaving it in (AX,BX,CX); a - // scalar rides 1 word in AX. Spill each element L->R, then pop - // into the cursor's registers in reverse so positional slot i - // lands in tupreg(i) — (scalar,str) keeps the historical AX + - // DX,CX,R8. The SAME cursor drives the receive sites. Over- - // capacity is a loud stop (return-ABI #10), never a silent drop. + // #83 / #164 (#107): positional register-return over a SysV + // dual class cursor (harec create_unpack_bindings, ref/harec/src/ + // check.c:1354-1416). A float takes one SSE eightbyte (X0,X1 = + // tupsse), everything else INTEGER eightbytes over [AX,DX,CX,R8] + // (tupreg) — a slice/str its 3-word {ptr,len,cap} header + // (ref/hare/rt/ensure.ha:4-8) cgexpr leaves in (AX,BX,CX), a + // scalar 1 word in AX. Integer words spill L->R to the stack and + // pop into the INTEGER cursor in reverse so positional slot i + // lands in tupreg(i) (byte-id with #83 when no float is present). + // Each float must spill X0 to @tupfscr as we walk, since a later + // element's cgexpr clobbers X0; after the integer pops the saved + // floats reload into X0/X1 by SSE index — INDEPENDENT of the + // INTEGER cursor (ref/qbe/amd64/sysv.c retr L95-108). Both rows + // 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 total: i32 = 0; + let ssecap: i32 = 2; // X0,X1 per SysV + let gptotal: i32 = 0; + let ssecount: i32 = 0; let e: *node = rhs.list; for (e != nil) { - let wide: bool = nodeisstr(c, e) || nodeisslice(c, e); - total = total + tupebytes(wide); - e = e.next; - }; - if (total > 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 register-return ABI capacity (4 eightbytes); see return-ABI #10\n"; - os.write(2, msg.ptr, msg.len: u64); - os.exit(1); - }; - e = rhs.list; - for (e != nil) { - let wide: bool = nodeisstr(c, e) || nodeisslice(c, e); - cgexpr(c, e); - emitline("\tPUSHQ\tAX\n"); // scalar / .ptr - if (wide) { - emitline("\tPUSHQ\tBX\n"); // .len - emitline("\tPUSHQ\tCX\n"); // .cap + if (isfloattype(c, e)) { + ssecount = ssecount + 1; + } else { + let wide: bool = nodeisstr(c, e) || nodeisslice(c, e); + gptotal = gptotal + tupebytes(wide); }; e = e.next; }; - let i: i32 = total - 1; + 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); + }; + let fscr: i32 = 0; + if (ssecount > 0) { + fscr = localadd(c, "@tupfscr", ssecap * 8, nil); + }; + let sseidx: i32 = 0; + e = rhs.list; + for (e != nil) { + let isflt: bool = isfloattype(c, e); + cgexpr(c, e); + if (isflt) { + let mov: str = "MOVSD"; + if (isf32type(c, e)) { mov = "MOVSS"; }; + emitline("\t"); + emitline(mov); + emitline("\tX0, "); + emitoff((fscr + sseidx * 8): i64); + emitline("(BP)\n"); + sseidx = sseidx + 1; + } else { + emitline("\tPUSHQ\tAX\n"); // scalar / .ptr + if (nodeisstr(c, e) || nodeisslice(c, e)) { + emitline("\tPUSHQ\tBX\n"); // .len + emitline("\tPUSHQ\tCX\n"); // .cap + }; + }; + e = e.next; + }; + let i: i32 = gptotal - 1; for (i >= 0) { emitline("\tPOPQ\t"); emitline(tupreg(i)); emitline("\n"); i = i - 1; }; + let j: i32 = 0; + e = rhs.list; + for (e != nil) { + if (isfloattype(c, e)) { + let mov: str = "MOVSD"; + if (isf32type(c, e)) { mov = "MOVSS"; }; + emitline("\t"); + emitline(mov); + emitline("\t"); + emitoff((fscr + j * 8): i64); + emitline("(BP), "); + emitline(tupsse(j)); + emitline("\n"); + j = j + 1; + }; + e = e.next; + }; emitline("\tMOVQ\tBP, SP\n"); emitline("\tPOPQ\tBP\n"); emitline("\tRET\n"); @@ -24943,12 +25008,15 @@ fn cglet(c: *cgen, n: *node) void = { return; }; // 32B tuple init for `let t: (scalar, str) = call()` / - // `let t: (str, scalar) = call()`. Per the AX:DX:CX:R8 return - // convention: AX = scalar elem, DX = str.ptr, CX = str.len, - // R8 = str.cap. Layout is positional (str takes 24B at its - // position), so we route each register to the slot dictated by - // element type, not by AX/DX position. str IS []u8 (24B) → 32B - // tuple (#1/Phase 3, task #5). + // `let t: (str, scalar) = call()` (#105 / #164/#107). Each + // element rides its SysV class — a float its SSE cursor reg + // (X0,X1 = tupsse), an integer/ptr word its INTEGER cursor reg + // (tupreg), a slice/str its 3-word {ptr,len,cap} header over + // consecutive INTEGER cursor regs — on INDEPENDENT counters. + // tupstore routes each element from its real class into its + // positional slot (eoff steps by the element's slot size: a + // slice/str takes its 24B header). str IS []u8 (24B) → 32B tuple + // (#1/Phase 3, task #5). Mirror of the cstage unified branch. if (n.lhs != nil) { if (n.lhs.kind == nkind.N_TTUPLE) { let p0: *node = n.lhs.list; @@ -24958,38 +25026,36 @@ fn cglet(c: *cgen, n: *node) void = { let p1t: *node = nil; if (p0 != nil) { p0t = p0.lhs; }; if (p1 != nil) { p1t = p1.lhs; }; - let s0_is_str: bool = isstrtype(c, p0t); - let s1_is_str: bool = isstrtype(c, p1t); + let s0_is_str: bool = isstrtype(c, p0t) + || isslicetype(c, p0t); + let s1_is_str: bool = isstrtype(c, p1t) + || isslicetype(c, p1t); if (p0 != nil) { if (p1 != nil) { if (s0_is_str != s1_is_str) { cgexpr(c, rhs); - if (s0_is_str) { - emitline("\tMOVQ\tDX, "); - emitoff(off: i64); - emitline("(BP)\n"); - emitline("\tMOVQ\tCX, "); - emitoff((off + 8): i64); - emitline("(BP)\n"); - emitline("\tMOVQ\tR8, "); - emitoff((off + 16): i64); - emitline("(BP)\n"); - emitline("\tMOVQ\tAX, "); - emitoff((off + 24): i64); - emitline("(BP)\n"); - } else { - emitline("\tMOVQ\tAX, "); - emitoff(off: i64); - emitline("(BP)\n"); - emitline("\tMOVQ\tDX, "); - emitoff((off + 8): i64); - emitline("(BP)\n"); - emitline("\tMOVQ\tCX, "); - emitoff((off + 16): i64); - emitline("(BP)\n"); - emitline("\tMOVQ\tR8, "); - emitoff((off + 24): i64); - emitline("(BP)\n"); + let gpcur: i32 = 0; + let ssecur: i32 = 0; + let eoff: i32 = 0; + let q: *node = n.lhs.list; + for (q != nil) { + let qt: *node = q.lhs; + let isflt: bool = isfloattype(c, qt); + let wide: bool = isstrtype(c, qt) + || isslicetype(c, qt); + tupstore(c, gpcur, ssecur, + off + eoff, wide, qt); + if (isflt) { + ssecur = ssecur + 1; + } else { + gpcur = gpcur + tupebytes(wide); + }; + if (wide) { + eoff = eoff + (tyslicesize(): i32); + } else { + eoff = eoff + 8; + }; + q = q.next; }; c.lastwasreturn = 0; return; @@ -24998,54 +25064,35 @@ fn cglet(c: *cgen, n: *node) void = { }; }; }; - // 16B tuple init from a function call. An integer word rides - // its integer cursor reg (AX, DX); a single f64/f32 word rides - // X0, the SSE return reg — the RETURN leaves the float in X0 - // and pushes garbage through that word's integer slot, so a - // blanket MOVQ-from-integer spill stores garbage and the #103- - // FACE-Z field read (MOVSD-from-slot) reads it (#105). Spill - // each word from its real class. Multi-float tuples collide on - // X0 at the RETURN (#107), out of scope. Mirror of cstage - // cgen.c. Without this branch a 16B tuple receive (any element - // mix) fell to the generic single-word store below and dropped - // word1 — silent loss of t.1 (#102). + // 16B tuple init from a function call (#105 / #164/#107). Each + // eightbyte rides its SysV class: a float its SSE cursor reg + // (X0,X1 = tupsse), an integer/ptr word its INTEGER cursor reg + // (AX,DX = tupreg), INDEPENDENT counters — the RETURN leaves + // floats in X0/X1 and integer words in AX/DX, so a blanket MOVQ + // spill would store garbage where a float rode and the #103- + // FACE-Z field read (MOVSD-from-slot) would see it. tupstore + // routes each word from its real class; the same split drives + // the destructure / reassign sites. Without this branch a 16B + // tuple receive fell to the generic single-word store below and + // dropped word1 — silent loss of t.1 (#102). let rt16: *node = rettupleof(c, rhs); if (rt16 != nil && sz == 16) { - let q0: *node = rt16.list; - let q1: *node = nil; - if (q0 != nil) { q1 = q0.next; }; - let q0t: *node = nil; - let q1t: *node = nil; - if (q0 != nil) { q0t = q0.lhs; }; - if (q1 != nil) { q1t = q1.lhs; }; - let e0f: bool = isfloattype(c, q0t); - let e1f: bool = isfloattype(c, q1t); cgexpr(c, rhs); - if (e0f) { - let mov: str = "MOVSD"; - if (isf32type(c, q0t)) { mov = "MOVSS"; }; - emitline("\t"); - emitline(mov); - emitline("\tX0, "); - emitoff(off: i64); - emitline("(BP)\n"); - } else { - emitline("\tMOVQ\tAX, "); - emitoff(off: i64); - emitline("(BP)\n"); - }; - if (e1f) { - let mov: str = "MOVSD"; - if (isf32type(c, q1t)) { mov = "MOVSS"; }; - emitline("\t"); - emitline(mov); - emitline("\tX0, "); - emitoff((off + 8): i64); - emitline("(BP)\n"); - } else { - emitline("\tMOVQ\tDX, "); - emitoff((off + 8): i64); - emitline("(BP)\n"); + let gpcur: i32 = 0; + let ssecur: i32 = 0; + let eoff: i32 = 0; + let q: *node = rt16.list; + for (q != nil) { + let qt: *node = q.lhs; + let isflt: bool = isfloattype(c, qt); + tupstore(c, gpcur, ssecur, off + eoff, false, qt); + if (isflt) { + ssecur = ssecur + 1; + } else { + gpcur = gpcur + 1; + }; + eoff = eoff + 8; + q = q.next; }; c.lastwasreturn = 0; return; @@ -25517,40 +25564,59 @@ fn cgmassign(c: *cgen, n: *node) void = { if (n.rhs != nil) { cgexpr(c, n.rhs); }; - let total: i32 = 0; + let ssecap: i32 = 2; // X0,X1 per SysV + let gptotal: i32 = 0; + let ssetotal: i32 = 0; let l: *node = n.list; let pt: *node = nil; if (rettuple != nil) { pt = rettuple.list; }; for (l != nil) { let tn: *node = nil; if (pt != nil) { tn = pt.lhs; }; - let wide: bool = isstrtype(c, tn) || isslicetype(c, tn); - total = total + tupebytes(wide); + if (isfloattype(c, tn)) { + ssetotal = ssetotal + 1; + } else { + let wide: bool = isstrtype(c, tn) || isslicetype(c, tn); + gptotal = gptotal + tupebytes(wide); + }; l = l.next; if (pt != nil) { pt = pt.next; }; }; - if (total > 4) { // AX,DX,CX,R8 capacity + 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 destructure exceeds register-return ABI capacity (4 eightbytes); see return-ABI #10\n"; + let msg: str = "tuple destructure 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 (ssetotal > ssecap) { + let msg: str = "tuple destructure 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); }; - let cur: i32 = 0; + let gpcur: i32 = 0; + let ssecur: i32 = 0; l = n.list; pt = nil; if (rettuple != nil) { pt = rettuple.list; }; for (l != nil) { let tn: *node = nil; if (pt != nil) { tn = pt.lhs; }; + let isflt: bool = isfloattype(c, tn); let wide: bool = isstrtype(c, tn) || isslicetype(c, tn); let off: i32 = 0; if (l.kind == nkind.N_IDENT) { off = localfind(c, l.str); }; + // harec `_` (off==0): skip the store but CONSUME the cursor + // slot so the next element stays aligned. if (off != 0) { - tupstore(c, cur, off, wide, tn); + tupstore(c, gpcur, ssecur, off, wide, tn); + }; + if (isflt) { + ssecur = ssecur + 1; + } else { + gpcur = gpcur + tupebytes(wide); }; - cur = cur + tupebytes(wide); l = l.next; if (pt != nil) { pt = pt.next; }; }; @@ -25585,7 +25651,9 @@ fn cgmlet(c: *cgen, n: *node) void = { cgexpr(c, rhs); - let total: i32 = 0; + let ssecap: i32 = 2; // X0,X1 per SysV + let gptotal: i32 = 0; + let ssetotal: i32 = 0; let l: *node = n.list; let pt: *node = nil; if (rettuple != nil) { pt = rettuple.list; }; @@ -25594,20 +25662,30 @@ fn cgmlet(c: *cgen, n: *node) void = { if (tn == nil) { if (pt != nil) { tn = pt.lhs; }; }; - let wide: bool = isstrtype(c, tn) || isslicetype(c, tn); - total = total + tupebytes(wide); + if (isfloattype(c, tn)) { + ssetotal = ssetotal + 1; + } else { + let wide: bool = isstrtype(c, tn) || isslicetype(c, tn); + gptotal = gptotal + tupebytes(wide); + }; l = l.next; if (pt != nil) { pt = pt.next; }; }; - if (total > 4) { // AX,DX,CX,R8 capacity + 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 destructure exceeds register-return ABI capacity (4 eightbytes); see return-ABI #10\n"; + let msg: str = "tuple destructure 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 (ssetotal > ssecap) { + let msg: str = "tuple destructure 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); }; - let cur: i32 = 0; + let gpcur: i32 = 0; + let ssecur: i32 = 0; l = n.list; pt = nil; if (rettuple != nil) { pt = rettuple.list; }; @@ -25616,12 +25694,17 @@ fn cgmlet(c: *cgen, n: *node) void = { if (tn == nil) { if (pt != nil) { tn = pt.lhs; }; }; + let isflt: bool = isfloattype(c, tn); let wide: bool = isstrtype(c, tn) || isslicetype(c, tn); let sz: i32 = 8; if (wide) { sz = tyslicesize(): i32; }; let off: i32 = localadd(c, l.str, sz, tn); - tupstore(c, cur, off, wide, tn); - cur = cur + tupebytes(wide); + tupstore(c, gpcur, ssecur, off, wide, tn); + if (isflt) { + ssecur = ssecur + 1; + } else { + gpcur = gpcur + tupebytes(wide); + }; l = l.next; if (pt != nil) { pt = pt.next; }; }; diff --git a/test/wcc/956_tuprecv_f64_run.c b/test/wcc/956_tuprecv_f64_run.c index ea8bf447..2c755329 100644 --- a/test/wcc/956_tuprecv_f64_run.c +++ b/test/wcc/956_tuprecv_f64_run.c @@ -1,8 +1,22 @@ /* - * 956_tuprecv_f64_run — runtime + byte-id regression net for #105: a - * tuple-from-call receive corrupts the f64 word when the callee is - * BRANCHED. Covers ALL THREE receive forms, which share the #83 - * tuple_rseq cursor and all carried the same defect: + * 956_tuprecv_f64_run — runtime + byte-id regression net for #105 and + * the #164 (#107) multi-float extension. + * + * #164 (#107): a multi-float tuple return (e.g. (f64,f64)) mis-routes — + * the SEND emitted every float through X0 (cgexpr clobbers X0 per + * element), so two floats collided on X0 and the receive read both from + * X0. The fix gives the tuple return a SysV SSE cursor [X0,X1] parallel + * to the integer cursor [AX,DX,CX,R8]: a float rides the next XMM on an + * INDEPENDENT counter (ref/qbe/amd64/sysv.c retr). The SEND spills each + * float to @tupfscr as it walks (X0 is clobbered by later elements) and + * reloads X0/X1 by SSE index after the integer pops; every receive site + * (single-var 16B/32B, destructure, reassign) reads the float from its + * SSE-cursor reg. SSE caps at 2 (X0,X1) — (f64,f64,f64) loud-stops at + * compile (f64x3_loudstop row asserts the compiler ERRORS, both stages). + * + * #105 (original): a tuple-from-call receive corrupts the f64 word when + * the callee is BRANCHED. Covers ALL THREE receive forms, which share the + * #83 tuple_rseq cursor and all carried the same defect: * 1. SINGLE-VAR `let r = norm(); ...r.0` (cglet 16B-tuple branch) * 2. DESTRUCTURE `let (m,i) = norm()` (N_MLET / cgmlet+tupstore) * 3. REASSIGN `m,i = norm()` (N_MASSIGN / cgmassign+tupstore) @@ -60,7 +74,13 @@ runwait(const char *cmd) return -1; } -struct row { const char *label; const char *src; int want_exit; int chk_stamped; }; +struct row { + const char *label; + const char *src; + int want_exit; + int chk_stamped; + int want_compile_fail; /* #164: loud-stop rows must NOT compile */ +}; static const struct row rows[] = { /* BUG — minimal repro. norm is BRANCHED (inner issub() CALL clobbers @@ -184,6 +204,175 @@ static const struct row rows[] = { "\tif (i != 0) { return 2; };\n" "\treturn 0;\n" "};\n", 0 }, + /* #164 (#107) HEADLINE — multi-float (f64, f64), destructure. The + * callee is BRANCHED (issub CALL clobbers X0), so on master BOTH + * elements collide on X0: cgexpr(a) leaves a in X0, cgexpr(b) + * overwrites it, and every receive read spills from X0 -> x==y==b + * (5.0). Post-fix a rides the SSE cursor X0, b rides X1; the receive + * splits them. x=3.0, y=5.0. Pre-fix: x==5.0 -> return 1. */ + { "f64f64_destr_br", + "package main;\n" + "fn issub(n: f64) bool = { return false; };\n" + "fn pair(a: f64, b: f64) (f64, f64) = {\n" + "\tif (issub(a)) { return (a*2.0, b*2.0); };\n" + "\treturn (a, b);\n" + "};\n" + "export fn main() i32 = {\n" + "\tlet (x, y) = pair(3.0, 5.0);\n" + "\tif (x != 3.0) { return 1; };\n" + "\tif (y != 5.0) { return 2; };\n" + "\treturn 0;\n" + "};\n", 0, 1 }, + /* #164 HEADLINE — multi-float (f64, f64), SINGLE-VAR whole-tuple + * receive (`let r = pair(); r.0 / r.1`, the 16B rt16 branch). Same + * X0-collision on master; post-fix r.0 from X0, r.1 from X1. */ + { "f64f64_single_br", + "package main;\n" + "fn issub(n: f64) bool = { return false; };\n" + "fn pair(a: f64, b: f64) (f64, f64) = {\n" + "\tif (issub(a)) { return (a*2.0, b*2.0); };\n" + "\treturn (a, b);\n" + "};\n" + "export fn main() i32 = {\n" + "\tconst r = pair(3.0, 5.0);\n" + "\tconst x: f64 = r.0;\n" + "\tconst y: f64 = r.1;\n" + "\tif (x != 3.0) { return 1; };\n" + "\tif (y != 5.0) { return 2; };\n" + "\treturn 0;\n" + "};\n", 0 }, + /* #164 HEADLINE — multi-float (f64, f64), REASSIGN into pre-declared + * slots (N_MASSIGN / cgmassign+tupstore). x=3.0, y=5.0. */ + { "f64f64_massign_br", + "package main;\n" + "fn issub(n: f64) bool = { return false; };\n" + "fn pair(a: f64, b: f64) (f64, f64) = {\n" + "\tif (issub(a)) { return (a*2.0, b*2.0); };\n" + "\treturn (a, b);\n" + "};\n" + "export fn main() i32 = {\n" + "\tlet x: f64 = 0.0;\n" + "\tlet y: f64 = 0.0;\n" + "\tx, y = pair(3.0, 5.0);\n" + "\tif (x != 3.0) { return 1; };\n" + "\tif (y != 5.0) { return 2; };\n" + "\treturn 0;\n" + "};\n", 0 }, + /* #164 — INTERLEAVED (i64, f64, i64): kills naive position->reg. The + * two i64s ride the INTEGER cursor (AX,DX), the f64 the SSE cursor + * (X0) on an independent counter — so x=AX, z=DX, y=X0. 3-element + * destructure. x=3, y=2.0, z=7. */ + { "i64_f64_i64_destr", + "package main;\n" + "fn issub(n: i64) bool = { return false; };\n" + "fn tri(a: i64, b: f64, c: i64) (i64, f64, i64) = {\n" + "\tif (issub(a)) { return (a*2, b*2.0, c*2); };\n" + "\treturn (a, b, c);\n" + "};\n" + "export fn main() i32 = {\n" + "\tlet (x, y, z) = tri(3, 2.0, 7);\n" + "\tif (x != 3) { return 1; };\n" + "\tif (y != 2.0) { return 2; };\n" + "\tif (z != 7) { return 3; };\n" + "\treturn 0;\n" + "};\n", 0, 1 }, + /* #164 — (f64, str): SSE + wide (24B {ptr,len,cap} header) coexist. + * The f64 rides the SSE cursor (X0); the str rides the INTEGER + * cursor (AX,DX,CX) since the float consumes no GP slot. Destructure + * form. f=4.0, s.len=5 ("hello"). */ + { "f64_str_destr", + "package main;\n" + "fn issub(n: f64) bool = { return false; };\n" + "fn fs(n: f64) (f64, str) = {\n" + "\tif (issub(n)) { return (n*2.0, \"x\"); };\n" + "\treturn (n, \"hello\");\n" + "};\n" + "export fn main() i32 = {\n" + "\tlet (f, s) = fs(4.0);\n" + "\tif (f != 4.0) { return 1; };\n" + "\tif (s.len != 5) { return 2; };\n" + "\treturn 0;\n" + "};\n", 0, 1 }, + /* #164 STR-FIRST destructure (str, f64): the unification's new- + * coverage shape with the wide header in slot 0. The str rides the + * INTEGER cursor (AX,DX,CX = ptr,len,cap), the f64 the SSE cursor (X0) + * — independent counters, the float consuming no GP slot. The + * destructure path's cursor handled str-first on master too, so this + * pins the dual-cursor restructure PRESERVED it (byte-id both ways) + * AND that the f64 coexists. s.len=5 ("hello"), f=4.0. */ + { "str_f64_destr", + "package main;\n" + "fn issub(n: f64) bool = { return false; };\n" + "fn sf(n: f64) (str, f64) = {\n" + "\tif (issub(n)) { return (\"x\", n*2.0); };\n" + "\treturn (\"hello\", n);\n" + "};\n" + "export fn main() i32 = {\n" + "\tlet (s, f) = sf(4.0);\n" + "\tif (s.len != 5) { return 1; };\n" + "\tif (f != 4.0) { return 2; };\n" + "\treturn 0;\n" + "};\n", 0, 1 }, + /* #164 STR-FIRST destructure (str, i64): pure-integer str-first. + * str@AX,DX,CX then i64@R8. Correct on master too (single-cursor + * destructure already routed ptr=AX) — byte-id regression guard that + * the dual cursor left the integer str-first mapping intact. s.len=5, + * k=7. */ + { "str_i64_destr", + "package main;\n" + "fn issub(n: i64) bool = { return false; };\n" + "fn si(n: i64) (str, i64) = {\n" + "\tif (issub(n)) { return (\"x\", n*2); };\n" + "\treturn (\"hello\", n);\n" + "};\n" + "export fn main() i32 = {\n" + "\tlet (s, k) = si(7);\n" + "\tif (s.len != 5) { return 1; };\n" + "\tif (k != 7) { return 2; };\n" + "\treturn 0;\n" + "};\n", 0 }, + /* #164 STR-FIRST SINGLE-VAR (str, i64), annotated `let t: (str,i64) = + * si(); t.0/t.1`: the shape the OLD 32B single-var branch got WRONG — + * it read .ptr from DX while the send placed .ptr in AX (self- + * inconsistent), scrambling the slot so t.1 read the str.ptr word. + * DISCRIMINATES: pre-fix t.1 = a large address != 7; post-fix the + * unified dual cursor lands str@AX,DX,CX + i64@R8 so t.1=7. Both + * stages were wrong IDENTICALLY pre-fix (byte-id held, runtime broke), + * right identically post-fix. Annotated (not bare `const r=`) because + * the wwstage 32B single-var branch keys on the N_TTUPLE type node — + * a bare 32B single-var is a pre-existing stage asymmetry out of #164 + * scope (16B bare single-var rows above cover the inferred path). */ + { "str_i64_single", + "package main;\n" + "fn issub(n: i64) bool = { return false; };\n" + "fn si(n: i64) (str, i64) = {\n" + "\tif (issub(n)) { return (\"x\", n*2); };\n" + "\treturn (\"hello\", n);\n" + "};\n" + "export fn main() i32 = {\n" + "\tlet t: (str, i64) = si(7);\n" + "\tif (t.1 != 7) { return 1; };\n" + "\tif (t.0.len != 5) { return 2; };\n" + "\treturn 0;\n" + "};\n", 0 }, + /* #164 LOUD-STOP — three f64 = 0 GP / 3 SSE exceeds the SSE return + * cap (X0,X1 only). Must FAIL TO COMPILE in BOTH stages (rule-7: + * surface, never silently collide). Master has no SSE cap and + * miscompiles (build succeeds), so want_compile_fail discriminates: + * pre-fix the build succeeds (test fails), post-fix both stages + * error (test passes). */ + { "f64x3_loudstop", + "package main;\n" + "fn tri(a: f64, b: f64, c: f64) (f64, f64, f64) = {\n" + "\treturn (a, b, c);\n" + "};\n" + "export fn main() i32 = {\n" + "\tlet (x, y, z) = tri(1.0, 2.0, 3.0);\n" + "\tif (x != 1.0) { return 1; };\n" + "\tif (y != 2.0) { return 2; };\n" + "\tif (z != 3.0) { return 3; };\n" + "\treturn 0;\n" + "};\n", 0, 0, 1 }, /* CONTROL — all-integer branched 2-tuple. The integer-cursor MOVQ * path is untouched by the fix (e0/e1 not float), so this is correct * pre- and post-fix and byte-id both ways. a=5, b=7 -> 12. */ @@ -289,13 +478,51 @@ main(void) fputs(rows[i].src, f); fclose(f); + char cmd[2048]; + + /* #164 LOUD-STOP rows: the SSE-cap overflow must FAIL TO COMPILE + * in BOTH stages (rule-7). Assert (a) cstage `ww build` errors, + * and (b) w6c AND w6c_ww each return non-zero — proving the + * loud-stop fires symmetrically. No .s is produced, so the + * byte-id cmp is skipped. Discriminates against master, which + * has no SSE cap and builds the (mis)compile. */ + if (rows[i].want_compile_fail) { + char ldir[64]; + snprintf(ldir, sizeof ldir, "/tmp/wwtupf_%d_l_%d", + getpid(), i); + mkdir(ldir, 0755); + snprintf(cmd, sizeof cmd, + "cd %s && %s/ww build %s >/dev/null 2>&1", + ldir, bin, src); + if (runwait(cmd) == 0) { + fprintf(stderr, "row[%s]: cstage build SUCCEEDED, " + "want loud-stop (SSE cap)\n", rows[i].label); + fail++; + } + snprintf(cmd, sizeof cmd, "%s -o /dev/null %s 2>/dev/null", + w6c, src); + if (runwait(cmd) == 0) { + fprintf(stderr, "row[%s]: w6c emitted .s, want " + "loud-stop\n", rows[i].label); + fail++; + } + snprintf(cmd, sizeof cmd, "%s -o /dev/null %s 2>/dev/null", + w6c_ww, src); + if (runwait(cmd) == 0) { + fprintf(stderr, "row[%s]: w6c_ww emitted .s, want " + "loud-stop\n", rows[i].label); + fail++; + } + unlink(src); rmdir(ldir); + continue; + } + /* (a) cstage build + run in a scratch dir. */ char tmpdir[64]; snprintf(tmpdir, sizeof tmpdir, "/tmp/wwtupf_%d_d_%d", getpid(), i); mkdir(tmpdir, 0755); - char cmd[2048]; snprintf(cmd, sizeof cmd, "cd %s && %s/ww build %s", tmpdir, bin, src); if (runwait(cmd) != 0) {