From d14a23b85e687081a7a7ce715c985bc0b101fa77 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Fri, 5 Jun 2026 09:05:01 +0900 Subject: [PATCH] =?UTF-8?q?cgen:=20#57=20in-cap=20tuple=20cursor=20fill=20?= =?UTF-8?q?keys=20on=20the=20DECLARED=20element=20type=20=E2=80=94=20tagge?= =?UTF-8?q?d=20elems=20from=20concrete=20rvalues=20widen,=20both=20stages?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The N_TUPLE literal's stamped type is CONSTRUCTED from its elements (check.c N_TUPLE keeps untyped/concrete element types; assignability is consumer-side), so the in-cap cursor fill — count (tuple_lit_gpwords/tuplitgpwords) + push (tuple_lit_push_elem/ tuplitpushelem) — never saw the DECLARED tuple type. A declared-TAGGED element whose expr is a concrete rvalue (`return (5: size, 9)` into (un16, size)) counted ONE word and skipped the widen entirely: 2 words sent against the receiver's declared 3-word walk, every later element read garbage. Both stages, byte-identical, gate-blind (ken /tmp/ken57 p8/p9: t.1 read entry-junk). The let-literal twin (`let t: (un16, size) = (5: size, 9)`) and the tagged-SECOND-elem shift broke identically (probes q1/q2). The over-cap (sret) arm already walks declared params (#240/#22b) — only the in-cap path was declared-blind. Fix threads the declared tuple type into the ONE shared helper pair and its two loop sites: - tuple_lit_gpwords/tuplitpushelem take the declared elem type; declared-TAGGED + concrete rvalue widens into the shared tagged scratch (cg_tagscr_slot/tagscradd + cg_widen_tagged_store/ cgwidentaggedstore, the cgreturn tagged-@retscr shape) and pushes the box words; declared-TAGGED gates the SSE row off (a (void|f64) box rides INTEGER eightbytes). Tagged->tagged subset (eslot mismatch) louds — the #23/#40 widening-remap family. - cg_tuple_lit_to_cursor/cgtuplelittocursor grow a decl param; cgreturn's in-cap N_TUPLE loops thread cg_ret_type/c.fnret.list (the same pp/pt walk its over-cap arm does); the N_LET in-cap tuple arm passes the declared type for an N_TUPLE rhs; the bare cgexpr route passes NULL/nil (emission unchanged). Ident-elem sources keep the existing slot-load push byte-identically (t57_ident_no_regress); the CALL-elem tripwire stays loud (#41, t57_loud_call_elem). RESIDUAL FILED, not folded (rule 11): the N_MASSIGN destructure-reassign literal rhs routes through the bare cgexpr path (decl=NULL) and stays silent-wrong — probe q5_massign, task #64, cited at the massign arm both stages. The annotated multi-let spelling (`let (a, b): (un, size) = lit`) does not parse (both stages), so N_MLET has no declared-literal route. 941 rows t57_*: return (named + inline union), let-literal, tagged second elem, float payload, bare-untyped payload (rides the #33 chooser through the new wire), ident anchor, loud CALL tripwire; ken's adversarial shapes (tagged-MID elem, two tagged rvalue elems incl. void, plain-f64 SSE coexisting with a declared-tagged box), the in-cap/over-cap boundary loud (k57d), and the NEW #57 tag-remap loud pinned. Pre-fix at e8977a4: p8/p9 rows exit 1, q1_let exit 1, q2_mixed exit 2. Task #57. --- cmd/w6c/cgen.c | 184 +++++++++++++++++++------ selfhost/cmd/w6c/main.combined.ww | 174 +++++++++++++++++++++--- selfhost/cmd/wcc/cgenexpr.ww | 2 +- selfhost/cmd/wcc/cgenstmt.ww | 172 ++++++++++++++++++++--- selfhost/cmd/wwdump/main.combined.ww | 174 +++++++++++++++++++++--- test/wcc/941_tuple_slot_layout_run.c | 195 +++++++++++++++++++++++++++ 6 files changed, 796 insertions(+), 105 deletions(-) diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index fed46516..ce05fabb 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -3289,10 +3289,20 @@ cg_structlit_fill_bp(Cg *c, Local **locals_p, Type *lu, Node *lit, int bp_off) * float rides the SSE row (0 GP words); str/slice push their 3-word * header; a tagged element its tuple_eslot/8 box words; a void element * pushes nothing (the checker's 0-slot — pre-#22 the push/receive - * disagreed with the checker here, latent, no consumer); a scalar 1. */ + * disagreed with the checker here, latent, no consumer); a scalar 1. + * + * #57: `dt` is the DECLARED tuple element type (NULL when the consumer + * has none). The N_TUPLE literal's stamped type is CONSTRUCTED from + * its elements (check.c N_TUPLE), so a concrete rvalue under a + * declared-TAGGED slot counted ONE word here while the receive walks + * the declared eslot — the cursor shifted and every later element + * read garbage. Declared-tagged keys the count on the DECLARED box. */ static int -tuple_lit_gpwords(Node *e) +tuple_lit_gpwords(Node *e, Type *dt) { + Type *du = dt ? type_chase_named(dt) : NULL; + if (du && du->kind == TY_TAGGED) + return tuple_eslot(dt) / 8; int f32; if (fld_isfloat(e->type, &f32)) return 0; if (node_isstr(e) || node_isslice(e)) return (int)(ty_str->size / 8); @@ -3306,20 +3316,49 @@ tuple_lit_gpwords(Node *e) * its INTEGER cursor words L→R (the pop side fills tuple_rseq in * reverse). A tagged element loads its box words straight from its * local slot — cgexpr's ident load is word0-only for tagged (every - * tagged consumer reads memory), so the cursor fill must too; any - * other tagged source shape is loud (rule 7; the cursor-receive arm - * for call results rides the #35 non-ident-source family, widening - * literals #23). Shared by cg_tuple_lit_to_cursor and the cgreturn - * N_TUPLE arm — count (tuple_lit_gpwords) and push live or die - * together. */ + * tagged consumer reads memory), so the cursor fill must too. Shared + * by cg_tuple_lit_to_cursor and the cgreturn N_TUPLE arm — count + * (tuple_lit_gpwords) and push live or die together. + * + * #57: a DECLARED-tagged element whose expr is a concrete rvalue + * (`return (5: size, 9)` — cast, literal, call) skipped the widen + * entirely: the stamped-keyed arm below saw a scalar and pushed ONE + * word, the receiver read the declared box words — silent shift, both + * stages, gate-blind (ken /tmp/ken57). Such an element now widens + * into the shared tagged scratch (cg_widen_tagged_store, the cgreturn + * tagged-@retscr shape) and pushes the box words. A tagged→tagged + * SUBSET element (eslot mismatch) needs a tag remap on the way into + * the slot — loud (rule 7, the #23/#40 widening family). */ static void -tuple_lit_push_elem(Cg *c, Local *locals, Node *e) +tuple_lit_push_elem(Cg *c, Local **locals_p, Node *e, Type *dt) { + Type *du = dt ? type_chase_named(dt) : NULL; Type *eu = type_chase_named(e->type); + if (du && du->kind == TY_TAGGED + && !(eu && eu->kind == TY_TAGGED)) { + int eslot = tuple_eslot(dt); + int scr = cg_tagscr_slot(c, locals_p, eslot); + ins2(c, A_XORQ, areg(D_AX), areg(D_AX)); + for (int k = 0; k < eslot; k += 8) + ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, scr + k)); + cg_widen_tagged_store(c, locals_p, du, e, D_BP, scr, + eslot); + for (int k = 0; k < eslot / 8; k++) { + ins2(c, A_MOVQ, amem(D_BP, scr + k * 8), + areg(D_AX)); + ins1(c, A_PUSHQ, areg(D_AX)); + } + return; + } + if (du && du->kind == TY_TAGGED && eu && eu->kind == TY_TAGGED + && tuple_eslot(dt) != tuple_eslot(e->type)) + fatal("#57: tagged tuple element widening into a wider " + "declared union slot needs a tag remap (rule 7; " + "the #23/#40 widening family)"); if (eu && eu->kind == TY_TAGGED) { int eslot = tuple_eslot(e->type); int eoff = (e->kind == N_IDENT && e->str) - ? localfind(locals, e->str) : 0; + ? localfind(*locals_p, e->str) : 0; if (eoff == 0) fatal("#22a: tagged tuple element from a non-local " "source shape unwired (ident locals only; " @@ -3332,7 +3371,7 @@ tuple_lit_push_elem(Cg *c, Local *locals, Node *e) } return; } - cgexpr(c, e, locals); + cgexpr(c, e, *locals_p); if (eu && eu->kind == TY_VOID) return; ins1(c, A_PUSHQ, areg(D_AX)); @@ -3355,17 +3394,30 @@ tuple_lit_push_elem(Cg *c, Local *locals, Node *e) * 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. */ + * follow-up. + * + * #57: `decl` is the consumer's DECLARED tuple type (NULL when it has + * none — the bare cgexpr route). A declared-TAGGED element gates the + * SSE row off (its payload may be float-stamped but the BOX rides + * INTEGER eightbytes) and keys count + push on the declared eslot — + * see tuple_lit_gpwords / tuple_lit_push_elem. */ static void -cg_tuple_lit_to_cursor(Cg *c, Local **locals, Node *tuple) +cg_tuple_lit_to_cursor(Cg *c, Local **locals, Node *tuple, Type *decl) { + Type *du = decl ? type_chase_named(decl) : NULL; + Tparam *dp0 = (du && du->kind == TY_TUPLE) ? du->params : NULL; int f32; int gptotal = 0, ssecount = 0; + Tparam *dp = dp0; for (Node *e = tuple->list; e; e = e->next) { - if (fld_isfloat(e->type, &f32)) + Type *dtu = dp ? type_chase_named(dp->type) : NULL; + int dtagged = dtu && dtu->kind == TY_TAGGED; + if (!dtagged && fld_isfloat(e->type, &f32)) ssecount++; else - gptotal += tuple_lit_gpwords(e); + gptotal += tuple_lit_gpwords(e, + dp ? dp->type : NULL); + if (dp) dp = dp->next; } if (gptotal > TUPLE_GPCAP || ssecount > TUPLE_SSECAP) fatal("tuple literal exceeds register-return ABI capacity " @@ -3383,27 +3435,36 @@ cg_tuple_lit_to_cursor(Cg *c, Local **locals, Node *tuple) } } int sseidx = 0; + dp = dp0; for (Node *e = tuple->list; e; e = e->next) { - int isflt = fld_isfloat(e->type, &f32); + Type *dtu = dp ? type_chase_named(dp->type) : NULL; + int dtagged = dtu && dtu->kind == TY_TAGGED; + int isflt = !dtagged && fld_isfloat(e->type, &f32); if (isflt) { cgexpr(c, e, *locals); ins2(c, f32 ? A_MOVSS : A_MOVSD, areg(D_X0), amem(D_BP, fscr + sseidx * 8)); sseidx++; - continue; + } else { + tuple_lit_push_elem(c, locals, e, + dp ? dp->type : NULL); } - tuple_lit_push_elem(c, *locals, e); + if (dp) dp = dp->next; } for (int i = gptotal - 1; i >= 0; i--) ins1(c, A_POPQ, areg(tuple_rseq[i])); int j = 0; + dp = dp0; 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++; + Type *dtu = dp ? type_chase_named(dp->type) : NULL; + int dtagged = dtu && dtu->kind == TY_TAGGED; + if (!dtagged && fld_isfloat(e->type, &f32)) { + ins2(c, f32 ? A_MOVSS : A_MOVSD, + amem(D_BP, fscr + j * 8), + areg(tuple_sse_seq[j])); + j++; + } + if (dp) dp = dp->next; } } @@ -11474,7 +11535,7 @@ cgexpr(Cg *c, Node *n, Local *locals) * 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); + cg_tuple_lit_to_cursor(c, &locals, n, NULL); break; default: cgexpr_int(c, 0); @@ -11636,7 +11697,17 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) * receive below, exactly as before. */ if (n->rhs && lu && lu->kind == TY_TUPLE && cg_sret_retsize(lt) == 0) { - cgexpr(c, n->rhs, *locals); + /* #57: a tuple LITERAL rhs carries the DECLARED type + * into the cursor fill — its stamped type is element- + * constructed, so a declared-tagged element's concrete + * rvalue skipped the widen and the fill/receive cursor + * walks skewed (let-twin of the return-position bug; + * probe /tmp/p57/q1_let). Same emission as the cgexpr + * route for every declared-tagged-free literal. */ + if (n->rhs->kind == N_TUPLE) + cg_tuple_lit_to_cursor(c, locals, n->rhs, lu); + else + cgexpr(c, n->rhs, *locals); int gpcur = 0, ssecur = 0, eoff = 0, ef32; for (Tparam *p = lu->params; p; p = p->next) { int isflt = fld_isfloat(p->type, &ef32); @@ -12857,11 +12928,29 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) * drives the receive sites. */ int ssecap = TUPLE_SSECAP; int gptotal = 0, ssecount = 0, f32; + /* #57: count + push key on the DECLARED return-type + * element (cg_ret_type tuple params) — the literal's + * stamped type is element-constructed, so a declared- + * TAGGED element's concrete rvalue counted 1 word and + * skipped the widen while the caller's receive walks + * the declared eslot (2 words sent for a 3-word shape; + * ken /tmp/ken57 p8/p9). Same pp walk the over-cap arm + * already does (#240/#22b). */ + Type *rttc = cg_ret_type + ? type_chase_named(cg_ret_type) : NULL; + Tparam *rp0 = (rttc && rttc->kind == TY_TUPLE) + ? rttc->params : NULL; + Tparam *rp = rp0; for (Node *e = n->lhs->list; e; e = e->next) { - if (fld_isfloat(e->type, &f32)) + Type *rdu = rp + ? type_chase_named(rp->type) : NULL; + int rdtag = rdu && rdu->kind == TY_TAGGED; + if (!rdtag && fld_isfloat(e->type, &f32)) ssecount++; else - gptotal += tuple_lit_gpwords(e); + gptotal += tuple_lit_gpwords(e, + rp ? rp->type : NULL); + if (rp) rp = rp->next; } /* #22b: classify and emit MUST agree (the #10 SSoT * note at TUPLE_GPCAP). The over-cap DECISION rides @@ -13014,29 +13103,42 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) } } int sseidx = 0; + rp = rp0; for (Node *e = n->lhs->list; e; e = e->next) { - int isflt = fld_isfloat(e->type, &f32); + Type *rdu = rp + ? type_chase_named(rp->type) : NULL; + int rdtag = rdu && rdu->kind == TY_TAGGED; + int isflt = !rdtag + && fld_isfloat(e->type, &f32); if (isflt) { cgexpr(c, e, *locals); /* float=X0 */ ins2(c, f32 ? A_MOVSS : A_MOVSD, areg(D_X0), amem(D_BP, fscr + sseidx * 8)); sseidx++; - continue; + } else { + /* scalar=AX; slice/str=AX,BX,CX; tagged + * box from its slot or widened scratch + * (tuple_lit_push_elem) */ + tuple_lit_push_elem(c, locals, e, + rp ? rp->type : NULL); } - /* scalar=AX; slice/str=AX,BX,CX; tagged box - * from its slot (tuple_lit_push_elem) */ - tuple_lit_push_elem(c, *locals, e); + if (rp) rp = rp->next; } for (int i = gptotal - 1; i >= 0; i--) ins1(c, A_POPQ, areg(tuple_rseq[i])); int j = 0; + rp = rp0; 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++; + Type *rdu = rp + ? type_chase_named(rp->type) : NULL; + int rdtag = rdu && rdu->kind == TY_TAGGED; + if (!rdtag && fld_isfloat(e->type, &f32)) { + ins2(c, f32 ? A_MOVSS : A_MOVSD, + amem(D_BP, fscr + j * 8), + areg(tuple_sse_seq[j])); + j++; + } + if (rp) rp = rp->next; } } else if (n->lhs) { cgexpr(c, n->lhs, *locals); @@ -13461,6 +13563,10 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) * SAME producer source the SEND walks. */ int sret_recv = (n->rhs && n->rhs->kind == N_CALL) ? cg_sret_retsize(n->rhs->type) : 0; + /* #64 (filed, rule 7): an N_TUPLE literal rhs rides this + * decl-less cgexpr route, so a declared-TAGGED element's + * concrete rvalue still fills the cursor stamped-keyed + * (silent skew) — the #57 decl wire stops at return/let. */ cgexpr(c, n->rhs, *locals); Type *rt = n->rhs ? n->rhs->type : NULL; Type *ru = (rt && rt->kind == TY_NAMED) ? rt->under : rt; diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 0b07fcf2..a52c52d9 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -20959,7 +20959,7 @@ fn cgexpr(c: *cgen, n: *node) void = { // 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); + cgtuplelittocursor(c, n, nil); return; case: // Default fallback: produce a deterministic AX = 0. Mirrors @@ -32211,7 +32211,17 @@ fn tupstore(c: *cgen, gpcur: i32, ssecur: i32, off: i32, eslot: i32, tn: *node) // (0 GP words); str/slice push their 3-word header; a tagged element // its tupeslot/8 box words; a void element pushes nothing (the // checker's 0-slot); a scalar 1. Mirror of cstage tuple_lit_gpwords. -fn tuplitgpwords(c: *cgen, e: *node) i32 = { +// +// #57: `dtn` is the DECLARED tuple element TYPE node (nil when the +// consumer has none). The N_TUPLE literal's stamped type is +// CONSTRUCTED from its elements, so a concrete rvalue under a +// declared-TAGGED slot counted ONE word here while the receive walks +// the declared eslot — the cursor shifted and every later element +// read garbage. Declared-tagged keys the count on the DECLARED box. +fn tuplitgpwords(c: *cgen, e: *node, dtn: *node) i32 = { + if (dtn != nil) { + if (istaggedtype(c, dtn)) { return tupeslotn(dtn) / 8; }; + }; if (isfloattype(c, e)) { return 0; }; if (nodeisstr(c, e) || nodeisslice(c, e)) { return (tyslicesize() / 8i64): i32; @@ -32229,15 +32239,56 @@ fn tuplitgpwords(c: *cgen, e: *node) i32 = { // INTEGER cursor words L->R (the pop side fills tupreg in reverse). A // tagged element loads its box words straight from its local slot — // cgexpr's ident load is word0-only for tagged (every tagged consumer -// reads memory), so the cursor fill must too; any other tagged source -// shape is loud (rule 7; the cursor-receive arm for call results -// rides the #35 non-ident-source family, widening literals #23). Mirror of -// cstage tuple_lit_push_elem — count (tuplitgpwords) and push live or -// die together. -fn tuplitpushelem(c: *cgen, e: *node) void = { +// reads memory), so the cursor fill must too. Mirror of cstage +// tuple_lit_push_elem — count (tuplitgpwords) and push live or die +// together. +// +// #57: a DECLARED-tagged element whose expr is a concrete rvalue +// (`return (5: size, 9)` — cast, literal, call) skipped the widen +// entirely: the stamped-keyed arm below saw a scalar and pushed ONE +// word, the receiver read the declared box words — silent shift, both +// stages, gate-blind (ken /tmp/ken57). Such an element now widens +// into the shared tagged scratch (cgwidentaggedstore, the cgreturn +// tagged-@retscr shape) and pushes the box words. A tagged->tagged +// SUBSET element (eslot mismatch) needs a tag remap on the way into +// the slot — loud (rule 7, the #23/#40 widening family). +fn tuplitpushelem(c: *cgen, e: *node, dtn: *node) void = { let t: *tinfo = e.type_: *tinfo; for (t != nil && t.kind == tykind.TY_NAMED) { t = t.under; }; - if (t != nil && t.kind == tykind.TY_TAGGED) { + let etagged: bool = false; + if (t != nil) { if (t.kind == tykind.TY_TAGGED) { etagged = true; }; }; + if (dtn != nil) { + if (istaggedtype(c, dtn) && !etagged) { + let eslot: i32 = tupeslotn(dtn); + let scr: i32 = tagscradd(c, eslot); + emitline("\tXORQ\tAX, AX\n"); + let z: i32 = 0; + for (z < eslot) { + emitline("\tMOVQ\tAX, "); + emitoff((scr + z): i64); + emitline("(BP)\n"); + z += 8; + }; + cgwidentaggedstore(c, dtn.type_: *tinfo, e, + "BP", scr, eslot); + let pk: i32 = 0; + for (pk < eslot / 8) { + emitline("\tMOVQ\t"); + emitoff((scr + pk * 8): i64); + emitline("(BP), AX\n"); + emitline("\tPUSHQ\tAX\n"); + pk += 1; + }; + return; + }; + if (istaggedtype(c, dtn) && etagged + && tupeslotn(dtn) != tupeslotn(e)) { + let m57: str = "#57: tagged tuple element widening into a wider declared union slot needs a tag remap (rule 7; the #23/#40 widening family)\n"; + os.write(2, m57.ptr, m57.len: u64); + os.exit(1); + }; + }; + if (etagged) { let eslot: i32 = tupeslotn(e); let eoff: i32 = 0; if (e.kind == nkind.N_IDENT) { eoff = localfind(c, e.str); }; @@ -32275,17 +32326,35 @@ fn tuplitpushelem(c: *cgen, e: *node) void = { // 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 = { +// +// #57: `decl` is the consumer's DECLARED tuple TYPE node (N_TTUPLE, +// nil when it has none — the bare cgexpr route). A declared-TAGGED +// element gates the SSE row off (its payload may be float-stamped but +// the BOX rides INTEGER eightbytes) and keys count + push on the +// declared eslot — see tuplitgpwords / tuplitpushelem. Mirrors cstage +// cg_tuple_lit_to_cursor's decl walk; decl.list nodes wrap the elem +// type in .lhs (the c.fnret.list shape the over-cap arm walks). +fn cgtuplelittocursor(c: *cgen, tuple: *node, decl: *node) void = { + let dp0: *node = nil; + if (decl != nil) { + if (decl.kind == nkind.N_TTUPLE) { dp0 = decl.list; }; + }; let ssecap: i32 = TUPLE_SSECAP; let gptotal: i32 = 0; let ssecount: i32 = 0; + let dp: *node = dp0; let e: *node = tuple.list; for (e != nil) { - if (isfloattype(c, e)) { + let dtn: *node = nil; + if (dp != nil) { dtn = dp.lhs; }; + let dtagged: bool = false; + if (dtn != nil) { dtagged = istaggedtype(c, dtn); }; + if (!dtagged && isfloattype(c, e)) { ssecount = ssecount + 1; } else { - gptotal = gptotal + tuplitgpwords(c, e); + gptotal = gptotal + tuplitgpwords(c, e, dtn); }; + if (dp != nil) { dp = dp.next; }; e = e.next; }; if (gptotal > TUPLE_GPCAP || ssecount > ssecap) { @@ -32298,9 +32367,14 @@ fn cgtuplelittocursor(c: *cgen, tuple: *node) void = { fscr = localadd(c, "@tupfscr", ssecap * 8, nil); }; let sseidx: i32 = 0; + dp = dp0; e = tuple.list; for (e != nil) { - let isflt: bool = isfloattype(c, e); + let dtn: *node = nil; + if (dp != nil) { dtn = dp.lhs; }; + let dtagged: bool = false; + if (dtn != nil) { dtagged = istaggedtype(c, dtn); }; + let isflt: bool = !dtagged && isfloattype(c, e); if (isflt) { cgexpr(c, e); let mov: str = "MOVSD"; @@ -32310,8 +32384,9 @@ fn cgtuplelittocursor(c: *cgen, tuple: *node) void = { emitline("(BP)\n"); sseidx = sseidx + 1; } else { - tuplitpushelem(c, e); + tuplitpushelem(c, e, dtn); }; + if (dp != nil) { dp = dp.next; }; e = e.next; }; let i: i32 = gptotal - 1; @@ -32322,9 +32397,14 @@ fn cgtuplelittocursor(c: *cgen, tuple: *node) void = { i = i - 1; }; let j: i32 = 0; + dp = dp0; e = tuple.list; for (e != nil) { - if (isfloattype(c, e)) { + let dtn: *node = nil; + if (dp != nil) { dtn = dp.lhs; }; + let dtagged: bool = false; + if (dtn != nil) { dtagged = istaggedtype(c, dtn); }; + if (!dtagged && isfloattype(c, e)) { let mov: str = "MOVSD"; if (isf32type(c, e)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\t"); @@ -32334,6 +32414,7 @@ fn cgtuplelittocursor(c: *cgen, tuple: *node) void = { emitline("\n"); j = j + 1; }; + if (dp != nil) { dp = dp.next; }; e = e.next; }; }; @@ -32470,13 +32551,33 @@ fn cgreturn(c: *cgen, n: *node) void = { let ssecap: i32 = TUPLE_SSECAP; // X0,X1 per SysV let gptotal: i32 = 0; let ssecount: i32 = 0; + // #57: count + push key on the DECLARED return-type + // element (c.fnret.list) — the literal's stamped type + // is element-constructed, so a declared-TAGGED + // element's concrete rvalue counted 1 word and skipped + // the widen while the caller's receive walks the + // declared eslot (2 words sent for a 3-word shape; + // ken /tmp/ken57 p8/p9). Same pt walk the over-cap arm + // already does (#240/#22b). Mirrors cstage cgreturn. + let rp0: *node = nil; + if (c.fnret != nil) { + if (c.fnret.kind == nkind.N_TTUPLE) { + rp0 = c.fnret.list; + }; + }; + let rp: *node = rp0; let e: *node = rhs.list; for (e != nil) { - if (isfloattype(c, e)) { + let rdtn: *node = nil; + if (rp != nil) { rdtn = rp.lhs; }; + let rdtag: bool = false; + if (rdtn != nil) { rdtag = istaggedtype(c, rdtn); }; + if (!rdtag && isfloattype(c, e)) { ssecount = ssecount + 1; } else { - gptotal = gptotal + tuplitgpwords(c, e); + gptotal = gptotal + tuplitgpwords(c, e, rdtn); }; + if (rp != nil) { rp = rp.next; }; e = e.next; }; // #22b: classify and emit MUST agree (the #10 SSoT note @@ -32640,9 +32741,14 @@ fn cgreturn(c: *cgen, n: *node) void = { fscr = localadd(c, "@tupfscr", ssecap * 8, nil); }; let sseidx: i32 = 0; + rp = rp0; e = rhs.list; for (e != nil) { - let isflt: bool = isfloattype(c, e); + let rdtn: *node = nil; + if (rp != nil) { rdtn = rp.lhs; }; + let rdtag: bool = false; + if (rdtn != nil) { rdtag = istaggedtype(c, rdtn); }; + let isflt: bool = !rdtag && isfloattype(c, e); if (isflt) { cgexpr(c, e); let mov: str = "MOVSD"; @@ -32655,9 +32761,11 @@ fn cgreturn(c: *cgen, n: *node) void = { sseidx = sseidx + 1; } else { // scalar=AX; slice/str=AX,BX,CX; tagged - // box from its slot (tuplitpushelem) - tuplitpushelem(c, e); + // box from its slot or widened scratch + // (tuplitpushelem) + tuplitpushelem(c, e, rdtn); }; + if (rp != nil) { rp = rp.next; }; e = e.next; }; let i: i32 = gptotal - 1; @@ -32668,9 +32776,14 @@ fn cgreturn(c: *cgen, n: *node) void = { i = i - 1; }; let j: i32 = 0; + rp = rp0; e = rhs.list; for (e != nil) { - if (isfloattype(c, e)) { + let rdtn: *node = nil; + if (rp != nil) { rdtn = rp.lhs; }; + let rdtag: bool = false; + if (rdtn != nil) { rdtag = istaggedtype(c, rdtn); }; + if (!rdtag && isfloattype(c, e)) { let mov: str = "MOVSD"; if (isf32type(c, e)) { mov = "MOVSS"; }; emitline("\t"); @@ -32682,6 +32795,7 @@ fn cgreturn(c: *cgen, n: *node) void = { emitline("\n"); j = j + 1; }; + if (rp != nil) { rp = rp.next; }; e = e.next; }; emitline("\tMOVQ\tBP, SP\n"); @@ -34098,7 +34212,19 @@ fn cglet(c: *cgen, n: *node) void = { if (ttup != nil) { if (ttup.kind == nkind.N_TTUPLE && sretretsize(c, ttup) == 0) { - cgexpr(c, rhs); + // #57: a tuple LITERAL rhs carries the DECLARED + // type into the cursor fill — its stamped type + // is element-constructed, so a declared-tagged + // element's concrete rvalue skipped the widen + // and the fill/receive cursor walks skewed + // (let-twin of the return-position bug; probe + // /tmp/p57/q1_let). Same emission as the cgexpr + // route for every declared-tagged-free literal. + if (rhs.kind == nkind.N_TUPLE) { + cgtuplelittocursor(c, rhs, ttup); + } else { + cgexpr(c, rhs); + }; let gpcur: i32 = 0; let ssecur: i32 = 0; let eoff: i32 = 0; @@ -34918,6 +35044,10 @@ fn cgmassign(c: *cgen, n: *node) void = { }; }; + // #64 (filed, rule 7): an N_TUPLE literal rhs rides this decl-less + // cgexpr route, so a declared-TAGGED element's concrete rvalue + // still fills the cursor stamped-keyed (silent skew) — the #57 + // decl wire stops at return/let. if (n.rhs != nil) { cgexpr(c, n.rhs); }; if (sretrecv > 0) { diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 31a1842c..fc01976f 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -115,7 +115,7 @@ fn cgexpr(c: *cgen, n: *node) void = { // 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); + cgtuplelittocursor(c, n, nil); return; case: // Default fallback: produce a deterministic AX = 0. Mirrors diff --git a/selfhost/cmd/wcc/cgenstmt.ww b/selfhost/cmd/wcc/cgenstmt.ww index 72b76dbe..fb49d3a7 100644 --- a/selfhost/cmd/wcc/cgenstmt.ww +++ b/selfhost/cmd/wcc/cgenstmt.ww @@ -306,7 +306,17 @@ fn tupstore(c: *cgen, gpcur: i32, ssecur: i32, off: i32, eslot: i32, tn: *node) // (0 GP words); str/slice push their 3-word header; a tagged element // its tupeslot/8 box words; a void element pushes nothing (the // checker's 0-slot); a scalar 1. Mirror of cstage tuple_lit_gpwords. -fn tuplitgpwords(c: *cgen, e: *node) i32 = { +// +// #57: `dtn` is the DECLARED tuple element TYPE node (nil when the +// consumer has none). The N_TUPLE literal's stamped type is +// CONSTRUCTED from its elements, so a concrete rvalue under a +// declared-TAGGED slot counted ONE word here while the receive walks +// the declared eslot — the cursor shifted and every later element +// read garbage. Declared-tagged keys the count on the DECLARED box. +fn tuplitgpwords(c: *cgen, e: *node, dtn: *node) i32 = { + if (dtn != nil) { + if (istaggedtype(c, dtn)) { return tupeslotn(dtn) / 8; }; + }; if (isfloattype(c, e)) { return 0; }; if (nodeisstr(c, e) || nodeisslice(c, e)) { return (tyslicesize() / 8i64): i32; @@ -324,15 +334,56 @@ fn tuplitgpwords(c: *cgen, e: *node) i32 = { // INTEGER cursor words L->R (the pop side fills tupreg in reverse). A // tagged element loads its box words straight from its local slot — // cgexpr's ident load is word0-only for tagged (every tagged consumer -// reads memory), so the cursor fill must too; any other tagged source -// shape is loud (rule 7; the cursor-receive arm for call results -// rides the #35 non-ident-source family, widening literals #23). Mirror of -// cstage tuple_lit_push_elem — count (tuplitgpwords) and push live or -// die together. -fn tuplitpushelem(c: *cgen, e: *node) void = { +// reads memory), so the cursor fill must too. Mirror of cstage +// tuple_lit_push_elem — count (tuplitgpwords) and push live or die +// together. +// +// #57: a DECLARED-tagged element whose expr is a concrete rvalue +// (`return (5: size, 9)` — cast, literal, call) skipped the widen +// entirely: the stamped-keyed arm below saw a scalar and pushed ONE +// word, the receiver read the declared box words — silent shift, both +// stages, gate-blind (ken /tmp/ken57). Such an element now widens +// into the shared tagged scratch (cgwidentaggedstore, the cgreturn +// tagged-@retscr shape) and pushes the box words. A tagged->tagged +// SUBSET element (eslot mismatch) needs a tag remap on the way into +// the slot — loud (rule 7, the #23/#40 widening family). +fn tuplitpushelem(c: *cgen, e: *node, dtn: *node) void = { let t: *tinfo = e.type_: *tinfo; for (t != nil && t.kind == tykind.TY_NAMED) { t = t.under; }; - if (t != nil && t.kind == tykind.TY_TAGGED) { + let etagged: bool = false; + if (t != nil) { if (t.kind == tykind.TY_TAGGED) { etagged = true; }; }; + if (dtn != nil) { + if (istaggedtype(c, dtn) && !etagged) { + let eslot: i32 = tupeslotn(dtn); + let scr: i32 = tagscradd(c, eslot); + emitline("\tXORQ\tAX, AX\n"); + let z: i32 = 0; + for (z < eslot) { + emitline("\tMOVQ\tAX, "); + emitoff((scr + z): i64); + emitline("(BP)\n"); + z += 8; + }; + cgwidentaggedstore(c, dtn.type_: *tinfo, e, + "BP", scr, eslot); + let pk: i32 = 0; + for (pk < eslot / 8) { + emitline("\tMOVQ\t"); + emitoff((scr + pk * 8): i64); + emitline("(BP), AX\n"); + emitline("\tPUSHQ\tAX\n"); + pk += 1; + }; + return; + }; + if (istaggedtype(c, dtn) && etagged + && tupeslotn(dtn) != tupeslotn(e)) { + let m57: str = "#57: tagged tuple element widening into a wider declared union slot needs a tag remap (rule 7; the #23/#40 widening family)\n"; + os.write(2, m57.ptr, m57.len: u64); + os.exit(1); + }; + }; + if (etagged) { let eslot: i32 = tupeslotn(e); let eoff: i32 = 0; if (e.kind == nkind.N_IDENT) { eoff = localfind(c, e.str); }; @@ -370,17 +421,35 @@ fn tuplitpushelem(c: *cgen, e: *node) void = { // 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 = { +// +// #57: `decl` is the consumer's DECLARED tuple TYPE node (N_TTUPLE, +// nil when it has none — the bare cgexpr route). A declared-TAGGED +// element gates the SSE row off (its payload may be float-stamped but +// the BOX rides INTEGER eightbytes) and keys count + push on the +// declared eslot — see tuplitgpwords / tuplitpushelem. Mirrors cstage +// cg_tuple_lit_to_cursor's decl walk; decl.list nodes wrap the elem +// type in .lhs (the c.fnret.list shape the over-cap arm walks). +fn cgtuplelittocursor(c: *cgen, tuple: *node, decl: *node) void = { + let dp0: *node = nil; + if (decl != nil) { + if (decl.kind == nkind.N_TTUPLE) { dp0 = decl.list; }; + }; let ssecap: i32 = TUPLE_SSECAP; let gptotal: i32 = 0; let ssecount: i32 = 0; + let dp: *node = dp0; let e: *node = tuple.list; for (e != nil) { - if (isfloattype(c, e)) { + let dtn: *node = nil; + if (dp != nil) { dtn = dp.lhs; }; + let dtagged: bool = false; + if (dtn != nil) { dtagged = istaggedtype(c, dtn); }; + if (!dtagged && isfloattype(c, e)) { ssecount = ssecount + 1; } else { - gptotal = gptotal + tuplitgpwords(c, e); + gptotal = gptotal + tuplitgpwords(c, e, dtn); }; + if (dp != nil) { dp = dp.next; }; e = e.next; }; if (gptotal > TUPLE_GPCAP || ssecount > ssecap) { @@ -393,9 +462,14 @@ fn cgtuplelittocursor(c: *cgen, tuple: *node) void = { fscr = localadd(c, "@tupfscr", ssecap * 8, nil); }; let sseidx: i32 = 0; + dp = dp0; e = tuple.list; for (e != nil) { - let isflt: bool = isfloattype(c, e); + let dtn: *node = nil; + if (dp != nil) { dtn = dp.lhs; }; + let dtagged: bool = false; + if (dtn != nil) { dtagged = istaggedtype(c, dtn); }; + let isflt: bool = !dtagged && isfloattype(c, e); if (isflt) { cgexpr(c, e); let mov: str = "MOVSD"; @@ -405,8 +479,9 @@ fn cgtuplelittocursor(c: *cgen, tuple: *node) void = { emitline("(BP)\n"); sseidx = sseidx + 1; } else { - tuplitpushelem(c, e); + tuplitpushelem(c, e, dtn); }; + if (dp != nil) { dp = dp.next; }; e = e.next; }; let i: i32 = gptotal - 1; @@ -417,9 +492,14 @@ fn cgtuplelittocursor(c: *cgen, tuple: *node) void = { i = i - 1; }; let j: i32 = 0; + dp = dp0; e = tuple.list; for (e != nil) { - if (isfloattype(c, e)) { + let dtn: *node = nil; + if (dp != nil) { dtn = dp.lhs; }; + let dtagged: bool = false; + if (dtn != nil) { dtagged = istaggedtype(c, dtn); }; + if (!dtagged && isfloattype(c, e)) { let mov: str = "MOVSD"; if (isf32type(c, e)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\t"); @@ -429,6 +509,7 @@ fn cgtuplelittocursor(c: *cgen, tuple: *node) void = { emitline("\n"); j = j + 1; }; + if (dp != nil) { dp = dp.next; }; e = e.next; }; }; @@ -565,13 +646,33 @@ fn cgreturn(c: *cgen, n: *node) void = { let ssecap: i32 = TUPLE_SSECAP; // X0,X1 per SysV let gptotal: i32 = 0; let ssecount: i32 = 0; + // #57: count + push key on the DECLARED return-type + // element (c.fnret.list) — the literal's stamped type + // is element-constructed, so a declared-TAGGED + // element's concrete rvalue counted 1 word and skipped + // the widen while the caller's receive walks the + // declared eslot (2 words sent for a 3-word shape; + // ken /tmp/ken57 p8/p9). Same pt walk the over-cap arm + // already does (#240/#22b). Mirrors cstage cgreturn. + let rp0: *node = nil; + if (c.fnret != nil) { + if (c.fnret.kind == nkind.N_TTUPLE) { + rp0 = c.fnret.list; + }; + }; + let rp: *node = rp0; let e: *node = rhs.list; for (e != nil) { - if (isfloattype(c, e)) { + let rdtn: *node = nil; + if (rp != nil) { rdtn = rp.lhs; }; + let rdtag: bool = false; + if (rdtn != nil) { rdtag = istaggedtype(c, rdtn); }; + if (!rdtag && isfloattype(c, e)) { ssecount = ssecount + 1; } else { - gptotal = gptotal + tuplitgpwords(c, e); + gptotal = gptotal + tuplitgpwords(c, e, rdtn); }; + if (rp != nil) { rp = rp.next; }; e = e.next; }; // #22b: classify and emit MUST agree (the #10 SSoT note @@ -735,9 +836,14 @@ fn cgreturn(c: *cgen, n: *node) void = { fscr = localadd(c, "@tupfscr", ssecap * 8, nil); }; let sseidx: i32 = 0; + rp = rp0; e = rhs.list; for (e != nil) { - let isflt: bool = isfloattype(c, e); + let rdtn: *node = nil; + if (rp != nil) { rdtn = rp.lhs; }; + let rdtag: bool = false; + if (rdtn != nil) { rdtag = istaggedtype(c, rdtn); }; + let isflt: bool = !rdtag && isfloattype(c, e); if (isflt) { cgexpr(c, e); let mov: str = "MOVSD"; @@ -750,9 +856,11 @@ fn cgreturn(c: *cgen, n: *node) void = { sseidx = sseidx + 1; } else { // scalar=AX; slice/str=AX,BX,CX; tagged - // box from its slot (tuplitpushelem) - tuplitpushelem(c, e); + // box from its slot or widened scratch + // (tuplitpushelem) + tuplitpushelem(c, e, rdtn); }; + if (rp != nil) { rp = rp.next; }; e = e.next; }; let i: i32 = gptotal - 1; @@ -763,9 +871,14 @@ fn cgreturn(c: *cgen, n: *node) void = { i = i - 1; }; let j: i32 = 0; + rp = rp0; e = rhs.list; for (e != nil) { - if (isfloattype(c, e)) { + let rdtn: *node = nil; + if (rp != nil) { rdtn = rp.lhs; }; + let rdtag: bool = false; + if (rdtn != nil) { rdtag = istaggedtype(c, rdtn); }; + if (!rdtag && isfloattype(c, e)) { let mov: str = "MOVSD"; if (isf32type(c, e)) { mov = "MOVSS"; }; emitline("\t"); @@ -777,6 +890,7 @@ fn cgreturn(c: *cgen, n: *node) void = { emitline("\n"); j = j + 1; }; + if (rp != nil) { rp = rp.next; }; e = e.next; }; emitline("\tMOVQ\tBP, SP\n"); @@ -2193,7 +2307,19 @@ fn cglet(c: *cgen, n: *node) void = { if (ttup != nil) { if (ttup.kind == nkind.N_TTUPLE && sretretsize(c, ttup) == 0) { - cgexpr(c, rhs); + // #57: a tuple LITERAL rhs carries the DECLARED + // type into the cursor fill — its stamped type + // is element-constructed, so a declared-tagged + // element's concrete rvalue skipped the widen + // and the fill/receive cursor walks skewed + // (let-twin of the return-position bug; probe + // /tmp/p57/q1_let). Same emission as the cgexpr + // route for every declared-tagged-free literal. + if (rhs.kind == nkind.N_TUPLE) { + cgtuplelittocursor(c, rhs, ttup); + } else { + cgexpr(c, rhs); + }; let gpcur: i32 = 0; let ssecur: i32 = 0; let eoff: i32 = 0; @@ -3013,6 +3139,10 @@ fn cgmassign(c: *cgen, n: *node) void = { }; }; + // #64 (filed, rule 7): an N_TUPLE literal rhs rides this decl-less + // cgexpr route, so a declared-TAGGED element's concrete rvalue + // still fills the cursor stamped-keyed (silent skew) — the #57 + // decl wire stops at return/let. if (n.rhs != nil) { cgexpr(c, n.rhs); }; if (sretrecv > 0) { diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 17f54be9..ba587b81 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -20959,7 +20959,7 @@ fn cgexpr(c: *cgen, n: *node) void = { // 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); + cgtuplelittocursor(c, n, nil); return; case: // Default fallback: produce a deterministic AX = 0. Mirrors @@ -32211,7 +32211,17 @@ fn tupstore(c: *cgen, gpcur: i32, ssecur: i32, off: i32, eslot: i32, tn: *node) // (0 GP words); str/slice push their 3-word header; a tagged element // its tupeslot/8 box words; a void element pushes nothing (the // checker's 0-slot); a scalar 1. Mirror of cstage tuple_lit_gpwords. -fn tuplitgpwords(c: *cgen, e: *node) i32 = { +// +// #57: `dtn` is the DECLARED tuple element TYPE node (nil when the +// consumer has none). The N_TUPLE literal's stamped type is +// CONSTRUCTED from its elements, so a concrete rvalue under a +// declared-TAGGED slot counted ONE word here while the receive walks +// the declared eslot — the cursor shifted and every later element +// read garbage. Declared-tagged keys the count on the DECLARED box. +fn tuplitgpwords(c: *cgen, e: *node, dtn: *node) i32 = { + if (dtn != nil) { + if (istaggedtype(c, dtn)) { return tupeslotn(dtn) / 8; }; + }; if (isfloattype(c, e)) { return 0; }; if (nodeisstr(c, e) || nodeisslice(c, e)) { return (tyslicesize() / 8i64): i32; @@ -32229,15 +32239,56 @@ fn tuplitgpwords(c: *cgen, e: *node) i32 = { // INTEGER cursor words L->R (the pop side fills tupreg in reverse). A // tagged element loads its box words straight from its local slot — // cgexpr's ident load is word0-only for tagged (every tagged consumer -// reads memory), so the cursor fill must too; any other tagged source -// shape is loud (rule 7; the cursor-receive arm for call results -// rides the #35 non-ident-source family, widening literals #23). Mirror of -// cstage tuple_lit_push_elem — count (tuplitgpwords) and push live or -// die together. -fn tuplitpushelem(c: *cgen, e: *node) void = { +// reads memory), so the cursor fill must too. Mirror of cstage +// tuple_lit_push_elem — count (tuplitgpwords) and push live or die +// together. +// +// #57: a DECLARED-tagged element whose expr is a concrete rvalue +// (`return (5: size, 9)` — cast, literal, call) skipped the widen +// entirely: the stamped-keyed arm below saw a scalar and pushed ONE +// word, the receiver read the declared box words — silent shift, both +// stages, gate-blind (ken /tmp/ken57). Such an element now widens +// into the shared tagged scratch (cgwidentaggedstore, the cgreturn +// tagged-@retscr shape) and pushes the box words. A tagged->tagged +// SUBSET element (eslot mismatch) needs a tag remap on the way into +// the slot — loud (rule 7, the #23/#40 widening family). +fn tuplitpushelem(c: *cgen, e: *node, dtn: *node) void = { let t: *tinfo = e.type_: *tinfo; for (t != nil && t.kind == tykind.TY_NAMED) { t = t.under; }; - if (t != nil && t.kind == tykind.TY_TAGGED) { + let etagged: bool = false; + if (t != nil) { if (t.kind == tykind.TY_TAGGED) { etagged = true; }; }; + if (dtn != nil) { + if (istaggedtype(c, dtn) && !etagged) { + let eslot: i32 = tupeslotn(dtn); + let scr: i32 = tagscradd(c, eslot); + emitline("\tXORQ\tAX, AX\n"); + let z: i32 = 0; + for (z < eslot) { + emitline("\tMOVQ\tAX, "); + emitoff((scr + z): i64); + emitline("(BP)\n"); + z += 8; + }; + cgwidentaggedstore(c, dtn.type_: *tinfo, e, + "BP", scr, eslot); + let pk: i32 = 0; + for (pk < eslot / 8) { + emitline("\tMOVQ\t"); + emitoff((scr + pk * 8): i64); + emitline("(BP), AX\n"); + emitline("\tPUSHQ\tAX\n"); + pk += 1; + }; + return; + }; + if (istaggedtype(c, dtn) && etagged + && tupeslotn(dtn) != tupeslotn(e)) { + let m57: str = "#57: tagged tuple element widening into a wider declared union slot needs a tag remap (rule 7; the #23/#40 widening family)\n"; + os.write(2, m57.ptr, m57.len: u64); + os.exit(1); + }; + }; + if (etagged) { let eslot: i32 = tupeslotn(e); let eoff: i32 = 0; if (e.kind == nkind.N_IDENT) { eoff = localfind(c, e.str); }; @@ -32275,17 +32326,35 @@ fn tuplitpushelem(c: *cgen, e: *node) void = { // 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 = { +// +// #57: `decl` is the consumer's DECLARED tuple TYPE node (N_TTUPLE, +// nil when it has none — the bare cgexpr route). A declared-TAGGED +// element gates the SSE row off (its payload may be float-stamped but +// the BOX rides INTEGER eightbytes) and keys count + push on the +// declared eslot — see tuplitgpwords / tuplitpushelem. Mirrors cstage +// cg_tuple_lit_to_cursor's decl walk; decl.list nodes wrap the elem +// type in .lhs (the c.fnret.list shape the over-cap arm walks). +fn cgtuplelittocursor(c: *cgen, tuple: *node, decl: *node) void = { + let dp0: *node = nil; + if (decl != nil) { + if (decl.kind == nkind.N_TTUPLE) { dp0 = decl.list; }; + }; let ssecap: i32 = TUPLE_SSECAP; let gptotal: i32 = 0; let ssecount: i32 = 0; + let dp: *node = dp0; let e: *node = tuple.list; for (e != nil) { - if (isfloattype(c, e)) { + let dtn: *node = nil; + if (dp != nil) { dtn = dp.lhs; }; + let dtagged: bool = false; + if (dtn != nil) { dtagged = istaggedtype(c, dtn); }; + if (!dtagged && isfloattype(c, e)) { ssecount = ssecount + 1; } else { - gptotal = gptotal + tuplitgpwords(c, e); + gptotal = gptotal + tuplitgpwords(c, e, dtn); }; + if (dp != nil) { dp = dp.next; }; e = e.next; }; if (gptotal > TUPLE_GPCAP || ssecount > ssecap) { @@ -32298,9 +32367,14 @@ fn cgtuplelittocursor(c: *cgen, tuple: *node) void = { fscr = localadd(c, "@tupfscr", ssecap * 8, nil); }; let sseidx: i32 = 0; + dp = dp0; e = tuple.list; for (e != nil) { - let isflt: bool = isfloattype(c, e); + let dtn: *node = nil; + if (dp != nil) { dtn = dp.lhs; }; + let dtagged: bool = false; + if (dtn != nil) { dtagged = istaggedtype(c, dtn); }; + let isflt: bool = !dtagged && isfloattype(c, e); if (isflt) { cgexpr(c, e); let mov: str = "MOVSD"; @@ -32310,8 +32384,9 @@ fn cgtuplelittocursor(c: *cgen, tuple: *node) void = { emitline("(BP)\n"); sseidx = sseidx + 1; } else { - tuplitpushelem(c, e); + tuplitpushelem(c, e, dtn); }; + if (dp != nil) { dp = dp.next; }; e = e.next; }; let i: i32 = gptotal - 1; @@ -32322,9 +32397,14 @@ fn cgtuplelittocursor(c: *cgen, tuple: *node) void = { i = i - 1; }; let j: i32 = 0; + dp = dp0; e = tuple.list; for (e != nil) { - if (isfloattype(c, e)) { + let dtn: *node = nil; + if (dp != nil) { dtn = dp.lhs; }; + let dtagged: bool = false; + if (dtn != nil) { dtagged = istaggedtype(c, dtn); }; + if (!dtagged && isfloattype(c, e)) { let mov: str = "MOVSD"; if (isf32type(c, e)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\t"); @@ -32334,6 +32414,7 @@ fn cgtuplelittocursor(c: *cgen, tuple: *node) void = { emitline("\n"); j = j + 1; }; + if (dp != nil) { dp = dp.next; }; e = e.next; }; }; @@ -32470,13 +32551,33 @@ fn cgreturn(c: *cgen, n: *node) void = { let ssecap: i32 = TUPLE_SSECAP; // X0,X1 per SysV let gptotal: i32 = 0; let ssecount: i32 = 0; + // #57: count + push key on the DECLARED return-type + // element (c.fnret.list) — the literal's stamped type + // is element-constructed, so a declared-TAGGED + // element's concrete rvalue counted 1 word and skipped + // the widen while the caller's receive walks the + // declared eslot (2 words sent for a 3-word shape; + // ken /tmp/ken57 p8/p9). Same pt walk the over-cap arm + // already does (#240/#22b). Mirrors cstage cgreturn. + let rp0: *node = nil; + if (c.fnret != nil) { + if (c.fnret.kind == nkind.N_TTUPLE) { + rp0 = c.fnret.list; + }; + }; + let rp: *node = rp0; let e: *node = rhs.list; for (e != nil) { - if (isfloattype(c, e)) { + let rdtn: *node = nil; + if (rp != nil) { rdtn = rp.lhs; }; + let rdtag: bool = false; + if (rdtn != nil) { rdtag = istaggedtype(c, rdtn); }; + if (!rdtag && isfloattype(c, e)) { ssecount = ssecount + 1; } else { - gptotal = gptotal + tuplitgpwords(c, e); + gptotal = gptotal + tuplitgpwords(c, e, rdtn); }; + if (rp != nil) { rp = rp.next; }; e = e.next; }; // #22b: classify and emit MUST agree (the #10 SSoT note @@ -32640,9 +32741,14 @@ fn cgreturn(c: *cgen, n: *node) void = { fscr = localadd(c, "@tupfscr", ssecap * 8, nil); }; let sseidx: i32 = 0; + rp = rp0; e = rhs.list; for (e != nil) { - let isflt: bool = isfloattype(c, e); + let rdtn: *node = nil; + if (rp != nil) { rdtn = rp.lhs; }; + let rdtag: bool = false; + if (rdtn != nil) { rdtag = istaggedtype(c, rdtn); }; + let isflt: bool = !rdtag && isfloattype(c, e); if (isflt) { cgexpr(c, e); let mov: str = "MOVSD"; @@ -32655,9 +32761,11 @@ fn cgreturn(c: *cgen, n: *node) void = { sseidx = sseidx + 1; } else { // scalar=AX; slice/str=AX,BX,CX; tagged - // box from its slot (tuplitpushelem) - tuplitpushelem(c, e); + // box from its slot or widened scratch + // (tuplitpushelem) + tuplitpushelem(c, e, rdtn); }; + if (rp != nil) { rp = rp.next; }; e = e.next; }; let i: i32 = gptotal - 1; @@ -32668,9 +32776,14 @@ fn cgreturn(c: *cgen, n: *node) void = { i = i - 1; }; let j: i32 = 0; + rp = rp0; e = rhs.list; for (e != nil) { - if (isfloattype(c, e)) { + let rdtn: *node = nil; + if (rp != nil) { rdtn = rp.lhs; }; + let rdtag: bool = false; + if (rdtn != nil) { rdtag = istaggedtype(c, rdtn); }; + if (!rdtag && isfloattype(c, e)) { let mov: str = "MOVSD"; if (isf32type(c, e)) { mov = "MOVSS"; }; emitline("\t"); @@ -32682,6 +32795,7 @@ fn cgreturn(c: *cgen, n: *node) void = { emitline("\n"); j = j + 1; }; + if (rp != nil) { rp = rp.next; }; e = e.next; }; emitline("\tMOVQ\tBP, SP\n"); @@ -34098,7 +34212,19 @@ fn cglet(c: *cgen, n: *node) void = { if (ttup != nil) { if (ttup.kind == nkind.N_TTUPLE && sretretsize(c, ttup) == 0) { - cgexpr(c, rhs); + // #57: a tuple LITERAL rhs carries the DECLARED + // type into the cursor fill — its stamped type + // is element-constructed, so a declared-tagged + // element's concrete rvalue skipped the widen + // and the fill/receive cursor walks skewed + // (let-twin of the return-position bug; probe + // /tmp/p57/q1_let). Same emission as the cgexpr + // route for every declared-tagged-free literal. + if (rhs.kind == nkind.N_TUPLE) { + cgtuplelittocursor(c, rhs, ttup); + } else { + cgexpr(c, rhs); + }; let gpcur: i32 = 0; let ssecur: i32 = 0; let eoff: i32 = 0; @@ -34918,6 +35044,10 @@ fn cgmassign(c: *cgen, n: *node) void = { }; }; + // #64 (filed, rule 7): an N_TUPLE literal rhs rides this decl-less + // cgexpr route, so a declared-TAGGED element's concrete rvalue + // still fills the cursor stamped-keyed (silent skew) — the #57 + // decl wire stops at return/let. if (n.rhs != nil) { cgexpr(c, n.rhs); }; if (sretrecv > 0) { diff --git a/test/wcc/941_tuple_slot_layout_run.c b/test/wcc/941_tuple_slot_layout_run.c index 44ead195..37d13b43 100644 --- a/test/wcc/941_tuple_slot_layout_run.c +++ b/test/wcc/941_tuple_slot_layout_run.c @@ -1925,6 +1925,201 @@ static const struct row rows[] = { " if (t.1 != 36) { return 1; };\n" " return 0;\n" "};\n", 0, K_RUN, NULL }, + /* #57: an IN-CAP tuple literal's cursor fill keyed each element on + * its STAMPED type (element-constructed, check.c N_TUPLE), so a + * declared-TAGGED element from a concrete rvalue (`(5: size, 9)`) + * counted ONE word and skipped the widen — 2 words sent against the + * receiver's declared 3-word walk; every later element read garbage + * (both stages, byte-id, gate-blind; ken /tmp/ken57). Rows: the + * return position (named + inline union spelling, t.N + destructure + * readback), the let-literal twin, tagged SECOND element (shift + * direction), float payload (the SSE-row gate: a (void|f64) box + * rides INTEGER eightbytes), bare-untyped payload (rides the #33 + * chooser through the new widen wire), the ident no-regress anchor, + * and the still-loud CALL-elem tripwire (#41). */ + { "t57_ret_cast_elem", + "package main;\n" + "type un16 = (void | size);\n" + "fn mk() (un16, size) = {\n" + " return (5: size, 9);\n" + "};\n" + "export fn main() i32 = {\n" + " let t = mk();\n" + " if (t.1 != 9) { return 1; };\n" + " let (a, b) = mk();\n" + " if (!(a is size)) { return 2; };\n" + " if (a as size != 5) { return 3; };\n" + " if (b != 9) { return 4; };\n" + " return 0;\n" + "};\n", 0, K_RUN, NULL }, + { "t57_ret_inline_union", + "package main;\n" + "fn mk() ((void | size), size) = {\n" + " return (5: size, 9);\n" + "};\n" + "export fn main() i32 = {\n" + " let (a, b) = mk();\n" + " if (!(a is size)) { return 1; };\n" + " if (b != 9) { return 2; };\n" + " if (a as size != 5) { return 3; };\n" + " return 0;\n" + "};\n", 0, K_RUN, NULL }, + { "t57_let_literal", + "package main;\n" + "type un16 = (void | size);\n" + "export fn main() i32 = {\n" + " let t: (un16, size) = (5: size, 9);\n" + " if (t.1 != 9) { return 1; };\n" + " if (!(t.0 is size)) { return 2; };\n" + " if (t.0 as size != 5) { return 3; };\n" + " return 0;\n" + "};\n", 0, K_RUN, NULL }, + { "t57_tagged_second_elem", + "package main;\n" + "type un16 = (void | size);\n" + "fn mk2() (size, un16) = {\n" + " return (3, 7: size);\n" + "};\n" + "export fn main() i32 = {\n" + " let (a, b) = mk2();\n" + " if (a != 3) { return 1; };\n" + " if (!(b is size)) { return 2; };\n" + " if (b as size != 7) { return 3; };\n" + " return 0;\n" + "};\n", 0, K_RUN, NULL }, + { "t57_float_payload", + "package main;\n" + "type unf = (void | f64);\n" + "fn mkf() (unf, size) = {\n" + " return (1.5, 9);\n" + "};\n" + "export fn main() i32 = {\n" + " let (a, b) = mkf();\n" + " if (b != 9) { return 1; };\n" + " if (!(a is f64)) { return 2; };\n" + " if (a as f64 != 1.5) { return 3; };\n" + " return 0;\n" + "};\n", 0, K_RUN, NULL }, + /* ken's adversarial shapes (k57a/b/c, /tmp/ken57) as rows: the + * tagged-MID elem shifts the cursor on BOTH sides of the box; two + * tagged rvalue elems (incl. a VOID rvalue) reuse the shared scratch + * sequentially; a true SSE elem (plain f64) coexisting with a + * declared-tagged box pins the gate splitting the rows. */ + { "t57_tagged_mid_elem", + "package main;\n" + "type un = (void | size);\n" + "fn mk3() (size, un, size) = {\n" + " return (1, 5: size, 9);\n" + "};\n" + "export fn main() i32 = {\n" + " let (a, b, c) = mk3();\n" + " if (a != 1) { return 1; };\n" + " if (!(b is size)) { return 2; };\n" + " if (b as size != 5) { return 3; };\n" + " if (c != 9) { return 4; };\n" + " return 0;\n" + "};\n", 0, K_RUN, NULL }, + { "t57_two_tagged_void_elem", + "package main;\n" + "type un = (void | size);\n" + "fn mk() (un, un) = {\n" + " return (5: size, void);\n" + "};\n" + "export fn main() i32 = {\n" + " let t = mk();\n" + " if (!(t.0 is size)) { return 1; };\n" + " if (t.0 as size != 5) { return 2; };\n" + " if (!(t.1 is void)) { return 3; };\n" + " return 0;\n" + "};\n", 0, K_RUN, NULL }, + { "t57_sse_coexist", + "package main;\n" + "type un = (void | size);\n" + "fn mk() (f64, un) = {\n" + " return (0.5, 7: size);\n" + "};\n" + "export fn main() i32 = {\n" + " let (x, e) = mk();\n" + " if (x * 4.0 != 2.0) { return 1; };\n" + " if (!(e is size)) { return 2; };\n" + " if (e as size != 7) { return 3; };\n" + " return 0;\n" + "};\n", 0, K_RUN, NULL }, + { "t57_bare_untyped_payload", + "package main;\n" + "type un16 = (void | size);\n" + "fn mkb() (un16, size) = {\n" + " return (5, 9);\n" + "};\n" + "export fn main() i32 = {\n" + " let (a, b) = mkb();\n" + " if (!(a is size)) { return 1; };\n" + " if (a as size != 5) { return 2; };\n" + " if (b != 9) { return 3; };\n" + " return 0;\n" + "};\n", 0, K_RUN, NULL }, + { "t57_ident_no_regress", + "package main;\n" + "type un16 = (void | size);\n" + "fn mk() (un16, size) = {\n" + " let e: un16 = 5: size;\n" + " return (e, 9);\n" + "};\n" + "export fn main() i32 = {\n" + " let t = mk();\n" + " if (t.1 != 9) { return 1; };\n" + " if (!(t.0 is size)) { return 2; };\n" + " if (t.0 as size != 5) { return 3; };\n" + " return 0;\n" + "};\n", 0, K_RUN, NULL }, + { "t57_loud_call_elem", + "package main;\n" + "type un16 = (void | size);\n" + "fn mke() un16 = {\n" + " return 5: size;\n" + "};\n" + "export fn main() i32 = {\n" + " let t: (un16, size) = (mke(), 9);\n" + " if (t.1 != 9) { return 1; };\n" + " return 0;\n" + "};\n", 0, + K_BUILDERR, "tagged tuple element from a non-local source shape unwired" }, + /* the NEW #57 loud: an in-cap tagged->tagged SUBSET elem (stamped + * eslot 16 into declared eslot 24) needs a tag remap on the way into + * the slot — pinned loud both stages until the #23/#40 widening + * family wires it. (big,size) = 3+1 GP words, in-cap by GPCAP. */ + { "t57_loud_remap", + "package main;\n" + "type st16 = struct { a: size, b: size };\n" + "type small = (void | u8);\n" + "type big = (void | u8 | st16);\n" + "fn mk() (big, size) = {\n" + " let s: small = 5u8;\n" + " return (s, 9);\n" + "};\n" + "export fn main() i32 = {\n" + " let t = mk();\n" + " if (t.1 != 9) { return 1; };\n" + " return 0;\n" + "};\n", 0, + K_BUILDERR, "widening into a wider declared union slot needs a tag remap" }, + /* ken k57d: the in-cap/over-cap BOUNDARY — an over-cap + * (un,un,size,size) return with concrete rvalue tagged elems must + * route to the sret arm's #22b ident-only loud-stop, not slip into + * the (now declared-keyed) in-cap path: pins that the declared + * count and the over-cap classify (cg_sret_retsize) agree. */ + { "t57_loud_overcap_boundary", + "package main;\n" + "type un = (void | size);\n" + "fn mk() (un, un, size, size) = {\n" + " return (5: size, void, 8, 9);\n" + "};\n" + "export fn main() i32 = {\n" + " let t = mk();\n" + " if (t.2 != 8) { return 1; };\n" + " return 0;\n" + "};\n", 0, + K_BUILDERR, "tagged element in an over-cap (sret) tuple return" }, }; /* build+run via a driver (ww / ww_ww); returns 0 pass, nonzero fail. */