diff --git a/Makefile b/Makefile index 7f9e70a3..ddbd10af 100644 --- a/Makefile +++ b/Makefile @@ -289,6 +289,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_subslice_ptresz_run \ $(BIN)/test_deref_slice_store_run \ $(BIN)/test_tuple_nary_destructure_run \ + $(BIN)/test_rvalue_tuple_destructure_run \ $(BIN)/test_overcap_tuple_field_store_run \ $(BIN)/test_mixed_scalar_tuple_sret_run \ $(BIN)/test_tuple_in_union_run \ @@ -1164,6 +1165,14 @@ $(BIN)/test_tuple_nary_destructure_run: test/wcc/945_tuple_nary_destructure_run. $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +# #241: materialise an RVALUE tuple (literal / match-yield / ?-call payload) +# into the register cursor before a destructure (run + cs==ww byte-id). +$(BIN)/test_rvalue_tuple_destructure_run: test/wcc/945_rvalue_tuple_destructure_run.c \ + $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ + $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + # #234: over-cap tuple sret store into a local struct field / indexed local # (run + cs==ww byte-id), deferred forms loud-stop (builderr both drivers). $(BIN)/test_overcap_tuple_field_store_run: test/wcc/940_overcap_tuple_field_store_run.c \ diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index b2ca391e..47f7c09f 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -2568,6 +2568,163 @@ cg_structlit_fill_bp(Cg *c, Local **locals_p, Type *lu, Node *lit, int bp_off) cg_structlit_fill(c, locals_p, lu, lit, DST_BP, 0, NULL, bp_off); } +/* cg_tuple_lit_to_cursor — #241: materialise an N_TUPLE literal's elements + * into the SysV register-return cursor — integer words L→R over tuple_rseq + * (AX,DX,CX,R8), floats over tuple_sse_seq (X0,X1), a slice/str's + * {ptr,len,cap} header over three consecutive INTEGER regs — the SAME ABI a + * tuple-returning CALL leaves, which every tuple consumer (tuple_store at + * the N_LET/N_MLET sites) already reads. cgexpr otherwise can't make a tuple + * value (the default arm zeroed AX), so a literal/yield rvalue tuple bound + * or destructured read garbage past word0. Each element's cgexpr clobbers + * AX/X0, so integer words spill L→R and pop into the cursor reversed, floats + * spill to @tupfscr and reload by SSE index — INDEPENDENT counters (ref/qbe/ + * amd64/sysv.c retr). Byte-identical extraction of cgreturn's N_TUPLE arm, + * now shared with cgexpr. Over-cap loud-stops (rule 7); a bare expression + * value can't sret, so the >cap rvalue-tuple materialisation is the #10 + * follow-up. */ +static void +cg_tuple_lit_to_cursor(Cg *c, Local **locals, Node *tuple) +{ + int f32; + int gptotal = 0, ssecount = 0; + for (Node *e = tuple->list; e; e = e->next) { + if (fld_isfloat(e->type, &f32)) + ssecount++; + else + gptotal += tuple_ebytes(node_isstr(e) + || node_isslice(e)); + } + if (gptotal > TUPLE_GPCAP || ssecount > TUPLE_SSECAP) + fatal("tuple literal exceeds register-return ABI capacity " + "(integer %d/%d, SSE %d/%d); over-cap rvalue-tuple " + "materialisation is the #10 sret follow-up", + gptotal, TUPLE_GPCAP, ssecount, TUPLE_SSECAP); + int fscr = 0; + if (ssecount > 0) { + if (cg_tupfscr != 0) + fscr = cg_tupfscr; + else { + fscr = local_alloc(c, locals, "@tupfscr", + TUPLE_SSECAP * 8, cg_frame); + cg_tupfscr = fscr; + } + } + int sseidx = 0; + for (Node *e = tuple->list; e; e = e->next) { + int isflt = fld_isfloat(e->type, &f32); + cgexpr(c, e, *locals); + 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)); + if (node_isstr(e) || node_isslice(e)) { + ins1(c, A_PUSHQ, areg(D_BX)); + ins1(c, A_PUSHQ, areg(D_CX)); + } + } + for (int i = gptotal - 1; i >= 0; i--) + ins1(c, A_POPQ, areg(tuple_rseq[i])); + int j = 0; + for (Node *e = tuple->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++; + } +} + +/* cg_tuple_slot_to_cursor — #241: load a tuple already materialised in a + * BP-relative slot (a tuple-typed IDENT: a let-bound tuple, a match-bound + * union payload) into the SAME register-return cursor. The slot uses the + * register-ABI stride the tuple-init / #242 destructure write (a scalar 8B, + * a slice/str its 3-word header), NOT the packed t.N field layout (#238). + * All sources are memory, so each word loads straight into its cursor reg — + * no spill dance (unlike the literal arm whose element cgexpr clobbers). So + * `yield t` / `return t` / `let q = t` over a tuple ident leave the whole + * tuple in the cursor, not just word0 in AX. Over-cap loud-stops (rule 7; + * the #10 sret follow-up). */ +static void +cg_tuple_slot_to_cursor(Cg *c, int srcoff, Type *tu) +{ + int f32; + int gptotal = 0, ssecount = 0; + for (Tparam *p = tu->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) + fatal("tuple ident exceeds register-return ABI capacity " + "(integer %d/%d, SSE %d/%d); over-cap rvalue-tuple " + "materialisation is the #10 sret follow-up", + gptotal, TUPLE_GPCAP, ssecount, TUPLE_SSECAP); + int gp = 0, sse = 0, foff = 0; + for (Tparam *p = tu->params; p; p = p->next) { + Type *pu = type_chase_named(p->type); + int wide = pu && (pu->kind == TY_SLICE || pu->kind == TY_STR); + int isflt = fld_isfloat(p->type, &f32); + if (isflt) { + ins2(c, f32 ? A_MOVSS : A_MOVSD, + amem(D_BP, srcoff + foff), + areg(tuple_sse_seq[sse])); + sse++; + foff += 8; + } else if (wide) { + for (int k = 0; k < 3; k++) + ins2(c, A_MOVQ, + amem(D_BP, srcoff + foff + k * 8), + areg(tuple_rseq[gp + k])); + gp += 3; + foff += (int)pu->size; + } else { + ins2(c, A_MOVQ, amem(D_BP, srcoff + foff), + areg(tuple_rseq[gp])); + gp += 1; + foff += 8; + } + } +} + +/* cg_tagged_tuple_payload_shift — #241: a `?`-unwrapped tuple payload is an + * rvalue tuple that must fill the register cursor the let/destructure + * consumer reads. A tagged return leaves AX=tag, DX=word0, CX=word1, + * R8=word2; the scalar/str unwrap lifts only word0->AX, stranding word1+ in + * CX/R8. Shift the whole payload DOWN one INTEGER reg so element i lands in + * tuple_rseq[i]. A float/slice/str payload element rides a different SysV + * class (X regs / 3-word header) the flat down-shift can't place — loud-stop + * (rule 7); the per-eightbyte tagged-tuple-payload classification is the + * #243 follow-up. */ +static void +cg_tagged_tuple_payload_shift(Cg *c, Type *tup) +{ + static const int seq[] = { D_AX, D_DX, D_CX, D_R8 }; + int f32; + int words = 0; + for (Tparam *p = tup->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) || wide) + fatal("tuple-in-union ? unwrap: float/slice/str payload " + "element needs SysV per-eightbyte classification " + "(see #243); only integer tuple payloads supported"); + words += tuple_ebytes(0); + } + /* tag occupies AX, so only DX/CX/R8 carry payload words. */ + if (words > (int)nelem(seq) - 1) + fatal("tuple-in-union ? unwrap payload exceeds the 3 integer " + "return regs past the tag (%d words); see #10/#243", words); + for (int i = 0; i < words; i++) + ins2(c, A_MOVQ, areg(seq[i + 1]), areg(seq[i])); +} + static void cgexpr(Cg *c, Node *n, Local *locals) { @@ -2612,7 +2769,13 @@ cgexpr(Cg *c, Node *n, Local *locals) case N_IDENT: { int off = localfind(locals, n->str); if (off != 0) { - if (node_isfloat(n)) { + Type *itu = type_chase_named(n->type); + if (itu && itu->kind == TY_TUPLE) { + /* #241: a tuple ident is a value — leave the whole + * tuple in the register cursor (`yield t` / `return + * t` / `let q = t`), not just word0 in AX. */ + cg_tuple_slot_to_cursor(c, off, itu); + } else if (node_isfloat(n)) { int op = op_for(n, A_MOVSD, A_MOVSS); ins2(c, op, amem(D_BP, off), areg(D_X0)); } else if (node_isstr(n)) { @@ -6635,6 +6798,16 @@ cgexpr(Cg *c, Node *n, Local *locals) ins1(c, A_POPQ, areg(D_BP)); ins0(c, A_RET); label(c, cont); + { + /* #241: a tuple success payload is an rvalue tuple — fill + * the cursor (shift past the tag) so the destructure / + * let consumer reads every element, not just word0. */ + Type *stu = type_chase_named(succ_t); + if (stu && stu->kind == TY_TUPLE) { + cg_tagged_tuple_payload_shift(c, stu); + break; + } + } if (success_is_str) { /* str IS []u8: success value arrives in the tagged * ABI as DX=ptr, CX=len, R8=cap (slot 32B). Move len @@ -6677,6 +6850,15 @@ cgexpr(Cg *c, Node *n, Local *locals) ins2(c, A_MOVQ, aimm(60), areg(D_AX)); ins0(c, A_SYSCALL); label(c, cont); + { + /* #241: tuple success payload fills the cursor (shift past + * the tag) — same rvalue-tuple-into-cursor story. */ + Type *stu = type_chase_named(succ_t); + if (stu && stu->kind == TY_TUPLE) { + cg_tagged_tuple_payload_shift(c, stu); + break; + } + } if (success_is_str) { /* str IS []u8: success arrives DX=ptr, CX=len, R8=cap * (slot 32B). Move len out before cap clobbers CX @@ -7965,6 +8147,13 @@ cgexpr(Cg *c, Node *n, Local *locals) } break; } + case N_TUPLE: + /* #241: a literal tuple rvalue `(a, b)` is a value — pack its + * elements into the register cursor (mirror cgreturn's N_TUPLE + * arm) so a let-bind / destructure consumer reads every element, + * not just AX = 0 from the default arm below. */ + cg_tuple_lit_to_cursor(c, &locals, n); + break; default: cgexpr_int(c, 0); break; diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index a7bf2c26..1820d976 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -11340,30 +11340,46 @@ fn astunsized(c: *checker, t: *node) bool = { // descend into a nested N_MATCH — each match opens its own yield // scope. exprtype is idempotent on already-stamped nodes (tinfocache // path at L467) so re-entering it on the yield operand here is safe. -fn matchyieldtype(c: *checker, body: *node) *node = { +// bname/btype carry the enclosing arm's case-binding name + declared type +// node. #241: when exprtype can't re-derive a `yield ` operand — +// the arm binder's scope is already popped by the time exprtype(N_MATCH) +// runs post-order (resolvewalk's N_MCASE restores c.cur before this), so +// scopelookup of the bare binder ident returns nil — the yielded type IS +// the binding type (btype). cstage avoids this by reading the operand's +// already-stamped ->type (check.c:122) rather than re-running cexpr; wwstage +// caches only node.type_ (a tinfo, not a type NODE), so this binder-typed +// fallback is the node-form recovery for the dominant match-bind-then-yield +// idiom (Hare's parseint `case let t => yield t`). +fn matchyieldtype(c: *checker, body: *node, bname: str, btype: *node) *node = { if (body == nil) { return nil; }; let k: nkind = body.kind; if (k == nkind.N_YIELD) { if (body.lhs == nil) { return nil; }; - return exprtype(c, body.lhs, nil); + let t: *node = exprtype(c, body.lhs, nil); + if (t != nil) { return t; }; + if (body.lhs.kind == nkind.N_IDENT && bname.len > 0 + && streq(body.lhs.str, bname)) { + return btype; + }; + return nil; }; if (k == nkind.N_MATCH) { return nil; }; if (k == nkind.N_BLOCK) { let s: *node = body.list; for (s != nil) { - let t: *node = matchyieldtype(c, s); + let t: *node = matchyieldtype(c, s, bname, btype); if (t != nil) { return t; }; s = s.next; }; return nil; }; if (k == nkind.N_IF) { - let t: *node = matchyieldtype(c, body.body); + let t: *node = matchyieldtype(c, body.body, bname, btype); if (t != nil) { return t; }; - return matchyieldtype(c, body.els); + return matchyieldtype(c, body.els, bname, btype); }; if (k == nkind.N_FOR || k == nkind.N_FORRANGE) { - return matchyieldtype(c, body.body); + return matchyieldtype(c, body.body, bname, btype); }; return nil; }; @@ -13154,7 +13170,10 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { let yt: *node = nil; let cs: *node = e.list; for (cs != nil) { - let t: *node = matchyieldtype(c, cs.body); + // cs.str/cs.lhs = the arm's case-binding name + declared + // type (the N_MCASE binder); fed to matchyieldtype's #241 + // scope-popped `yield ` fallback. + let t: *node = matchyieldtype(c, cs.body, cs.str, cs.lhs); if (t != nil) { yt = t; break; }; cs = cs.next; }; @@ -19045,6 +19064,14 @@ fn cgexpr(c: *cgen, n: *node) void = { if (k == nkind.N_TRYUNW) { cgtryunw(c, n); return; }; if (k == nkind.N_TYPETEST) { cgtypetest(c, n); return; }; if (k == nkind.N_TYPEASSERT) { cgtypeassert(c, n); return; }; + if (k == nkind.N_TUPLE) { + // #241: a literal tuple rvalue `(a, b)` is a value — pack its + // elements into the register cursor (mirror cgreturn's N_TUPLE + // arm) so a let-bind / destructure consumer reads every element, + // not just AX = 0 from the default arm below. + cgtuplelittocursor(c, n); + return; + }; // Default fallback: produce a deterministic AX = 0. Mirrors // the C cgen's `default: cgexpr_int(c, 0)` branch, which is // what `return eof{};` (N_STRUCTLIT with an empty !void @@ -19073,6 +19100,27 @@ fn cgtagvariantidx(c: *cgen, tagged: *node, vt: *node) i32 = { return flatvariantidx(c, tagged, vt); }; +// cgtrytupleshift — #241: if the `?`/`!` operand's success variant (tag 0) +// is a tuple, the unwrapped payload is an rvalue tuple that must fill the +// register cursor (shift past the tag), and the scalar/str MOVQ DX,AX tail +// is skipped. Returns true when it emitted the shift. Reads the operand's +// stamped tagged result tinfo (n.lhs.type_) — the success variant is the +// first param, matching the `CMPQ $0` success-tag convention. +fn cgtrytupleshift(c: *cgen, n: *node) bool = { + if (n.lhs == nil) { return false; }; + let ou: *tinfo = n.lhs.type_: *tinfo; + for (ou != nil && ou.kind == tykind.TY_NAMED) { ou = ou.under; }; + if (ou == nil) { return false; }; + if (ou.kind != tykind.TY_TAGGED) { return false; }; + if (ou.params == nil) { return false; }; + let sv: *tinfo = ou.params.type_; + for (sv != nil && sv.kind == tykind.TY_NAMED) { sv = sv.under; }; + if (sv == nil) { return false; }; + if (sv.kind != tykind.TY_TUPLE) { return false; }; + cgtaggedtuplepayloadshift(c, sv); + return true; +}; + // cgtryprop — `e?` propagates the error variant up the stack. // Success tag = 0 (#216 tracks the legacy/flag-aware success-tag // divergence — out of scope here, success check stays `CMPQ $0`). @@ -19136,6 +19184,10 @@ fn cgtryprop(c: *cgen, n: *node) void = { }; emitline("\tMOVQ\tBP, SP\n\tPOPQ\tBP\n\tRET\n"); emitlabel(cl); + // #241: a tuple success payload is an rvalue tuple — fill the cursor + // (shift past the tag) so the destructure / let consumer reads every + // element, not just word0. Success variant = tag 0 (first param). + if (cgtrytupleshift(c, n)) { return; }; // Success: unwrap value. Tag-only result was AX; the rest of // the codegen expects the success value in AX (and BX for str). // AX=tag, DX=val0, CX=val1 from the call ABI. For str success, @@ -19199,6 +19251,9 @@ fn cgtryunw(c: *cgen, n: *node) void = { emitline("\n"); emitline("\tMOVQ\t$1, DI\n\tMOVQ\t$60, AX\n\tSYSCALL\n"); emitlabel(cl); + // #241: tuple success payload fills the cursor (shift past the tag) — + // same rvalue-tuple-into-cursor story as cgtryprop. + if (cgtrytupleshift(c, n)) { return; }; // Unwrap success value. (Same shuffle pattern as cgtryprop.) let succisstr: bool = false; if (n.lhs != nil) { @@ -19577,6 +19632,16 @@ fn cgident(c: *cgen, n: *node) void = { let lc: *local = localfindnode(c, nm); if (lc != nil) { let off: i32 = lc.off; + // #241: a tuple ident is a value — leave the whole tuple in the + // register cursor (`yield t` / `return t` / `let q = t`), not + // just word0 in AX. Mirror of cstage cgexpr N_IDENT tuple arm. + let itu: *tinfo = nil; + if (lc.tnode != nil) { itu = lc.tnode.type_: *tinfo; }; + for (itu != nil && itu.kind == tykind.TY_NAMED) { itu = itu.under; }; + if (itu != nil) { if (itu.kind == tykind.TY_TUPLE) { + cgtupleslottocursor(c, off, itu); + return; + }; }; // Float local: MOVSS / MOVSD into X0. Skips the AX shuffle // so consumers (cgbin, cgcast, return) pick up the SSE value // directly. @@ -26592,6 +26657,197 @@ fn tupstore(c: *cgen, gpcur: i32, ssecur: i32, off: i32, wide: bool, tn: *node) emitline("(BP)\n"); }; +// cgtuplelittocursor — #241: materialise an N_TUPLE literal's elements into +// the SysV register-return cursor (integer words L->R over tupreg AX/DX/CX/ +// R8, floats over tupsse X0/X1, a slice/str's {ptr,len,cap} over three +// consecutive INTEGER regs) — the SAME ABI a tuple-returning call leaves, +// which every tuple consumer (tupstore at cgmlet/cgmassign) reads. cgexpr +// otherwise falls to its `MOVQ $0, AX` default for a tuple, so a literal +// rvalue tuple bound or destructured read garbage past word0. Byte-identical +// extraction of cgreturn's in-register N_TUPLE arm (cgenstmt.ww), now shared +// with cgexpr. Over-cap loud-stops (rule 7); a bare expression value can't +// sret, so the >cap rvalue-tuple materialisation is the #10 follow-up. +fn cgtuplelittocursor(c: *cgen, tuple: *node) void = { + let ssecap: i32 = TUPLE_SSECAP; + let gptotal: i32 = 0; + let ssecount: i32 = 0; + let e: *node = tuple.list; + for (e != nil) { + if (isfloattype(c, e)) { + ssecount = ssecount + 1; + } else { + let wide: bool = nodeisstr(c, e) || nodeisslice(c, e); + gptotal = gptotal + tupebytes(wide); + }; + e = e.next; + }; + if (gptotal > TUPLE_GPCAP || ssecount > ssecap) { + let msg: str = "tuple literal exceeds register-return ABI capacity (integer AX,DX,CX,R8 / SSE X0,X1); over-cap rvalue-tuple materialisation is the #10 sret follow-up\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 = tuple.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"); + if (nodeisstr(c, e) || nodeisslice(c, e)) { + emitline("\tPUSHQ\tBX\n"); + emitline("\tPUSHQ\tCX\n"); + }; + }; + 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 = tuple.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; + }; +}; + +// cgtupleslottocursor — #241: load a tuple already materialised in a BP- +// relative slot (a tuple-typed IDENT: a let-bound tuple, a match-bound union +// payload) into the SAME register cursor. The slot uses the register-ABI +// stride the tuple-init / #242 destructure write (a scalar 8B, a slice/str +// its 3-word header), NOT the packed t.N field layout (#238). All sources +// are memory, so each word loads straight into its cursor reg. So `yield t` +// / `return t` / `let q = t` over a tuple ident leave the whole tuple in the +// cursor, not just word0 in AX. Over-cap loud-stops (rule 7; #10). Mirror of +// cstage cg_tuple_slot_to_cursor. +fn cgtupleslottocursor(c: *cgen, srcoff: i32, tu: *tinfo) void = { + let gptotal: i32 = 0; + let ssecount: i32 = 0; + let el: *ttupleelem = tu.tupleelems; + for (el != nil) { + let et: *tinfo = el.type_; + for (et != nil && et.kind == tykind.TY_NAMED) { et = et.under; }; + if (et != nil && (et.kind == tykind.TY_F32 || et.kind == tykind.TY_F64)) { + ssecount = ssecount + 1; + } else { + let wide: bool = et != nil && (et.kind == tykind.TY_SLICE || et.kind == tykind.TY_STR); + gptotal = gptotal + tupebytes(wide); + }; + el = el.tnext; + }; + if (gptotal > TUPLE_GPCAP || ssecount > TUPLE_SSECAP) { + let msg: str = "tuple ident exceeds register-return ABI capacity (integer AX,DX,CX,R8 / SSE X0,X1); over-cap rvalue-tuple materialisation is the #10 sret follow-up\n"; + os.write(2, msg.ptr, msg.len: u64); + os.exit(1); + }; + let gp: i32 = 0; + let sse: i32 = 0; + let foff: i32 = 0; + el = tu.tupleelems; + for (el != nil) { + let et: *tinfo = el.type_; + for (et != nil && et.kind == tykind.TY_NAMED) { et = et.under; }; + let isflt: bool = et != nil && (et.kind == tykind.TY_F32 || et.kind == tykind.TY_F64); + let wide: bool = et != nil && (et.kind == tykind.TY_SLICE || et.kind == tykind.TY_STR); + if (isflt) { + let mov: str = "MOVSD"; + if (et.kind == tykind.TY_F32) { mov = "MOVSS"; }; + emitline("\t"); emitline(mov); emitline("\t"); + emitoff((srcoff + foff): i64); + emitline("(BP), "); + emitline(tupsse(sse)); + emitline("\n"); + sse = sse + 1; + foff += 8; + } else { if (wide) { + let k: i32 = 0; + for (k < 3) { + emitline("\tMOVQ\t"); + emitoff((srcoff + foff + k * 8): i64); + emitline("(BP), "); + emitline(tupreg(gp + k)); + emitline("\n"); + k += 1; + }; + gp += 3; + foff += et.size: i32; + } else { + emitline("\tMOVQ\t"); + emitoff((srcoff + foff): i64); + emitline("(BP), "); + emitline(tupreg(gp)); + emitline("\n"); + gp += 1; + foff += 8; + }; }; + el = el.tnext; + }; +}; + +// cgtaggedtuplepayloadshift — #241: a `?`-unwrapped tuple payload is an +// rvalue tuple that must fill the register cursor. The tagged return leaves +// AX=tag, DX=word0, CX=word1, R8=word2; the scalar/str unwrap lifts only +// word0->AX, stranding word1+ in CX/R8. Shift the whole payload DOWN one +// INTEGER reg so element i lands in tupreg(i). Float/slice/str payload +// elements ride a different SysV class — loud-stop (rule 7; the per- +// eightbyte tagged-tuple-payload classification is the #243 follow-up). +// Mirror of cstage cg_tagged_tuple_payload_shift. +fn cgtaggedtuplepayloadshift(c: *cgen, tup: *tinfo) void = { + let words: i32 = 0; + let el: *ttupleelem = tup.tupleelems; + for (el != nil) { + let et: *tinfo = el.type_; + for (et != nil && et.kind == tykind.TY_NAMED) { et = et.under; }; + let isflt: bool = et != nil && (et.kind == tykind.TY_F32 || et.kind == tykind.TY_F64); + let wide: bool = et != nil && (et.kind == tykind.TY_SLICE || et.kind == tykind.TY_STR); + if (isflt || wide) { + let msg: str = "tuple-in-union ? unwrap: float/slice/str payload element needs SysV per-eightbyte classification (see #243); only integer tuple payloads supported\n"; + os.write(2, msg.ptr, msg.len: u64); + os.exit(1); + }; + words = words + 1; + el = el.tnext; + }; + if (words > 3) { + let msg: str = "tuple-in-union ? unwrap payload exceeds the 3 integer return regs past the tag; see #10/#243\n"; + os.write(2, msg.ptr, msg.len: u64); + os.exit(1); + }; + let i: i32 = 0; + for (i < words) { + emitline("\tMOVQ\t"); + emitline(tupreg(i + 1)); + emitline(", "); + emitline(tupreg(i)); + emitline("\n"); + i = i + 1; + }; +}; + fn cgreturn(c: *cgen, n: *node) void = { rundefers(c); let rhs: *node = n.lhs; diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 3947f34a..e913e0d6 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -127,6 +127,14 @@ fn cgexpr(c: *cgen, n: *node) void = { if (k == nkind.N_TRYUNW) { cgtryunw(c, n); return; }; if (k == nkind.N_TYPETEST) { cgtypetest(c, n); return; }; if (k == nkind.N_TYPEASSERT) { cgtypeassert(c, n); return; }; + if (k == nkind.N_TUPLE) { + // #241: a literal tuple rvalue `(a, b)` is a value — pack its + // elements into the register cursor (mirror cgreturn's N_TUPLE + // arm) so a let-bind / destructure consumer reads every element, + // not just AX = 0 from the default arm below. + cgtuplelittocursor(c, n); + return; + }; // Default fallback: produce a deterministic AX = 0. Mirrors // the C cgen's `default: cgexpr_int(c, 0)` branch, which is // what `return eof{};` (N_STRUCTLIT with an empty !void @@ -155,6 +163,27 @@ fn cgtagvariantidx(c: *cgen, tagged: *node, vt: *node) i32 = { return flatvariantidx(c, tagged, vt); }; +// cgtrytupleshift — #241: if the `?`/`!` operand's success variant (tag 0) +// is a tuple, the unwrapped payload is an rvalue tuple that must fill the +// register cursor (shift past the tag), and the scalar/str MOVQ DX,AX tail +// is skipped. Returns true when it emitted the shift. Reads the operand's +// stamped tagged result tinfo (n.lhs.type_) — the success variant is the +// first param, matching the `CMPQ $0` success-tag convention. +fn cgtrytupleshift(c: *cgen, n: *node) bool = { + if (n.lhs == nil) { return false; }; + let ou: *tinfo = n.lhs.type_: *tinfo; + for (ou != nil && ou.kind == tykind.TY_NAMED) { ou = ou.under; }; + if (ou == nil) { return false; }; + if (ou.kind != tykind.TY_TAGGED) { return false; }; + if (ou.params == nil) { return false; }; + let sv: *tinfo = ou.params.type_; + for (sv != nil && sv.kind == tykind.TY_NAMED) { sv = sv.under; }; + if (sv == nil) { return false; }; + if (sv.kind != tykind.TY_TUPLE) { return false; }; + cgtaggedtuplepayloadshift(c, sv); + return true; +}; + // cgtryprop — `e?` propagates the error variant up the stack. // Success tag = 0 (#216 tracks the legacy/flag-aware success-tag // divergence — out of scope here, success check stays `CMPQ $0`). @@ -218,6 +247,10 @@ fn cgtryprop(c: *cgen, n: *node) void = { }; emitline("\tMOVQ\tBP, SP\n\tPOPQ\tBP\n\tRET\n"); emitlabel(cl); + // #241: a tuple success payload is an rvalue tuple — fill the cursor + // (shift past the tag) so the destructure / let consumer reads every + // element, not just word0. Success variant = tag 0 (first param). + if (cgtrytupleshift(c, n)) { return; }; // Success: unwrap value. Tag-only result was AX; the rest of // the codegen expects the success value in AX (and BX for str). // AX=tag, DX=val0, CX=val1 from the call ABI. For str success, @@ -281,6 +314,9 @@ fn cgtryunw(c: *cgen, n: *node) void = { emitline("\n"); emitline("\tMOVQ\t$1, DI\n\tMOVQ\t$60, AX\n\tSYSCALL\n"); emitlabel(cl); + // #241: tuple success payload fills the cursor (shift past the tag) — + // same rvalue-tuple-into-cursor story as cgtryprop. + if (cgtrytupleshift(c, n)) { return; }; // Unwrap success value. (Same shuffle pattern as cgtryprop.) let succisstr: bool = false; if (n.lhs != nil) { @@ -659,6 +695,16 @@ fn cgident(c: *cgen, n: *node) void = { let lc: *local = localfindnode(c, nm); if (lc != nil) { let off: i32 = lc.off; + // #241: a tuple ident is a value — leave the whole tuple in the + // register cursor (`yield t` / `return t` / `let q = t`), not + // just word0 in AX. Mirror of cstage cgexpr N_IDENT tuple arm. + let itu: *tinfo = nil; + if (lc.tnode != nil) { itu = lc.tnode.type_: *tinfo; }; + for (itu != nil && itu.kind == tykind.TY_NAMED) { itu = itu.under; }; + if (itu != nil) { if (itu.kind == tykind.TY_TUPLE) { + cgtupleslottocursor(c, off, itu); + return; + }; }; // Float local: MOVSS / MOVSD into X0. Skips the AX shuffle // so consumers (cgbin, cgcast, return) pick up the SSE value // directly. diff --git a/selfhost/cmd/wcc/cgenstmt.ww b/selfhost/cmd/wcc/cgenstmt.ww index a5de811b..197f2e3d 100644 --- a/selfhost/cmd/wcc/cgenstmt.ww +++ b/selfhost/cmd/wcc/cgenstmt.ww @@ -246,6 +246,197 @@ fn tupstore(c: *cgen, gpcur: i32, ssecur: i32, off: i32, wide: bool, tn: *node) emitline("(BP)\n"); }; +// cgtuplelittocursor — #241: materialise an N_TUPLE literal's elements into +// the SysV register-return cursor (integer words L->R over tupreg AX/DX/CX/ +// R8, floats over tupsse X0/X1, a slice/str's {ptr,len,cap} over three +// consecutive INTEGER regs) — the SAME ABI a tuple-returning call leaves, +// which every tuple consumer (tupstore at cgmlet/cgmassign) reads. cgexpr +// otherwise falls to its `MOVQ $0, AX` default for a tuple, so a literal +// rvalue tuple bound or destructured read garbage past word0. Byte-identical +// extraction of cgreturn's in-register N_TUPLE arm (cgenstmt.ww), now shared +// with cgexpr. Over-cap loud-stops (rule 7); a bare expression value can't +// sret, so the >cap rvalue-tuple materialisation is the #10 follow-up. +fn cgtuplelittocursor(c: *cgen, tuple: *node) void = { + let ssecap: i32 = TUPLE_SSECAP; + let gptotal: i32 = 0; + let ssecount: i32 = 0; + let e: *node = tuple.list; + for (e != nil) { + if (isfloattype(c, e)) { + ssecount = ssecount + 1; + } else { + let wide: bool = nodeisstr(c, e) || nodeisslice(c, e); + gptotal = gptotal + tupebytes(wide); + }; + e = e.next; + }; + if (gptotal > TUPLE_GPCAP || ssecount > ssecap) { + let msg: str = "tuple literal exceeds register-return ABI capacity (integer AX,DX,CX,R8 / SSE X0,X1); over-cap rvalue-tuple materialisation is the #10 sret follow-up\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 = tuple.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"); + if (nodeisstr(c, e) || nodeisslice(c, e)) { + emitline("\tPUSHQ\tBX\n"); + emitline("\tPUSHQ\tCX\n"); + }; + }; + 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 = tuple.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; + }; +}; + +// cgtupleslottocursor — #241: load a tuple already materialised in a BP- +// relative slot (a tuple-typed IDENT: a let-bound tuple, a match-bound union +// payload) into the SAME register cursor. The slot uses the register-ABI +// stride the tuple-init / #242 destructure write (a scalar 8B, a slice/str +// its 3-word header), NOT the packed t.N field layout (#238). All sources +// are memory, so each word loads straight into its cursor reg. So `yield t` +// / `return t` / `let q = t` over a tuple ident leave the whole tuple in the +// cursor, not just word0 in AX. Over-cap loud-stops (rule 7; #10). Mirror of +// cstage cg_tuple_slot_to_cursor. +fn cgtupleslottocursor(c: *cgen, srcoff: i32, tu: *tinfo) void = { + let gptotal: i32 = 0; + let ssecount: i32 = 0; + let el: *ttupleelem = tu.tupleelems; + for (el != nil) { + let et: *tinfo = el.type_; + for (et != nil && et.kind == tykind.TY_NAMED) { et = et.under; }; + if (et != nil && (et.kind == tykind.TY_F32 || et.kind == tykind.TY_F64)) { + ssecount = ssecount + 1; + } else { + let wide: bool = et != nil && (et.kind == tykind.TY_SLICE || et.kind == tykind.TY_STR); + gptotal = gptotal + tupebytes(wide); + }; + el = el.tnext; + }; + if (gptotal > TUPLE_GPCAP || ssecount > TUPLE_SSECAP) { + let msg: str = "tuple ident exceeds register-return ABI capacity (integer AX,DX,CX,R8 / SSE X0,X1); over-cap rvalue-tuple materialisation is the #10 sret follow-up\n"; + os.write(2, msg.ptr, msg.len: u64); + os.exit(1); + }; + let gp: i32 = 0; + let sse: i32 = 0; + let foff: i32 = 0; + el = tu.tupleelems; + for (el != nil) { + let et: *tinfo = el.type_; + for (et != nil && et.kind == tykind.TY_NAMED) { et = et.under; }; + let isflt: bool = et != nil && (et.kind == tykind.TY_F32 || et.kind == tykind.TY_F64); + let wide: bool = et != nil && (et.kind == tykind.TY_SLICE || et.kind == tykind.TY_STR); + if (isflt) { + let mov: str = "MOVSD"; + if (et.kind == tykind.TY_F32) { mov = "MOVSS"; }; + emitline("\t"); emitline(mov); emitline("\t"); + emitoff((srcoff + foff): i64); + emitline("(BP), "); + emitline(tupsse(sse)); + emitline("\n"); + sse = sse + 1; + foff += 8; + } else { if (wide) { + let k: i32 = 0; + for (k < 3) { + emitline("\tMOVQ\t"); + emitoff((srcoff + foff + k * 8): i64); + emitline("(BP), "); + emitline(tupreg(gp + k)); + emitline("\n"); + k += 1; + }; + gp += 3; + foff += et.size: i32; + } else { + emitline("\tMOVQ\t"); + emitoff((srcoff + foff): i64); + emitline("(BP), "); + emitline(tupreg(gp)); + emitline("\n"); + gp += 1; + foff += 8; + }; }; + el = el.tnext; + }; +}; + +// cgtaggedtuplepayloadshift — #241: a `?`-unwrapped tuple payload is an +// rvalue tuple that must fill the register cursor. The tagged return leaves +// AX=tag, DX=word0, CX=word1, R8=word2; the scalar/str unwrap lifts only +// word0->AX, stranding word1+ in CX/R8. Shift the whole payload DOWN one +// INTEGER reg so element i lands in tupreg(i). Float/slice/str payload +// elements ride a different SysV class — loud-stop (rule 7; the per- +// eightbyte tagged-tuple-payload classification is the #243 follow-up). +// Mirror of cstage cg_tagged_tuple_payload_shift. +fn cgtaggedtuplepayloadshift(c: *cgen, tup: *tinfo) void = { + let words: i32 = 0; + let el: *ttupleelem = tup.tupleelems; + for (el != nil) { + let et: *tinfo = el.type_; + for (et != nil && et.kind == tykind.TY_NAMED) { et = et.under; }; + let isflt: bool = et != nil && (et.kind == tykind.TY_F32 || et.kind == tykind.TY_F64); + let wide: bool = et != nil && (et.kind == tykind.TY_SLICE || et.kind == tykind.TY_STR); + if (isflt || wide) { + let msg: str = "tuple-in-union ? unwrap: float/slice/str payload element needs SysV per-eightbyte classification (see #243); only integer tuple payloads supported\n"; + os.write(2, msg.ptr, msg.len: u64); + os.exit(1); + }; + words = words + 1; + el = el.tnext; + }; + if (words > 3) { + let msg: str = "tuple-in-union ? unwrap payload exceeds the 3 integer return regs past the tag; see #10/#243\n"; + os.write(2, msg.ptr, msg.len: u64); + os.exit(1); + }; + let i: i32 = 0; + for (i < words) { + emitline("\tMOVQ\t"); + emitline(tupreg(i + 1)); + emitline(", "); + emitline(tupreg(i)); + emitline("\n"); + i = i + 1; + }; +}; + fn cgreturn(c: *cgen, n: *node) void = { rundefers(c); let rhs: *node = n.lhs; diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index 48d1653d..988c0a13 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -1090,30 +1090,46 @@ fn astunsized(c: *checker, t: *node) bool = { // descend into a nested N_MATCH — each match opens its own yield // scope. exprtype is idempotent on already-stamped nodes (tinfocache // path at L467) so re-entering it on the yield operand here is safe. -fn matchyieldtype(c: *checker, body: *node) *node = { +// bname/btype carry the enclosing arm's case-binding name + declared type +// node. #241: when exprtype can't re-derive a `yield ` operand — +// the arm binder's scope is already popped by the time exprtype(N_MATCH) +// runs post-order (resolvewalk's N_MCASE restores c.cur before this), so +// scopelookup of the bare binder ident returns nil — the yielded type IS +// the binding type (btype). cstage avoids this by reading the operand's +// already-stamped ->type (check.c:122) rather than re-running cexpr; wwstage +// caches only node.type_ (a tinfo, not a type NODE), so this binder-typed +// fallback is the node-form recovery for the dominant match-bind-then-yield +// idiom (Hare's parseint `case let t => yield t`). +fn matchyieldtype(c: *checker, body: *node, bname: str, btype: *node) *node = { if (body == nil) { return nil; }; let k: nkind = body.kind; if (k == nkind.N_YIELD) { if (body.lhs == nil) { return nil; }; - return exprtype(c, body.lhs, nil); + let t: *node = exprtype(c, body.lhs, nil); + if (t != nil) { return t; }; + if (body.lhs.kind == nkind.N_IDENT && bname.len > 0 + && streq(body.lhs.str, bname)) { + return btype; + }; + return nil; }; if (k == nkind.N_MATCH) { return nil; }; if (k == nkind.N_BLOCK) { let s: *node = body.list; for (s != nil) { - let t: *node = matchyieldtype(c, s); + let t: *node = matchyieldtype(c, s, bname, btype); if (t != nil) { return t; }; s = s.next; }; return nil; }; if (k == nkind.N_IF) { - let t: *node = matchyieldtype(c, body.body); + let t: *node = matchyieldtype(c, body.body, bname, btype); if (t != nil) { return t; }; - return matchyieldtype(c, body.els); + return matchyieldtype(c, body.els, bname, btype); }; if (k == nkind.N_FOR || k == nkind.N_FORRANGE) { - return matchyieldtype(c, body.body); + return matchyieldtype(c, body.body, bname, btype); }; return nil; }; @@ -2904,7 +2920,10 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { let yt: *node = nil; let cs: *node = e.list; for (cs != nil) { - let t: *node = matchyieldtype(c, cs.body); + // cs.str/cs.lhs = the arm's case-binding name + declared + // type (the N_MCASE binder); fed to matchyieldtype's #241 + // scope-popped `yield ` fallback. + let t: *node = matchyieldtype(c, cs.body, cs.str, cs.lhs); if (t != nil) { yt = t; break; }; cs = cs.next; }; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index bbc6a833..d85f8680 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -11340,30 +11340,46 @@ fn astunsized(c: *checker, t: *node) bool = { // descend into a nested N_MATCH — each match opens its own yield // scope. exprtype is idempotent on already-stamped nodes (tinfocache // path at L467) so re-entering it on the yield operand here is safe. -fn matchyieldtype(c: *checker, body: *node) *node = { +// bname/btype carry the enclosing arm's case-binding name + declared type +// node. #241: when exprtype can't re-derive a `yield ` operand — +// the arm binder's scope is already popped by the time exprtype(N_MATCH) +// runs post-order (resolvewalk's N_MCASE restores c.cur before this), so +// scopelookup of the bare binder ident returns nil — the yielded type IS +// the binding type (btype). cstage avoids this by reading the operand's +// already-stamped ->type (check.c:122) rather than re-running cexpr; wwstage +// caches only node.type_ (a tinfo, not a type NODE), so this binder-typed +// fallback is the node-form recovery for the dominant match-bind-then-yield +// idiom (Hare's parseint `case let t => yield t`). +fn matchyieldtype(c: *checker, body: *node, bname: str, btype: *node) *node = { if (body == nil) { return nil; }; let k: nkind = body.kind; if (k == nkind.N_YIELD) { if (body.lhs == nil) { return nil; }; - return exprtype(c, body.lhs, nil); + let t: *node = exprtype(c, body.lhs, nil); + if (t != nil) { return t; }; + if (body.lhs.kind == nkind.N_IDENT && bname.len > 0 + && streq(body.lhs.str, bname)) { + return btype; + }; + return nil; }; if (k == nkind.N_MATCH) { return nil; }; if (k == nkind.N_BLOCK) { let s: *node = body.list; for (s != nil) { - let t: *node = matchyieldtype(c, s); + let t: *node = matchyieldtype(c, s, bname, btype); if (t != nil) { return t; }; s = s.next; }; return nil; }; if (k == nkind.N_IF) { - let t: *node = matchyieldtype(c, body.body); + let t: *node = matchyieldtype(c, body.body, bname, btype); if (t != nil) { return t; }; - return matchyieldtype(c, body.els); + return matchyieldtype(c, body.els, bname, btype); }; if (k == nkind.N_FOR || k == nkind.N_FORRANGE) { - return matchyieldtype(c, body.body); + return matchyieldtype(c, body.body, bname, btype); }; return nil; }; @@ -13154,7 +13170,10 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { let yt: *node = nil; let cs: *node = e.list; for (cs != nil) { - let t: *node = matchyieldtype(c, cs.body); + // cs.str/cs.lhs = the arm's case-binding name + declared + // type (the N_MCASE binder); fed to matchyieldtype's #241 + // scope-popped `yield ` fallback. + let t: *node = matchyieldtype(c, cs.body, cs.str, cs.lhs); if (t != nil) { yt = t; break; }; cs = cs.next; }; @@ -19045,6 +19064,14 @@ fn cgexpr(c: *cgen, n: *node) void = { if (k == nkind.N_TRYUNW) { cgtryunw(c, n); return; }; if (k == nkind.N_TYPETEST) { cgtypetest(c, n); return; }; if (k == nkind.N_TYPEASSERT) { cgtypeassert(c, n); return; }; + if (k == nkind.N_TUPLE) { + // #241: a literal tuple rvalue `(a, b)` is a value — pack its + // elements into the register cursor (mirror cgreturn's N_TUPLE + // arm) so a let-bind / destructure consumer reads every element, + // not just AX = 0 from the default arm below. + cgtuplelittocursor(c, n); + return; + }; // Default fallback: produce a deterministic AX = 0. Mirrors // the C cgen's `default: cgexpr_int(c, 0)` branch, which is // what `return eof{};` (N_STRUCTLIT with an empty !void @@ -19073,6 +19100,27 @@ fn cgtagvariantidx(c: *cgen, tagged: *node, vt: *node) i32 = { return flatvariantidx(c, tagged, vt); }; +// cgtrytupleshift — #241: if the `?`/`!` operand's success variant (tag 0) +// is a tuple, the unwrapped payload is an rvalue tuple that must fill the +// register cursor (shift past the tag), and the scalar/str MOVQ DX,AX tail +// is skipped. Returns true when it emitted the shift. Reads the operand's +// stamped tagged result tinfo (n.lhs.type_) — the success variant is the +// first param, matching the `CMPQ $0` success-tag convention. +fn cgtrytupleshift(c: *cgen, n: *node) bool = { + if (n.lhs == nil) { return false; }; + let ou: *tinfo = n.lhs.type_: *tinfo; + for (ou != nil && ou.kind == tykind.TY_NAMED) { ou = ou.under; }; + if (ou == nil) { return false; }; + if (ou.kind != tykind.TY_TAGGED) { return false; }; + if (ou.params == nil) { return false; }; + let sv: *tinfo = ou.params.type_; + for (sv != nil && sv.kind == tykind.TY_NAMED) { sv = sv.under; }; + if (sv == nil) { return false; }; + if (sv.kind != tykind.TY_TUPLE) { return false; }; + cgtaggedtuplepayloadshift(c, sv); + return true; +}; + // cgtryprop — `e?` propagates the error variant up the stack. // Success tag = 0 (#216 tracks the legacy/flag-aware success-tag // divergence — out of scope here, success check stays `CMPQ $0`). @@ -19136,6 +19184,10 @@ fn cgtryprop(c: *cgen, n: *node) void = { }; emitline("\tMOVQ\tBP, SP\n\tPOPQ\tBP\n\tRET\n"); emitlabel(cl); + // #241: a tuple success payload is an rvalue tuple — fill the cursor + // (shift past the tag) so the destructure / let consumer reads every + // element, not just word0. Success variant = tag 0 (first param). + if (cgtrytupleshift(c, n)) { return; }; // Success: unwrap value. Tag-only result was AX; the rest of // the codegen expects the success value in AX (and BX for str). // AX=tag, DX=val0, CX=val1 from the call ABI. For str success, @@ -19199,6 +19251,9 @@ fn cgtryunw(c: *cgen, n: *node) void = { emitline("\n"); emitline("\tMOVQ\t$1, DI\n\tMOVQ\t$60, AX\n\tSYSCALL\n"); emitlabel(cl); + // #241: tuple success payload fills the cursor (shift past the tag) — + // same rvalue-tuple-into-cursor story as cgtryprop. + if (cgtrytupleshift(c, n)) { return; }; // Unwrap success value. (Same shuffle pattern as cgtryprop.) let succisstr: bool = false; if (n.lhs != nil) { @@ -19577,6 +19632,16 @@ fn cgident(c: *cgen, n: *node) void = { let lc: *local = localfindnode(c, nm); if (lc != nil) { let off: i32 = lc.off; + // #241: a tuple ident is a value — leave the whole tuple in the + // register cursor (`yield t` / `return t` / `let q = t`), not + // just word0 in AX. Mirror of cstage cgexpr N_IDENT tuple arm. + let itu: *tinfo = nil; + if (lc.tnode != nil) { itu = lc.tnode.type_: *tinfo; }; + for (itu != nil && itu.kind == tykind.TY_NAMED) { itu = itu.under; }; + if (itu != nil) { if (itu.kind == tykind.TY_TUPLE) { + cgtupleslottocursor(c, off, itu); + return; + }; }; // Float local: MOVSS / MOVSD into X0. Skips the AX shuffle // so consumers (cgbin, cgcast, return) pick up the SSE value // directly. @@ -26592,6 +26657,197 @@ fn tupstore(c: *cgen, gpcur: i32, ssecur: i32, off: i32, wide: bool, tn: *node) emitline("(BP)\n"); }; +// cgtuplelittocursor — #241: materialise an N_TUPLE literal's elements into +// the SysV register-return cursor (integer words L->R over tupreg AX/DX/CX/ +// R8, floats over tupsse X0/X1, a slice/str's {ptr,len,cap} over three +// consecutive INTEGER regs) — the SAME ABI a tuple-returning call leaves, +// which every tuple consumer (tupstore at cgmlet/cgmassign) reads. cgexpr +// otherwise falls to its `MOVQ $0, AX` default for a tuple, so a literal +// rvalue tuple bound or destructured read garbage past word0. Byte-identical +// extraction of cgreturn's in-register N_TUPLE arm (cgenstmt.ww), now shared +// with cgexpr. Over-cap loud-stops (rule 7); a bare expression value can't +// sret, so the >cap rvalue-tuple materialisation is the #10 follow-up. +fn cgtuplelittocursor(c: *cgen, tuple: *node) void = { + let ssecap: i32 = TUPLE_SSECAP; + let gptotal: i32 = 0; + let ssecount: i32 = 0; + let e: *node = tuple.list; + for (e != nil) { + if (isfloattype(c, e)) { + ssecount = ssecount + 1; + } else { + let wide: bool = nodeisstr(c, e) || nodeisslice(c, e); + gptotal = gptotal + tupebytes(wide); + }; + e = e.next; + }; + if (gptotal > TUPLE_GPCAP || ssecount > ssecap) { + let msg: str = "tuple literal exceeds register-return ABI capacity (integer AX,DX,CX,R8 / SSE X0,X1); over-cap rvalue-tuple materialisation is the #10 sret follow-up\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 = tuple.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"); + if (nodeisstr(c, e) || nodeisslice(c, e)) { + emitline("\tPUSHQ\tBX\n"); + emitline("\tPUSHQ\tCX\n"); + }; + }; + 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 = tuple.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; + }; +}; + +// cgtupleslottocursor — #241: load a tuple already materialised in a BP- +// relative slot (a tuple-typed IDENT: a let-bound tuple, a match-bound union +// payload) into the SAME register cursor. The slot uses the register-ABI +// stride the tuple-init / #242 destructure write (a scalar 8B, a slice/str +// its 3-word header), NOT the packed t.N field layout (#238). All sources +// are memory, so each word loads straight into its cursor reg. So `yield t` +// / `return t` / `let q = t` over a tuple ident leave the whole tuple in the +// cursor, not just word0 in AX. Over-cap loud-stops (rule 7; #10). Mirror of +// cstage cg_tuple_slot_to_cursor. +fn cgtupleslottocursor(c: *cgen, srcoff: i32, tu: *tinfo) void = { + let gptotal: i32 = 0; + let ssecount: i32 = 0; + let el: *ttupleelem = tu.tupleelems; + for (el != nil) { + let et: *tinfo = el.type_; + for (et != nil && et.kind == tykind.TY_NAMED) { et = et.under; }; + if (et != nil && (et.kind == tykind.TY_F32 || et.kind == tykind.TY_F64)) { + ssecount = ssecount + 1; + } else { + let wide: bool = et != nil && (et.kind == tykind.TY_SLICE || et.kind == tykind.TY_STR); + gptotal = gptotal + tupebytes(wide); + }; + el = el.tnext; + }; + if (gptotal > TUPLE_GPCAP || ssecount > TUPLE_SSECAP) { + let msg: str = "tuple ident exceeds register-return ABI capacity (integer AX,DX,CX,R8 / SSE X0,X1); over-cap rvalue-tuple materialisation is the #10 sret follow-up\n"; + os.write(2, msg.ptr, msg.len: u64); + os.exit(1); + }; + let gp: i32 = 0; + let sse: i32 = 0; + let foff: i32 = 0; + el = tu.tupleelems; + for (el != nil) { + let et: *tinfo = el.type_; + for (et != nil && et.kind == tykind.TY_NAMED) { et = et.under; }; + let isflt: bool = et != nil && (et.kind == tykind.TY_F32 || et.kind == tykind.TY_F64); + let wide: bool = et != nil && (et.kind == tykind.TY_SLICE || et.kind == tykind.TY_STR); + if (isflt) { + let mov: str = "MOVSD"; + if (et.kind == tykind.TY_F32) { mov = "MOVSS"; }; + emitline("\t"); emitline(mov); emitline("\t"); + emitoff((srcoff + foff): i64); + emitline("(BP), "); + emitline(tupsse(sse)); + emitline("\n"); + sse = sse + 1; + foff += 8; + } else { if (wide) { + let k: i32 = 0; + for (k < 3) { + emitline("\tMOVQ\t"); + emitoff((srcoff + foff + k * 8): i64); + emitline("(BP), "); + emitline(tupreg(gp + k)); + emitline("\n"); + k += 1; + }; + gp += 3; + foff += et.size: i32; + } else { + emitline("\tMOVQ\t"); + emitoff((srcoff + foff): i64); + emitline("(BP), "); + emitline(tupreg(gp)); + emitline("\n"); + gp += 1; + foff += 8; + }; }; + el = el.tnext; + }; +}; + +// cgtaggedtuplepayloadshift — #241: a `?`-unwrapped tuple payload is an +// rvalue tuple that must fill the register cursor. The tagged return leaves +// AX=tag, DX=word0, CX=word1, R8=word2; the scalar/str unwrap lifts only +// word0->AX, stranding word1+ in CX/R8. Shift the whole payload DOWN one +// INTEGER reg so element i lands in tupreg(i). Float/slice/str payload +// elements ride a different SysV class — loud-stop (rule 7; the per- +// eightbyte tagged-tuple-payload classification is the #243 follow-up). +// Mirror of cstage cg_tagged_tuple_payload_shift. +fn cgtaggedtuplepayloadshift(c: *cgen, tup: *tinfo) void = { + let words: i32 = 0; + let el: *ttupleelem = tup.tupleelems; + for (el != nil) { + let et: *tinfo = el.type_; + for (et != nil && et.kind == tykind.TY_NAMED) { et = et.under; }; + let isflt: bool = et != nil && (et.kind == tykind.TY_F32 || et.kind == tykind.TY_F64); + let wide: bool = et != nil && (et.kind == tykind.TY_SLICE || et.kind == tykind.TY_STR); + if (isflt || wide) { + let msg: str = "tuple-in-union ? unwrap: float/slice/str payload element needs SysV per-eightbyte classification (see #243); only integer tuple payloads supported\n"; + os.write(2, msg.ptr, msg.len: u64); + os.exit(1); + }; + words = words + 1; + el = el.tnext; + }; + if (words > 3) { + let msg: str = "tuple-in-union ? unwrap payload exceeds the 3 integer return regs past the tag; see #10/#243\n"; + os.write(2, msg.ptr, msg.len: u64); + os.exit(1); + }; + let i: i32 = 0; + for (i < words) { + emitline("\tMOVQ\t"); + emitline(tupreg(i + 1)); + emitline(", "); + emitline(tupreg(i)); + emitline("\n"); + i = i + 1; + }; +}; + fn cgreturn(c: *cgen, n: *node) void = { rundefers(c); let rhs: *node = n.lhs; diff --git a/test/wcc/945_rvalue_tuple_destructure_run.c b/test/wcc/945_rvalue_tuple_destructure_run.c new file mode 100644 index 00000000..83c6f130 --- /dev/null +++ b/test/wcc/945_rvalue_tuple_destructure_run.c @@ -0,0 +1,241 @@ +/* + * 945_rvalue_tuple_destructure_run — project #241: materialise an RVALUE + * tuple into the register cursor before a destructure / let bind. + * + * cgexpr could not produce a tuple VALUE: a literal `(a, b)` fell to the + * `cgexpr_int(0)` / `MOVQ $0, AX` default, a tuple-typed IDENT loaded only + * word0 into AX, and the `?`/`!` unwrap of a tuple-in-union payload lifted + * only word0->AX (stranding word1 in CX). So `let (x, y) = ` + * read GARBAGE past the first element (cstage), and the un-typed binder left + * wwstage's checker aborting (asserttyped). DANGEROUS gate-blind cs!=ww — the + * strconv blocker (`let (sign, u) = parseint(s, base)?`). + * + * Fix (both stages, byte-identical per rule 10): cgexpr packs an N_TUPLE + * literal and a tuple IDENT into the SysV register-return cursor (the SAME + * ABI a tuple-returning call leaves, which the cgmlet/cgmassign consumers + * already read); the ?/! unwrap shifts a tuple success payload down one + * integer reg past the tag. wwstage's checker recovers the popped match-arm + * binder type for a `yield ` (scope-free, via the arm's declared + * type) so the destructured binders stamp. + * + * Rows assert BOTH elements on BOTH drivers AND cs==ww byte-identical: + * literal_destructure `let (a, b) = (true, 11u64)` + * match_yield `let (sg, u) = match (mk()) { case let t => yield t }` + * trycall_strconv `let (sign, u) = parseint(base)?` (the strconv shape) + * + * NOT covered (separate root, deferred): single-var `let q = (true, 9u64)` + * then `q.N` rides #238 — the N_LET tuple-init sz==16||32 gate + the N_DOT + * tuple-field PACKED-offset reader vs tuple_store's 8B stride disagree for a + * narrow-first tuple. Out of scope for #241 (the rvalue-into-cursor fix); see + * task #6 / proj #238. + * + * All K_RUN: build+run exit 0 on BOTH drivers AND cs==ww byte-identical. + * NNN<950, self-contained (/tmp, no imports), so rule-14's selfhost-sibling + * race does not apply (903/940/945 precedent). + */ +#include +#include +#include +#include +#include +#include + +static int +runwait(const char *cmd) +{ + int rc = system(cmd); + if (rc == -1) return -1; + if (WIFEXITED(rc)) return WEXITSTATUS(rc); + return -1; +} + +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), cb = fgetc(fb); + if (ca != cb) { rc = -1; break; } + if (ca == EOF) break; + } + fclose(fa); fclose(fb); + return rc; +} + +struct row { const char *label; const char *src; int want; }; + +static const struct row rows[] = { + { "literal_destructure", + "package main;\n" + "export fn main() i32 = {\n" + " let (a, b) = (true, 11u64);\n" + " if (b != 11u64) { return 1; };\n" + " if (!a) { return 2; };\n" + " return 0;\n" + "};\n", 0 }, + { "match_yield", + "package main;\n" + "type myerr = !void;\n" + "fn mk(x: u64, s: bool) ((bool, u64) | myerr) = { return (s, x); };\n" + "export fn main() i32 = {\n" + " let (sg, u) = match (mk(6u64, true)) {\n" + " case let t: (bool, u64) => yield t;\n" + " case myerr => { return 9; };\n" + " };\n" + " if (u != 6u64) { return 1; };\n" + " if (!sg) { return 2; };\n" + " return 0;\n" + "};\n", 0 }, + { "trycall_strconv", + "package main;\n" + "type invalid = !void;\n" + "fn parseint(base: int) ((bool, u64) | invalid) = {\n" + " let sg: bool = true;\n" + " let n: u64 = 42u64;\n" + " return (sg, n);\n" + "};\n" + "fn stoi(base: int) (u64 | invalid) = {\n" + " let (sign, u) = parseint(base)?;\n" + " if (!sign) { return 0u64; };\n" + " return u;\n" + "};\n" + "export fn main() i32 = {\n" + " match (stoi(10)) {\n" + " case let v: u64 => { if (v != 42u64) { return 1; }; };\n" + " case invalid => { return 2; };\n" + " };\n" + " return 0;\n" + "};\n", 0 }, +}; + +/* build+run via a driver (ww / ww_ww); returns 0 pass, nonzero fail. */ +static int +run_driver(const char *driver, const struct row *r, int i) +{ + char src[96], tmpdir[96], errf[96], cmd[1024]; + snprintf(src, sizeof src, "/tmp/rvtd_%d_%d.ww", getpid(), i); + snprintf(tmpdir, sizeof tmpdir, "/tmp/rvtd_%d_d_%d", getpid(), i); + snprintf(errf, sizeof errf, "/tmp/rvtd_%d_e_%d", getpid(), i); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(r->src, f); + fclose(f); + + mkdir(tmpdir, 0755); + snprintf(cmd, sizeof cmd, "cd %s && %s build %s >/dev/null 2>%s", + tmpdir, driver, src, errf); + int brc = runwait(cmd); + if (brc != 0) { + fprintf(stderr, "row[%s]: build via %s failed\n", + r->label, driver); + unlink(src); unlink(errf); rmdir(tmpdir); + return -1; + } + + const char *base = strrchr(src, '/'); + base = base ? base + 1 : src; + char outbin[256]; + snprintf(outbin, sizeof outbin, "%s/%s", tmpdir, base); + char *dot = strrchr(outbin, '.'); + if (dot && strcmp(dot, ".ww") == 0) *dot = '\0'; + int got = runwait(outbin); + + unlink(src); unlink(outbin); unlink(errf); rmdir(tmpdir); + if (got != r->want) { + fprintf(stderr, "row[%s]: %s exit %d, want %d\n", + r->label, driver, got, r->want); + return 1; + } + return 0; +} + +/* cs==ww .s byte-id (rule 10). */ +static int +byteid(const char *w6c, const char *w6c_ww, const struct row *r, int i) +{ + char src[96], cs_s[96], ws_s[96], cmd[1024]; + snprintf(src, sizeof src, "/tmp/rvtd_bi_%d_%d.ww", getpid(), i); + snprintf(cs_s, sizeof cs_s, "/tmp/rvtd_bi_%d_%d_cs.s", getpid(), i); + snprintf(ws_s, sizeof ws_s, "/tmp/rvtd_bi_%d_%d_ww.s", getpid(), i); + + FILE *f = fopen(src, "wb"); + if (!f) return -1; + fputs(r->src, f); + fclose(f); + + int rc = 0; + snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", w6c, cs_s, src); + if (runwait(cmd) != 0) { fprintf(stderr, "row[%s]: w6c failed\n", r->label); rc = 1; } + else { + 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 failed\n", r->label); rc = 1; } + else if (slurp_eq(cs_s, ws_s) != 0) { + fprintf(stderr, "row[%s]: cstage/wwstage .s DIFFER " + "(rule-10 byte-id)\n", r->label); + rc = 1; + } + } + unlink(src); unlink(cs_s); unlink(ws_s); + return rc; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[512]; + if (bin[0] != '/') { + char cwd[256]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + + char cdrv[640], wdrv[640], w6c[640], w6c_ww[640]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); + snprintf(w6c, sizeof w6c, "%s/w6c", bin); + snprintf(w6c_ww, sizeof w6c_ww, "%s/w6c_ww", bin); + + struct { const char *name; const char *path; int gated; } + drivers[] = { + { "cstage", cdrv, 0 }, + { "wwstage", wdrv, 1 }, + { NULL, NULL, 0 }, + }; + + int n = (int)(sizeof rows / sizeof rows[0]); + int total = 0, fail = 0; + + for (int d = 0; drivers[d].name; d++) { + if (drivers[d].gated && access(drivers[d].path, X_OK) != 0) { + fprintf(stderr, "rvalue_tuple_destructure: skip %s (no %s)\n", + drivers[d].name, drivers[d].path); + continue; + } + for (int i = 0; i < n; i++) { + total++; + if (run_driver(drivers[d].path, &rows[i], i) != 0) fail++; + } + } + + if (access(w6c_ww, X_OK) == 0) { + for (int i = 0; i < n; i++) { + total++; + if (byteid(w6c, w6c_ww, &rows[i], i) != 0) fail++; + } + } + + if (fail) { + fprintf(stderr, "rvalue_tuple_destructure: %d/%d checks failed\n", + fail, total); + return 1; + } + printf("rvalue_tuple_destructure: %d/%d ok\n", total, total); + return 0; +}