diff --git a/cmd/w6c/cgen.c b/cmd/w6c/cgen.c index 33498190..8e34002c 100644 --- a/cmd/w6c/cgen.c +++ b/cmd/w6c/cgen.c @@ -197,7 +197,7 @@ type_chase_named(Type *t) } /* cg_sret_retsize — sret classifier; defined after the tuple register- - * return helpers (tuple_rseq / tuple_ebytes / fld_isfloat) it consults + * return helpers (tuple_rseq / tuple_eslot / fld_isfloat) it consults * for the over-cap-tuple arm. Forward-declared here for the earlier * callers (cgcall, fn prologue). Task #23 / #10. */ static int cg_sret_retsize(Type *rt); @@ -286,10 +286,32 @@ static const int tuple_sse_seq[] = { D_X0, D_X1 }; #define TUPLE_GPCAP ((int)nelem(tuple_rseq)) #define TUPLE_SSECAP ((int)nelem(tuple_sse_seq)) +/* tuple_eslot — THE tuple element-stride accessor (#22): the slot a + * tuple element occupies, in bytes. slot = roundup8(size(elem)), 8B a + * FLOOR not a ceiling (user-ratified 2026-06-04): str/slice carry + * their 24B header, a tagged element its full tag+payload box + * ((str,str)=48B predates this; tagged was the one truncated >8B + * kind — the #237 fieldslotsize precedent), narrow scalars pad UP to + * one 8B eightbyte. Every tuple walk (cursor send/receive, t.N read, + * destructure, sret classify, DATA emit) MUST take its stride and + * its eightbyte count (eslot/8) from here — the per-site + * wide=(STR||SLICE)-else-8 predicates this absorbs were the #22 + * neighbor-slot/zeros miscompile. Checker twin: check.c N_TTUPLE / + * check.ww tupleelemslot. */ static int -tuple_ebytes(int wide) +tuple_eslot(Type *t) { - return wide ? (int)(ty_str->size / 8) : 1; + Type *u = type_chase_named(t); + if (u == NULL) return 8; + if (u->kind == TY_VOID) return 0; + /* a literal tuple's stamped element can be TY_UNTYPED_STR + * (size 0) — it occupies the str header slot (the C-t2 + * type_isstr lesson at the arg restage). */ + if (u->kind == TY_UNTYPED_STR) return (int)ty_str->size; + if (u->kind == TY_STR || u->kind == TY_SLICE + || u->kind == TY_TAGGED) + return (int)((u->size + 7) & ~(u64)7); + return 8; } static int @@ -373,13 +395,10 @@ cg_sret_retsize(Type *rt) if (rt->kind == TY_TUPLE) { int gptotal = 0, ssecount = 0, f32; for (Tparam *p = rt->params; p; p = p->next) { - Type *pu = type_chase_named(p->type); - int wide = pu && (pu->kind == TY_SLICE - || pu->kind == TY_STR); if (fld_isfloat(p->type, &f32)) ssecount++; else - gptotal += tuple_ebytes(wide); + gptotal += tuple_eslot(p->type) / 8; } if (gptotal > TUPLE_GPCAP || ssecount > TUPLE_SSECAP) return (int)rt->size; @@ -1624,25 +1643,26 @@ ins1(Cg *c, int op, Adr to) } /* tuple_store — store one received tuple element at BP-relative `off` - * from its SysV-class register. A slice/str rides its 3-word - * {ptr,len,cap} header from the INTEGER cursor tuple_rseq[gp..]; a float + * from its SysV-class register. A >8B element (slice/str 3-word + * {ptr,len,cap} header, tagged tag+payload box) rides tuple_eslot/8 + * consecutive INTEGER cursor words from tuple_rseq[gp..]; a float * rides tuple_sse_seq[sse] via MOVSD/MOVSS (#105 single-float widened to * the SSE cursor for #164/#107 multi-float); a scalar rides one INTEGER * word from tuple_rseq[gp]. The caller owns the dual cursor (validated + * advanced); this just emits the store. Shared by N_LET/N_MLET/N_MASSIGN * and, per #171, struct unpack — mirrors wwstage cgenstmt.ww tupstore. */ static void -tuple_store(Cg *c, Type *t, int wide, int gp, int sse, int off) +tuple_store(Cg *c, Type *t, int gp, int sse, int off) { int f32 = 0; + int eslot = tuple_eslot(t); - if (wide) { - ins2(c, A_MOVQ, areg(tuple_rseq[gp + 0]), - amem(D_BP, off + 0)); /* .ptr */ - ins2(c, A_MOVQ, areg(tuple_rseq[gp + 1]), - amem(D_BP, off + 8)); /* .len */ - ins2(c, A_MOVQ, areg(tuple_rseq[gp + 2]), - amem(D_BP, off + 16)); /* .cap */ + if (eslot == 0) + return; /* void element: the checker's 0-slot */ + if (eslot > 8) { + for (int k = 0; k < eslot / 8; k++) + ins2(c, A_MOVQ, areg(tuple_rseq[gp + k]), + amem(D_BP, off + k * 8)); return; } if (fld_isfloat(t, &f32)) { @@ -2502,8 +2522,18 @@ cg_widen_tagged_store(Cg *c, Local **locals_p, Type *dst, Node *src, * payload the slotted write assumes. Loud-stop (rule 7); the * SysV eightbyte tuple classification is a deferred follow-up. */ int total = 0; - for (Node *e = tupsrc->list; e; e = e->next) + for (Node *e = tupsrc->list; e; e = e->next) { + /* #22a (rule 7): a tagged element's box can't ride + * the scalar/wide store arms below — pre-guard it + * silently stored word0. Nested tagged-in-tuple-in- + * union packing is the #242/#22b family. */ + Type *eu = type_chase_named(e->type); + if (eu && eu->kind == TY_TAGGED) + fatal("cg_widen_tagged_store: tagged element " + "in a tuple-in-union payload unwired " + "(see #242/#22b)"); total += (node_isstr(e) || node_isslice(e)) ? 24 : 8; + } if (8 + total > sz) fatal("cg_widen_tagged_store: tuple-in-union payload needs " "SysV eightbyte packing (narrow elements share an " @@ -3093,6 +3123,65 @@ 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); } +/* tuple_lit_gpwords — INTEGER cursor words an N_TUPLE literal element + * occupies. MUST mirror the literal push arms (cg_tuple_lit_to_cursor / + * cgreturn N_TUPLE) exactly — the count drives the POP fill, so a + * count/push skew silently shifts every later element (#22 class). A + * 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. */ +static int +tuple_lit_gpwords(Node *e) +{ + int f32; + if (fld_isfloat(e->type, &f32)) return 0; + if (node_isstr(e) || node_isslice(e)) return (int)(ty_str->size / 8); + Type *eu = type_chase_named(e->type); + if (eu && (eu->kind == TY_TAGGED || eu->kind == TY_VOID)) + return tuple_eslot(e->type) / 8; + return 1; +} + +/* tuple_lit_push_elem — evaluate one N_TUPLE literal element and push + * 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 is with #22b's tupstore work, widening literals + * with #23). Shared by cg_tuple_lit_to_cursor and the cgreturn + * N_TUPLE arm — count (tuple_lit_gpwords) and push live or die + * together. */ +static void +tuple_lit_push_elem(Cg *c, Local *locals, Node *e) +{ + Type *eu = type_chase_named(e->type); + 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; + if (eoff == 0) + fatal("#22a: tagged tuple element from a non-local " + "source shape unwired (ident locals only; " + "rule 7, #22b/#23 follow-ups)"); + for (int k = 0; k < eslot / 8; k++) { + ins2(c, A_MOVQ, amem(D_BP, eoff + k * 8), + areg(D_AX)); + ins1(c, A_PUSHQ, areg(D_AX)); + } + return; + } + cgexpr(c, e, locals); + if (eu && eu->kind == TY_VOID) + return; + 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)); + } +} + /* 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 @@ -3116,8 +3205,7 @@ cg_tuple_lit_to_cursor(Cg *c, Local **locals, Node *tuple) if (fld_isfloat(e->type, &f32)) ssecount++; else - gptotal += tuple_ebytes(node_isstr(e) - || node_isslice(e)); + gptotal += tuple_lit_gpwords(e); } if (gptotal > TUPLE_GPCAP || ssecount > TUPLE_SSECAP) fatal("tuple literal exceeds register-return ABI capacity " @@ -3137,18 +3225,14 @@ cg_tuple_lit_to_cursor(Cg *c, Local **locals, Node *tuple) 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) { + cgexpr(c, e, *locals); 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)); - } + tuple_lit_push_elem(c, *locals, e); } for (int i = gptotal - 1; i >= 0; i--) ins1(c, A_POPQ, areg(tuple_rseq[i])); @@ -3179,12 +3263,10 @@ 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); + gptotal += tuple_eslot(p->type) / 8; } if (gptotal > TUPLE_GPCAP || ssecount > TUPLE_SSECAP) fatal("tuple ident exceeds register-return ABI capacity " @@ -3193,8 +3275,7 @@ cg_tuple_slot_to_cursor(Cg *c, int srcoff, Type *tu) 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 eslot = tuple_eslot(p->type); int isflt = fld_isfloat(p->type, &f32); if (isflt) { ins2(c, f32 ? A_MOVSS : A_MOVSD, @@ -3202,18 +3283,13 @@ cg_tuple_slot_to_cursor(Cg *c, int srcoff, Type *tu) areg(tuple_sse_seq[sse])); sse++; foff += 8; - } else if (wide) { - for (int k = 0; k < 3; k++) + } else { + for (int k = 0; k < eslot / 8; 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; + gp += eslot / 8; + foff += eslot; } } } @@ -3234,13 +3310,12 @@ cg_tagged_tuple_payload_shift(Cg *c, Type *tup) 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); + if (fld_isfloat(p->type, &f32) || tuple_eslot(p->type) != 8) + fatal("tuple-in-union ? unwrap: float/slice/str/tagged " + "payload element needs SysV per-eightbyte " + "classification (see #243); only integer tuple " + "payloads supported"); + words += 1; } /* tag occupies AX, so only DX/CX/R8 carry payload words. */ if (words > (int)nelem(seq) - 1) @@ -6780,17 +6855,10 @@ cgexpr(Cg *c, Node *n, Local *locals) idx = idx * 10 + (*q - '0'); Tparam *tp = bu->params; int foff = 0; - /* C-t0: slot stride, twin of the N_DOT - * TY_TUPLE walk. */ + /* C-t0/#22: slot stride (tuple_eslot), + * twin of the N_DOT TY_TUPLE walk. */ while (idx > 0 && tp) { - Type *su = (tp->type - && tp->type->kind == TY_NAMED) - ? tp->type->under : tp->type; - if (su && (su->kind == TY_STR - || su->kind == TY_SLICE)) - foff += (int)su->size; - else - foff += 8; + foff += tuple_eslot(tp->type); tp = tp->next; idx--; } @@ -8554,27 +8622,15 @@ cgexpr(Cg *c, Node *n, Local *locals) "kind unsupported (nested " "tuple/struct/array/tagged; " "rule 7)"); - /* C-t2: type_isstr/type_isslice, not a - * raw kind test — a LITERAL tuple's - * element types are the stamped expr - * types, so a strlit element is - * TY_UNTYPED_STR (size 0); the raw test - * under-classified it as 1 GP word - * against lit-to-cursor's node_isstr - * 3-word push. The wide stride reads - * ty_str->size (the header SSoT) for - * the same reason. */ - int wide = type_isstr(p->type) - || type_isslice(p->type); if (fld_isfloat(p->type, &ef32)) sstot++; else - gptot += tuple_ebytes(wide); + gptot += tuple_eslot(p->type) / 8; /* slot stride per element (sum == tuple slot * size); matches the wwstage slotsize() walk so * the @tupargscr width + reverse-push count agree * byte-for-byte. */ - tsz += wide ? (int)ty_str->size : 8; + tsz += tuple_eslot(p->type); } /* The producing call already satisfied #164's * return caps; guard anyway (tuple_store indexes @@ -8592,16 +8648,14 @@ cgexpr(Cg *c, Node *n, Local *locals) "#163)", cg_tupargscr_sz, tsz); } for (Tparam *p = tuparg_push->params; p; p = p->next) { - int wide = type_isstr(p->type) - || type_isslice(p->type); int isflt = fld_isfloat(p->type, &ef32); - tuple_store(c, p->type, wide, gpcur, ssecur, + tuple_store(c, p->type, gpcur, ssecur, cg_tupargscr + eoff); if (isflt) ssecur++; else - gpcur += tuple_ebytes(wide); - eoff += wide ? (int)ty_str->size : 8; + gpcur += tuple_eslot(p->type) / 8; + eoff += tuple_eslot(p->type); } for (int w = tsz - 8; w >= 0; w -= 8) { ins2(c, A_MOVQ, @@ -8812,10 +8866,6 @@ cgexpr(Cg *c, Node *n, Local *locals) * scope (twin of #164's cap). */ int ef32; for (Tparam *p = tu->params; p; p = p->next) { - /* C-t2: untyped-str-aware wide test — - * twin of the @tupargscr restage walk. */ - int wide = type_isstr(p->type) - || type_isslice(p->type); if (fld_isfloat(p->type, &ef32)) { if (fi >= 8) fatal("tuple arg float " @@ -8831,7 +8881,7 @@ cgexpr(Cg *c, Node *n, Local *locals) fi++; continue; } - int eb = tuple_ebytes(wide); + int eb = tuple_eslot(p->type) / 8; if (ii + eb > 6) fatal("tuple arg element " "overflows integer arg regs " @@ -9924,24 +9974,17 @@ cgexpr(Cg *c, Node *n, Local *locals) } } /* tuple positional field access: t.0, t.1, ... - * C-t0: slot stride (a str/slice its header, everything else - * one 8B eightbyte) — the layout every cursor transport site - * writes and the checker's TY_TUPLE size now counts. The load - * below keeps the element's NATURAL width (fldloadop). */ + * C-t0/#22: slot stride (tuple_eslot — str/slice header, + * tagged box, 8B floor) — the layout every cursor transport + * site writes and the checker's TY_TUPLE size counts. The + * load below keeps the element's NATURAL width (fldloadop). */ if (u && u->kind == TY_TUPLE && n->lhs->kind == N_IDENT && n->str) { int idx = 0; for (const char *q = n->str; *q; q++) idx = idx * 10 + (*q - '0'); Tparam *tp = u->params; int foff = 0; while (idx > 0 && tp) { - Type *su = (tp->type - && tp->type->kind == TY_NAMED) - ? tp->type->under : tp->type; - if (su && (su->kind == TY_STR - || su->kind == TY_SLICE)) - foff += (int)su->size; - else - foff += 8; + foff += tuple_eslot(tp->type); tp = tp->next; idx--; } @@ -9997,6 +10040,21 @@ cgexpr(Cg *c, Node *n, Local *locals) areg(D_CX)); break; } + /* #22a: tagged element — load the box into the + * tagged value regs (AX=tag, DX/CX/R8=payload), + * the same cursor the is/as spill + match read. + * A CX-based global never co-occurs (tagged + * element inits are loud at the DATA emit), so + * the ascending walk can't clobber the base. */ + if (fu && fu->kind == TY_TAGGED) { + int eslot = tuple_eslot(tp->type); + for (int k = 0; k < eslot / 8; k++) + ins2(c, A_MOVQ, + amem(base_reg, + base_disp + foff + k * 8), + areg(tuple_rseq[k])); + break; + } ins2(c, op, amem(base_reg, base_disp + foff), areg(D_AX)); } @@ -10961,21 +11019,33 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) cgexpr(c, n->rhs, *locals); int gpcur = 0, ssecur = 0, eoff = 0, ef32; for (Tparam *p = lu->params; p; p = p->next) { - Type *pu = (p->type && p->type->kind == TY_NAMED) - ? p->type->under : p->type; - int wide = pu && (pu->kind == TY_SLICE - || pu->kind == TY_STR); int isflt = fld_isfloat(p->type, &ef32); - tuple_store(c, p->type, wide, gpcur, ssecur, + tuple_store(c, p->type, gpcur, ssecur, off + eoff); if (isflt) ssecur++; else - gpcur += tuple_ebytes(wide); - eoff += wide ? (int)pu->size : 8; + gpcur += tuple_eslot(p->type) / 8; + eoff += tuple_eslot(p->type); } break; } + /* #22a (rule 7, ken R1): an OVER-CAP tuple init whose rhs is + * not a CALL has no store path — only the CALL shape rides + * the generic sret receive below; every other rhs fell past + * ALL the store arms to NOTHING (silent uninitialized-frame + * reads). Pre-existing for (str,str) literals; the #22a + * tagged slot sizes routed tagged shapes into it. cgexpr's + * cursor materialisers carry the loud caps (the literal / + * ident messages wwstage emits for the same shapes — rule + * 10); the fatal after is the net for any rhs kind whose + * cgexpr doesn't loud. Wiring is the #10/#22b sret family. */ + if (n->rhs && n->rhs->kind != N_CALL && lu + && lu->kind == TY_TUPLE && cg_sret_retsize(lt) > 0) { + cgexpr(c, n->rhs, *locals); + fatal("over-cap tuple initialiser from a non-call " + "source unwired (see #10/#22b)"); + } /* #38b residual (rule 7): `let w: T = f()?;` / `f()!` where f * returns an sret-classified tagged union — the unwrap would * need a mem-based read of the sret slot. The N_LET arms @@ -12175,8 +12245,7 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) if (fld_isfloat(e->type, &f32)) ssecount++; else - gptotal += tuple_ebytes(node_isstr(e) - || node_isslice(e)); + gptotal += tuple_lit_gpwords(e); } if (gptotal > TUPLE_GPCAP || ssecount > ssecap) { /* #10 Fold A: over-cap tuple returns via sret. The @@ -12203,6 +12272,18 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) Type *rtt = type_chase_named(cg_ret_type); Tparam *pp = (rtt && rtt->kind == TY_TUPLE) ? rtt->params : NULL; + /* #22a (rule 7): a tagged element's box can't + * ride the scalar/wide store arms below — the + * MEMORY-class tagged element store is #22b + * (task #28); pre-guard it silently stored + * word0 of the box. */ + for (Tparam *gp_ = pp; gp_; gp_ = gp_->next) { + Type *gu = type_chase_named(gp_->type); + if (gu && gu->kind == TY_TAGGED) + fatal("#22b: tagged element in " + "an over-cap (sret) tuple " + "return unwired (rule 7)"); + } int foff = 0; for (Node *e = n->lhs->list; e; e = e->next) { int isflt = fld_isfloat(e->type, &f32); @@ -12255,18 +12336,16 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) int sseidx = 0; for (Node *e = n->lhs->list; e; e = e->next) { int isflt = fld_isfloat(e->type, &f32); - cgexpr(c, e, *locals); /* scalar=AX; slice/str=AX,BX,CX; float=X0 */ if (isflt) { + cgexpr(c, e, *locals); /* float=X0 */ ins2(c, f32 ? A_MOVSS : A_MOVSD, areg(D_X0), amem(D_BP, fscr + sseidx * 8)); sseidx++; continue; } - ins1(c, A_PUSHQ, areg(D_AX)); /* scalar / .ptr */ - if (node_isstr(e) || node_isslice(e)) { - ins1(c, A_PUSHQ, areg(D_BX)); /* .len */ - ins1(c, A_PUSHQ, areg(D_CX)); /* .cap */ - } + /* scalar=AX; slice/str=AX,BX,CX; tagged box + * from its slot (tuple_lit_push_elem) */ + tuple_lit_push_elem(c, *locals, e); } for (int i = gptotal - 1; i >= 0; i--) ins1(c, A_POPQ, areg(tuple_rseq[i])); @@ -12574,22 +12653,19 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) int foff = 0; for (Node *l = n->list; l; l = l->next) { Type *t = l->type; - Type *u = type_chase_named(t); - int wide = u && (u->kind == TY_SLICE - || u->kind == TY_STR); + int eslot = tuple_eslot(t); int isflt = fld_isfloat(t, &lf32b); int esz = t ? (int)t->size : 8; - int bsz = wide ? esz : 8; int off = localoff(c, locals, l->str, - bsz, frame); + eslot > 8 ? eslot : 8, frame); if (isflt) { ins2(c, lf32b ? A_MOVSS : A_MOVSD, amem(D_BP, srcoff + foff), areg(D_X0)); ins2(c, lf32b ? A_MOVSS : A_MOVSD, areg(D_X0), amem(D_BP, off)); - } else if (wide) { - for (int k = 0; k < esz; k += 8) { + } else if (eslot > 8) { + for (int k = 0; k < eslot; k += 8) { ins2(c, A_MOVQ, amem(D_BP, srcoff + foff + k), areg(D_AX)); @@ -12603,7 +12679,7 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) ins2(c, fldstoreop(t, esz), areg(D_AX), amem(D_BP, off)); } - foff += wide ? 24 : 8; + foff += eslot; } break; } @@ -12617,20 +12693,18 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) int foff = 0; for (Node *l = n->list; l; l = l->next) { Type *t = l->type; - Type *u = type_chase_named(t); - int wide = u && (u->kind == TY_SLICE - || u->kind == TY_STR); + int eslot = tuple_eslot(t); int isflt = fld_isfloat(t, &lf32); int esz = t ? (int)t->size : 8; - int bsz = wide ? esz : 8; - int off = localoff(c, locals, l->str, bsz, frame); + int off = localoff(c, locals, l->str, + eslot > 8 ? eslot : 8, frame); if (isflt) { ins2(c, lf32 ? A_MOVSS : A_MOVSD, amem(D_BP, scr + foff), areg(D_X0)); ins2(c, lf32 ? A_MOVSS : A_MOVSD, areg(D_X0), amem(D_BP, off)); - } else if (wide) { - for (int k = 0; k < esz; k += 8) { + } else if (eslot > 8) { + for (int k = 0; k < eslot; k += 8) { ins2(c, A_MOVQ, amem(D_BP, scr + foff + k), areg(D_AX)); @@ -12645,7 +12719,7 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) } /* C-t0: slot stride — must mirror the * N_RETURN over-cap SEND's buffer layout. */ - foff += wide ? esz : 8; + foff += eslot; } break; } @@ -12653,13 +12727,10 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) int ssecap = TUPLE_SSECAP; int gptotal = 0, ssetotal = 0; for (Node *l = n->list; l; l = l->next) { - Type *t = l->type; - Type *u = (t && t->kind == TY_NAMED) ? t->under : t; - if (fld_isfloat(t, &lf32)) + if (fld_isfloat(l->type, &lf32)) ssetotal++; else - gptotal += tuple_ebytes(u && (u->kind == TY_SLICE - || u->kind == TY_STR)); + gptotal += tuple_eslot(l->type) / 8; } if (gptotal > gpcap) fatal("tuple destructure exceeds integer register-return " @@ -12672,16 +12743,15 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) int gpcur = 0, ssecur = 0; for (Node *l = n->list; l; l = l->next) { Type *t = l->type; - Type *u = (t && t->kind == TY_NAMED) ? t->under : t; - int wide = u && (u->kind == TY_SLICE || u->kind == TY_STR); + int eslot = tuple_eslot(t); int isflt = fld_isfloat(t, &lf32); - int sz = wide ? (int)u->size : 8; - int off = localoff(c, locals, l->str, sz, frame); - tuple_store(c, t, wide, gpcur, ssecur, off); + int off = localoff(c, locals, l->str, + eslot > 8 ? eslot : 8, frame); + tuple_store(c, t, gpcur, ssecur, off); if (isflt) ssecur++; else - gpcur += tuple_ebytes(wide); + gpcur += eslot / 8; } break; } @@ -12722,9 +12792,7 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) Tparam *tp = tp0; for (Node *l = n->list; l; l = l->next) { Type *et = tp ? tp->type : NULL; - Type *eu = type_chase_named(et); - int wide = eu && (eu->kind == TY_SLICE - || eu->kind == TY_STR); + int eslot = tuple_eslot(et); int isflt = fld_isfloat(et, &mf32); int esz = et ? (int)et->size : 8; int off = (l->kind == N_IDENT) @@ -12736,8 +12804,8 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) areg(D_X0)); ins2(c, mf32 ? A_MOVSS : A_MOVSD, areg(D_X0), amem(D_BP, off)); - } else if (wide) { - for (int k = 0; k < esz; k += 8) { + } else if (eslot > 8) { + for (int k = 0; k < eslot; k += 8) { ins2(c, A_MOVQ, amem(D_BP, scr + foff + k), areg(D_AX)); @@ -12754,7 +12822,7 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) } /* C-t0: slot stride — must mirror the * N_RETURN over-cap SEND's buffer layout. */ - foff += wide ? esz : 8; + foff += eslot; if (tp) tp = tp->next; } break; @@ -12763,13 +12831,10 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) int ssecap = TUPLE_SSECAP; int gptotal = 0, ssetotal = 0; for (Tparam *tp = tp0; tp; tp = tp->next) { - Type *u = (tp->type && tp->type->kind == TY_NAMED) - ? tp->type->under : tp->type; if (fld_isfloat(tp->type, &mf32)) ssetotal++; else - gptotal += tuple_ebytes(u && (u->kind == TY_SLICE - || u->kind == TY_STR)); + gptotal += tuple_eslot(tp->type) / 8; } if (gptotal > gpcap) fatal("tuple destructure exceeds integer register-return " @@ -12783,19 +12848,17 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame) Tparam *tp = tp0; for (Node *l = n->list; l; l = l->next) { Type *et = tp ? tp->type : NULL; - Type *u = (et && et->kind == TY_NAMED) ? et->under : et; - int wide = u && (u->kind == TY_SLICE || u->kind == TY_STR); int isflt = fld_isfloat(et, &mf32); int off = (l->kind == N_IDENT) ? localfind(*locals, l->str) : 0; /* harec `_` (off==0): skip the store but CONSUME the * cursor slot so the next element stays aligned. */ if (off != 0) - tuple_store(c, et, wide, gpcur, ssecur, off); + tuple_store(c, et, gpcur, ssecur, off); if (isflt) ssecur++; else - gpcur += tuple_ebytes(wide); + gpcur += tuple_eslot(et) / 8; if (tp) tp = tp->next; } break; @@ -12988,11 +13051,6 @@ cgfn(Cg *c, FILE *out, Node *fn) int off = localoff(c, &locals, p->str, sz, &frame); int eoff = 0, ef32; for (Tparam *te = pu->params; te; te = te->next) { - Type *teu = (te->type - && te->type->kind == TY_NAMED) - ? te->type->under : te->type; - int wide = teu && (teu->kind == TY_SLICE - || teu->kind == TY_STR); if (fld_isfloat(te->type, &ef32)) { if (fargi >= 8) fatal("tuple param float element " @@ -13006,7 +13064,7 @@ cgfn(Cg *c, FILE *out, Node *fn) eoff += 8; continue; } - int eb = tuple_ebytes(wide); + int eb = tuple_eslot(te->type) / 8; if (argi + eb > 6) fatal("tuple param element overflows " "integer arg regs (DI/SI/DX/CX/R8/" @@ -13015,7 +13073,7 @@ cgfn(Cg *c, FILE *out, Node *fn) ins2(c, A_MOVQ, areg(sysv_argregs[argi]), amem(D_BP, off + eoff + k * 8)); - eoff += wide ? (int)teu->size : 8; + eoff += tuple_eslot(te->type); } if (tp) tp = tp->next; continue; @@ -13805,6 +13863,15 @@ emit_tuple_data(FILE *out, Cg *c, const char *name, const char *module, Node *ev = e; while (ev && ev->kind == N_CAST) ev = ev->lhs; if (ev == NULL) return 0; + /* #22a (rule 7): a tagged element slot has no static-init + * shape (tag word + payload widening) — reject so the + * caller loud-stops; pre-guard an int init would have + * emitted one 8B word into the 16B+ box (silent layout + * skew). */ + { + Type *eu = type_chase_named(tp ? tp->type : NULL); + if (eu && eu->kind == TY_TAGGED) return 0; + } int wide = tp && (type_isstr(tp->type) || type_isslice(tp->type)); if (wide) { @@ -13849,7 +13916,7 @@ emit_tuple_data(FILE *out, Cg *c, const char *name, const char *module, fprintf(out, "DATAR %s+%d(SB),%s(SB)\n", sym, foff, lab); } - foff += wide ? (int)ty_str->size : 8; + foff += tuple_eslot(tp ? tp->type : NULL); } return 1; } diff --git a/cmd/wcc/check.c b/cmd/wcc/check.c index 1a33db8e..45cf6ae6 100644 --- a/cmd/wcc/check.c +++ b/cmd/wcc/check.c @@ -652,11 +652,16 @@ resolve_type(Checker *c, Node *n) if (tp->type && tp->type->align > al) al = tp->type->align; /* Slot layout is the tuple SSoT (tuple arc C-t0, * user-ratified): every element occupies the stride - * cgen's cursor transport actually writes — a - * str/slice its header, everything else (narrow - * scalars included) one 8B eightbyte. ww-internal ABI - * only (tuples never cross extern); size((u32,u32))=16 - * is observable via size() and diverges from Hare + * cgen's cursor transport actually writes — slot = + * roundup8(size(elem)), 8B a FLOOR not a ceiling + * (#22, user-ratified 2026-06-04): str/slice carry + * their 24B header, a tagged element its full + * tag+payload box ((str,str)=48B predates this; + * tagged was the one truncated >8B kind — the #237 + * fieldslotsize precedent), narrow scalars pad UP + * to one 8B eightbyte. ww-internal ABI only (tuples + * never cross extern); size((u32,u32))=16 is + * observable via size() and diverges from Hare * (harec type_store.c:533-580 anonymous-struct rule) * AND from ww's own structs (which pack narrow fields * post-fldloadop) — that internal inconsistency is @@ -669,8 +674,9 @@ resolve_type(Checker *c, Node *n) Type *eu = tp->type->kind == TY_NAMED ? tp->type->under : tp->type; if (eu && (eu->kind == TY_STR - || eu->kind == TY_SLICE)) - sz += eu->size; + || eu->kind == TY_SLICE + || eu->kind == TY_TAGGED)) + sz += (eu->size + 7) & ~(u64)7; else if (eu == NULL || eu->kind != TY_VOID) sz += 8; /* sizelint-ok: the slot IS the 8B eightbyte */ } diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 560cc90d..a88aefaa 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -11864,6 +11864,13 @@ fn tupleelemslot(pt: *tinfo) u64 = { if (pk == tykind.TY_VOID) { return 0u64; }; if (pk == tykind.TY_STR) { return t.size; }; if (pk == tykind.TY_SLICE) { return t.size; }; + // #22 (user-ratified 2026-06-04): slot = roundup8(size(elem)) — 8B + // is a FLOOR, not a ceiling. (str,str)=48B predates this; tagged + // was the one truncated >8B kind (the #237 fieldslotsize-missing- + // TY_TUPLE precedent: fieldslotsize below already carried this + // arm). Cstage twin: check.c N_TTUPLE; cgen accessor: tuple_eslot + // / tupeslot. + if (pk == tykind.TY_TAGGED) { return (t.size + 7u64) & ~7u64; }; if (pk == tykind.TY_PTR || pk == tykind.TY_FN || pk == tykind.TY_CHAN || pk == tykind.TY_I64 || pk == tykind.TY_U64 || pk == tykind.TY_INT || @@ -16273,6 +16280,22 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node, memphase: bool) i32 = { }; }; }; + // #22a: t.N tuple-element read leaves the + // same AX/DX/CX/R8 box cursor (this arc's + // t.N box load) — without this gate the + // widening scalar branch clamps the + // unresolvable tag to 0 and the callee + // reads variant 0. Stamped-carrier (#67) + // twin of the N_INDEX arm above; cstage + // needs no kind gate (its widen[i] `same` + // check is type-keyed on args[i]->type). + if (arg.kind == nkind.N_DOT) { + if (istaggedtype(c, arg)) { + if (slotsize(c, arg) == slotsize(c, ptype)) { + aistagged = true; + }; + }; + }; if (!aistagged) { widensz = slotsize(c, ptype); let tagged: *node = resolvetagged(c, ptype); @@ -16751,13 +16774,17 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node, memphase: bool) i32 = { os.exit(1); }; }; + // tagged is guarded loud above, so wide-vs-scalar is + // the full slot split here; eslot keeps the stride + // arithmetic on the accessor scale (#22). let wide: bool = false; if (tuplit) { wide = nodeisstr(c, et) || nodeisslice(c, et); } else { wide = isstrtype(c, et) || isslicetype(c, et); }; + let eslot: i32 = 8; + if (wide) { eslot = tyslicesize(): i32; }; if (isfloattype(c, et)) { sstot += 1; } - else { gptot += tupebytes(wide); }; - if (wide) { tsz += (tyslicesize(): i32); } - else { tsz += 8; }; + else { gptot += eslot / 8; }; + tsz += eslot; p = p.next; }; // The producing cursor fill already satisfied #164's caps; @@ -16778,11 +16805,12 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node, memphase: bool) i32 = { let wide: bool = false; if (tuplit) { wide = nodeisstr(c, et) || nodeisslice(c, et); } else { wide = isstrtype(c, et) || isslicetype(c, et); }; - tupstore(c, gpcur, ssecur, scr + eoff, wide, et); + let eslot: i32 = 8; + if (wide) { eslot = tyslicesize(): i32; }; + tupstore(c, gpcur, ssecur, scr + eoff, eslot, et); if (isfloattype(c, et)) { ssecur += 1; } - else { gpcur += tupebytes(wide); }; - if (wide) { eoff += (tyslicesize(): i32); } - else { eoff += 8; }; + else { gpcur += eslot / 8; }; + eoff += eslot; p = p.next; }; let w: i32 = tsz - 8; @@ -16855,7 +16883,13 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node, memphase: bool) i32 = { // arg.type_ is the element tinfo (istaggedtype/slotsize read // .type_) — feed the N_INDEX node directly, dropping the // indexvaluetnode walk. - if (arg.kind == nkind.N_INDEX) { + // #22a: N_DOT rides the same arm — the t.N tuple-element box load + // (this arc) fills the identical AX/DX/CX/R8 cursor. cstage's twin + // is the generic node_istaggedarg push (cgen.c cgcall); wwstage + // keeps the stamped-carrier kind gate (#67 pattern) — the + // remaining kinds (deref/cast/unwrap) are word0-only reads today, + // filed residual. + if (arg.kind == nkind.N_INDEX || arg.kind == nkind.N_DOT) { if (istaggedtype(c, arg)) { let isz: i32 = slotsize(c, arg); if (isz > 24) { emitline("\tPUSHQ\tR8\n"); }; @@ -17661,8 +17695,7 @@ export fn sretretsize(c: *cgen, t: *node) i32 = { if (isfloattype(c, et)) { ssecount = ssecount + 1; } else { - let wide: bool = isstrtype(c, et) || isslicetype(c, et); - gptotal = gptotal + tupebytes(wide); + gptotal = gptotal + tupeslotn(et) / 8; }; pt = pt.next; }; @@ -19438,6 +19471,20 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *tinfo, src: *node, slot_off: i32, slot_s let ttotal: i32 = 0; let ce: *node = tupsrc.list; for (ce != nil) { + // #22a (rule 7): a tagged element's box can't + // ride the scalar/wide store arms below — + // pre-guard it silently stored word0. Nested + // tagged-in-tuple-in-union packing is the + // #242/#22b family. Mirrors cstage. + let ceti: *tinfo = ce.type_: *tinfo; + for (ceti != nil && ceti.kind == tykind.TY_NAMED) { + ceti = ceti.under; + }; + if (ceti != nil && ceti.kind == tykind.TY_TAGGED) { + let m22: str = "cgwidentaggedstore: tagged element in a tuple-in-union payload unwired (see #242/#22b)\n"; + os.write(2, m22.ptr, m22.len: u64); + os.exit(1); + }; if (nodeisstr(c, ce) || nodeisslice(c, ce)) { ttotal += 24; } else { ttotal += 8; }; @@ -20570,7 +20617,22 @@ fn cgexpr(c: *cgen, n: *node) void = { fn cgtagvariantidx(c: *cgen, tagged: *node, vt: *node) i32 = { if (tagged == nil) { return -1; }; if (vt == nil) { return -1; }; - if (tagged.kind != nkind.N_TTAGGED) { return -1; }; + if (tagged.kind != nkind.N_TTAGGED) { + // #22a: a STAMPED-CARRIER scrutinee (the #67 matchscrutt + // shape — is/as on a tuple element t.N, a struct field, an + // indexed element) is not an N_TTAGGED type-AST node; its + // tagged type rides .type_. Resolve via the tinfo twin + // (flatvariantidxt), the same core the widen-store uses — + // cstage cg_tag_for_variant is type-based for every + // scrutinee shape, so the AST-keyed -1 here was a silent + // tag-0 clamp on wwstage (cs CMPQ $1 vs ww CMPQ $0). + let sti: *tinfo = tagged.type_: *tinfo; + for (sti != nil && sti.kind == tykind.TY_NAMED) { sti = sti.under; }; + if (sti != nil && sti.kind == tykind.TY_TAGGED && vt.type_ != nil) { + return flatvariantidxt(sti, vt.type_: *tinfo); + }; + return -1; + }; // `is []T` / `as []T` — slice-shape lookup routes through the // element-aware helper, which carries the loose first-slice-shape // fallback (cstage type_assignable stand-in) that flatvariantidx's @@ -23080,7 +23142,9 @@ fn cgdot(c: *cgen, n: *node) void = { for (i < idx) { if (tp == nil) { i = idx; } else { - foff += slotsize(c, tp.lhs); + // C-t0/#22: slot stride + // (tupeslot accessor). + foff += tupeslotn(tp.lhs); tp = tp.next; i += 1; }; @@ -23114,6 +23178,25 @@ fn cgdot(c: *cgen, n: *node) void = { emitline("(BP), X0\n"); return; }; + // #22a: tagged element — load the box + // into the tagged value regs (AX=tag, + // DX/CX/R8=payload), the cursor the + // is/as spill + match read. Byte-id + // twin of cstage's N_DOT TY_TUPLE + // tagged arm. + if (istaggedtype(c, tpt)) { + let eslot: i32 = tupeslotn(tpt); + let k: i32 = 0; + for (k < eslot / 8) { + emitline("\tMOVQ\t"); + emitoff((lc.off + foff + k * 8): i64); + emitline("(BP), "); + emitline(tupreg(k)); + emitline("\n"); + k += 1; + }; + return; + }; // C-t0: load at the element's NATURAL // width (narrow MOVL/MOVSXD/... at the // slot base), not the 8B slot width — @@ -23284,7 +23367,9 @@ fn cgdot(c: *cgen, n: *node) void = { for (gi < gidx) { if (gtp == nil) { gi = gidx; } else { - gfoff += slotsize(c, gtp.lhs); + // C-t0/#22: slot stride + // (tupeslot accessor). + gfoff += tupeslotn(gtp.lhs); gtp = gtp.next; gi += 1; }; @@ -26474,7 +26559,9 @@ fn cgcall(c: *cgen, n: *node) void = { for (i < idx) { if (tp == nil) { i = idx; } else { - foff += slotsize(c, tp.lhs); + // C-t0/#22: slot + // stride (tupeslot). + foff += tupeslotn(tp.lhs); tp = tp.next; i += 1; }; @@ -26510,7 +26597,9 @@ fn cgcall(c: *cgen, n: *node) void = { for (gi < gidx) { if (gtp == nil) { gi = gidx; } else { - gfoff += slotsize(c, gtp.lhs); + // C-t0/#22: slot + // stride (tupeslot). + gfoff += tupeslotn(gtp.lhs); gtp = gtp.next; gi += 1; }; @@ -26963,10 +27052,14 @@ fn cgcall(c: *cgen, n: *node) void = { fpidx += 1; popped += 1; } else { + // tagged is guarded loud at the restage, so + // wide-vs-scalar is the full slot split here + // (#22 accessor scale). let wide: bool = false; if (tuplit) { wide = nodeisstr(c, et) || nodeisslice(c, et); } else { wide = isstrtype(c, et) || isslicetype(c, et); }; - let eb: i32 = tupebytes(wide); + let eb: i32 = 1; + if (wide) { eb = (tyslicesize() / 8i64): i32; }; if (intidx + eb > 6) { let msg: str = "tuple arg element overflows integer arg regs (DI/SI/DX/CX/R8/R9); stitch out of scope, see #163\n"; os.write(2, msg.ptr, msg.len: u64); @@ -31050,9 +31143,36 @@ fn tupsse(i: i32) str = { return "X1"; }; -fn tupebytes(wide: bool) i32 = { - if (wide) { return (tyslicesize() / 8i64): i32; }; - return 1; +// tupeslot — THE tuple element-stride accessor (#22): the slot a tuple +// element occupies, in bytes. slot = roundup8(size(elem)), 8B a FLOOR +// not a ceiling (user-ratified 2026-06-04): str/slice carry their 24B +// header, a tagged element its full tag+payload box ((str,str)=48B +// predates this; tagged was the one truncated >8B kind — the #237 +// fieldslotsize precedent), narrow scalars pad UP to one 8B eightbyte. +// Every tuple walk (cursor send/receive, t.N read, destructure, sret +// classify, DATA emit) takes its stride and its eightbyte count +// (eslot/8) from here — the per-site wide=(STR||SLICE)-else-8 +// predicates this absorbs were the #22 neighbor-slot/zeros miscompile. +// Checker twin: check.ww tupleelemslot / check.c N_TTUPLE; cstage twin: +// tuple_eslot (cmd/w6c/cgen.c). +export fn tupeslot(ti: *tinfo) i32 = { + let t: *tinfo = ti; + for (t != nil && t.kind == tykind.TY_NAMED) { t = t.under; }; + if (t == nil) { return 8; }; + if (t.kind == tykind.TY_VOID) { return 0; }; + // a literal tuple's stamped element can be untyped_str (size 0) — + // it occupies the str header slot (the C-t2 type_isstr lesson). + if (t.kind == tykind.TY_UNTYPED_STR) { return tyslicesize(): i32; }; + if (t.kind == tykind.TY_STR || t.kind == tykind.TY_SLICE || + t.kind == tykind.TY_TAGGED) { + return ((t.size + 7u64) & ~7u64): i32; + }; + return 8; +}; + +export fn tupeslotn(n: *node) i32 = { + if (n == nil) { return 8; }; + return tupeslot(n.type_: *tinfo); }; // rettupleof — the N_TTUPLE return-type node of an N_CALL rhs (else nil). @@ -31119,29 +31239,24 @@ fn nodetuplearg(c: *cgen, a: *node) *node = { }; // tupstore — store the tuple element at register-cursor `cur` into the -// BP-relative slot at `off`. A slice/str stores its 3-word {ptr,len,cap} -// header (ref/hare/rt/ensure.ha:4-8) at off/+8/+16 from consecutive -// INTEGER cursor registers; a float rides the SSE cursor (X0,X1); a -// scalar stores 1 INTEGER word. The caller owns the dual cursor -// (validated + advanced). Byte-identical to the cstage tuple_store -// (cmd/w6c/cgen.c). -fn tupstore(c: *cgen, gpcur: i32, ssecur: i32, off: i32, wide: bool, tn: *node) void = { - if (wide) { - emitline("\tMOVQ\t"); - emitline(tupreg(gpcur + 0)); - emitline(", "); - emitoff(off: i64); - emitline("(BP)\n"); - emitline("\tMOVQ\t"); - emitline(tupreg(gpcur + 1)); - emitline(", "); - emitoff((off + 8): i64); - emitline("(BP)\n"); - emitline("\tMOVQ\t"); - emitline(tupreg(gpcur + 2)); - emitline(", "); - emitoff((off + 16): i64); - emitline("(BP)\n"); +// BP-relative slot at `off`. A >8B element (slice/str 3-word +// {ptr,len,cap} header, ref/hare/rt/ensure.ha:4-8; tagged tag+payload +// box, #22) stores its eslot/8 words from consecutive INTEGER cursor +// registers; a float rides the SSE cursor (X0,X1); a scalar stores 1 +// INTEGER word. The caller owns the dual cursor (validated + +// advanced). Byte-identical to the cstage tuple_store (cmd/w6c/cgen.c). +fn tupstore(c: *cgen, gpcur: i32, ssecur: i32, off: i32, eslot: i32, tn: *node) void = { + if (eslot == 0) { return; }; // void element: the checker's 0-slot + if (eslot > 8) { + let k: i32 = 0; + for (k < eslot / 8) { + emitline("\tMOVQ\t"); + emitline(tupreg(gpcur + k)); + emitline(", "); + emitoff((off + k * 8): i64); + emitline("(BP)\n"); + k += 1; + }; return; }; // #105 / #164 (#107): an f64/f32 element rides the SSE cursor reg @@ -31195,6 +31310,67 @@ fn tupstore(c: *cgen, gpcur: i32, ssecur: i32, off: i32, wide: bool, tn: *node) emitline("(BP)\n"); }; +// tuplitgpwords — INTEGER cursor words an N_TUPLE literal element +// occupies. MUST mirror the literal push arms (tuplitpushelem) exactly +// — the count drives the POP fill, so a count/push skew silently +// shifts every later element (#22 class). A float rides the SSE row +// (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 = { + if (isfloattype(c, e)) { return 0; }; + if (nodeisstr(c, e) || nodeisslice(c, e)) { + return (tyslicesize() / 8i64): i32; + }; + 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 || + t.kind == tykind.TY_VOID)) { + return tupeslotn(e) / 8; + }; + return 1; +}; + +// tuplitpushelem — evaluate one N_TUPLE literal element and push its +// 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 is +// with #22b's tupstore work, widening literals with #23). Mirror of +// cstage tuple_lit_push_elem — count (tuplitgpwords) and push live or +// die together. +fn tuplitpushelem(c: *cgen, e: *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 eslot: i32 = tupeslotn(e); + let eoff: i32 = 0; + if (e.kind == nkind.N_IDENT) { eoff = localfind(c, e.str); }; + if (eoff == 0) { + let m22: str = "#22a: tagged tuple element from a non-local source shape unwired (ident locals only; rule 7, #22b/#23 follow-ups)\n"; + os.write(2, m22.ptr, m22.len: u64); + os.exit(1); + }; + let k: i32 = 0; + for (k < eslot / 8) { + emitline("\tMOVQ\t"); + emitoff((eoff + k * 8): i64); + emitline("(BP), AX\n"); + emitline("\tPUSHQ\tAX\n"); + k += 1; + }; + return; + }; + cgexpr(c, e); + if (t != nil && t.kind == tykind.TY_VOID) { return; }; + emitline("\tPUSHQ\tAX\n"); + if (nodeisstr(c, e) || nodeisslice(c, e)) { + emitline("\tPUSHQ\tBX\n"); + emitline("\tPUSHQ\tCX\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 @@ -31214,8 +31390,7 @@ fn cgtuplelittocursor(c: *cgen, tuple: *node) void = { if (isfloattype(c, e)) { ssecount = ssecount + 1; } else { - let wide: bool = nodeisstr(c, e) || nodeisslice(c, e); - gptotal = gptotal + tupebytes(wide); + gptotal = gptotal + tuplitgpwords(c, e); }; e = e.next; }; @@ -31232,8 +31407,8 @@ fn cgtuplelittocursor(c: *cgen, tuple: *node) void = { e = tuple.list; for (e != nil) { let isflt: bool = isfloattype(c, e); - cgexpr(c, e); if (isflt) { + cgexpr(c, e); let mov: str = "MOVSD"; if (isf32type(c, e)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\tX0, "); @@ -31241,11 +31416,7 @@ fn cgtuplelittocursor(c: *cgen, tuple: *node) void = { 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"); - }; + tuplitpushelem(c, e); }; e = e.next; }; @@ -31292,8 +31463,7 @@ fn cgtupleslottocursor(c: *cgen, srcoff: i32, tu: *tinfo) void = { 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); + gptotal = gptotal + tupeslot(el.type_) / 8; }; el = el.tnext; }; @@ -31310,7 +31480,7 @@ fn cgtupleslottocursor(c: *cgen, srcoff: i32, tu: *tinfo) void = { 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); + let eslot: i32 = tupeslot(el.type_); if (isflt) { let mov: str = "MOVSD"; if (et.kind == tykind.TY_F32) { mov = "MOVSS"; }; @@ -31321,9 +31491,9 @@ fn cgtupleslottocursor(c: *cgen, srcoff: i32, tu: *tinfo) void = { emitline("\n"); sse = sse + 1; foff += 8; - } else { if (wide) { + } else { let k: i32 = 0; - for (k < 3) { + for (k < eslot / 8) { emitline("\tMOVQ\t"); emitoff((srcoff + foff + k * 8): i64); emitline("(BP), "); @@ -31331,17 +31501,9 @@ fn cgtupleslottocursor(c: *cgen, srcoff: i32, tu: *tinfo) void = { 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; - }; }; + gp += eslot / 8; + foff += eslot; + }; el = el.tnext; }; }; @@ -31361,9 +31523,8 @@ fn cgtaggedtuplepayloadshift(c: *cgen, tup: *tinfo) void = { 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"; + if (isflt || tupeslot(el.type_) != 8) { + let msg: str = "tuple-in-union ? unwrap: float/slice/str/tagged 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); }; @@ -31420,12 +31581,28 @@ fn cgreturn(c: *cgen, n: *node) void = { if (isfloattype(c, e)) { ssecount = ssecount + 1; } else { - let wide: bool = nodeisstr(c, e) || nodeisslice(c, e); - gptotal = gptotal + tupebytes(wide); + gptotal = gptotal + tuplitgpwords(c, e); }; e = e.next; }; if (gptotal > TUPLE_GPCAP || ssecount > ssecap) { + // #22a (rule 7): a tagged element's box can't + // ride the scalar/wide store arms below — the + // MEMORY-class tagged element store is #22b + // (task #28); pre-guard it silently stored + // word0 of the box. + let gq: *node = nil; + if (c.fnret != nil) { gq = c.fnret.list; }; + for (gq != nil) { + let gqt: *tinfo = gq.lhs.type_: *tinfo; + for (gqt != nil && gqt.kind == tykind.TY_NAMED) { gqt = gqt.under; }; + if (gqt != nil && gqt.kind == tykind.TY_TAGGED) { + let m22b: str = "#22b: tagged element in an over-cap (sret) tuple return unwired (rule 7)\n"; + os.write(2, m22b.ptr, m22b.len: u64); + os.exit(1); + }; + gq = gq.next; + }; // #10 Fold A: over-cap tuple returns via sret. The // prologue wired @sretarg (sretretsize agrees on the // caps — TUPLE_GPCAP/TUPLE_SSECAP, the shared SSoT), @@ -31485,7 +31662,12 @@ fn cgreturn(c: *cgen, n: *node) void = { emitline("\n"); }; }; - foff += esz; + // C-t0: the sret buffer is slot-laid like + // every tuple home (checker size, t.N + // reader, mlet receive agree); esz keeps + // the store WIDTH natural. Mirrors cstage + // cgen.c N_RETURN over-cap arm. + if (wide) { foff += esz; } else { foff += 8; }; we = we.next; if (pt != nil) { pt = pt.next; }; }; @@ -31506,8 +31688,8 @@ fn cgreturn(c: *cgen, n: *node) void = { e = rhs.list; for (e != nil) { let isflt: bool = isfloattype(c, e); - cgexpr(c, e); if (isflt) { + cgexpr(c, e); let mov: str = "MOVSD"; if (isf32type(c, e)) { mov = "MOVSS"; }; emitline("\t"); @@ -31517,11 +31699,9 @@ fn cgreturn(c: *cgen, n: *node) void = { emitline("(BP)\n"); sseidx = sseidx + 1; } else { - emitline("\tPUSHQ\tAX\n"); // scalar / .ptr - if (nodeisstr(c, e) || nodeisslice(c, e)) { - emitline("\tPUSHQ\tBX\n"); // .len - emitline("\tPUSHQ\tCX\n"); // .cap - }; + // scalar=AX; slice/str=AX,BX,CX; tagged + // box from its slot (tuplitpushelem) + tuplitpushelem(c, e); }; e = e.next; }; @@ -32962,20 +33142,15 @@ fn cglet(c: *cgen, n: *node) void = { for (q != nil) { let qt: *node = q.lhs; let isflt: bool = isfloattype(c, qt); - let wide: bool = isstrtype(c, qt) - || isslicetype(c, qt); + let eslot: i32 = tupeslotn(qt); tupstore(c, gpcur, ssecur, - off + eoff, wide, qt); + off + eoff, eslot, qt); if (isflt) { ssecur = ssecur + 1; } else { - gpcur = gpcur + tupebytes(wide); - }; - if (wide) { - eoff = eoff + (tyslicesize(): i32); - } else { - eoff = eoff + 8; + gpcur = gpcur + eslot / 8; }; + eoff = eoff + eslot; q = q.next; }; c.lastwasreturn = 0; @@ -33816,7 +33991,10 @@ fn cgmassign(c: *cgen, n: *node) void = { }; }; }; - foff += esz; + // C-t0: slot stride — must mirror the N_RETURN + // over-cap SEND's buffer layout (cstage N_MASSIGN + // twin strides tuple_eslot). + foff += tupeslotn(tn); lb = lb.next; if (pt2 != nil) { pt2 = pt2.next; }; }; @@ -33836,8 +34014,7 @@ fn cgmassign(c: *cgen, n: *node) void = { if (isfloattype(c, tn)) { ssetotal = ssetotal + 1; } else { - let wide: bool = isstrtype(c, tn) || isslicetype(c, tn); - gptotal = gptotal + tupebytes(wide); + gptotal = gptotal + tupeslotn(tn) / 8; }; l = l.next; if (pt != nil) { pt = pt.next; }; @@ -33864,18 +34041,18 @@ fn cgmassign(c: *cgen, n: *node) void = { let tn: *node = nil; if (pt != nil) { tn = pt.lhs; }; let isflt: bool = isfloattype(c, tn); - let wide: bool = isstrtype(c, tn) || isslicetype(c, tn); + let eslot: i32 = tupeslotn(tn); let off: i32 = 0; if (l.kind == nkind.N_IDENT) { off = localfind(c, l.str); }; // harec `_` (off==0): skip the store but CONSUME the cursor // slot so the next element stays aligned. if (off != 0) { - tupstore(c, gpcur, ssecur, off, wide, tn); + tupstore(c, gpcur, ssecur, off, eslot, tn); }; if (isflt) { ssecur = ssecur + 1; } else { - gpcur = gpcur + tupebytes(wide); + gpcur = gpcur + eslot / 8; }; l = l.next; if (pt != nil) { pt = pt.next; }; @@ -33938,13 +34115,13 @@ fn cgmlet(c: *cgen, n: *node) void = { for (lb != nil) { let tn: *node = lb.lhs; let isflt: bool = isfloattype(c, tn); - let wide: bool = isstrtype(c, tn) || isslicetype(c, tn); + let eslot: i32 = tupeslotn(tn); let esz: i32 = 8; let eti: *tinfo = nil; if (tn != nil) { eti = tn.type_: *tinfo; }; if (eti != nil) { esz = eti.size: i32; }; let bsz: i32 = 8; - if (wide) { bsz = tyslicesize(): i32; }; + if (eslot > 8) { bsz = eslot; }; let off: i32 = localadd(c, lb.str, bsz, tn); if (isflt) { let mov: str = "MOVSD"; @@ -33954,9 +34131,9 @@ fn cgmlet(c: *cgen, n: *node) void = { emitline("(BP), X0\n"); emitline("\t"); emitline(mov); emitline("\tX0, "); emitoff(off: i64); emitline("(BP)\n"); - } else { if (wide) { + } else { if (eslot > 8) { let k: i32 = 0; - for (k < esz) { + for (k < eslot) { emitline("\tMOVQ\t"); emitoff((srcoff + foff + k): i64); emitline("(BP), AX\n"); @@ -33974,7 +34151,7 @@ fn cgmlet(c: *cgen, n: *node) void = { emitline("\t"); emitline(sop); emitline("\tAX, "); emitoff(off: i64); emitline("(BP)\n"); }; }; - if (wide) { foff += 24; } else { foff += 8; }; + foff += eslot; lb = lb.next; }; c.lastwasreturn = 0; @@ -33995,14 +34172,14 @@ fn cgmlet(c: *cgen, n: *node) void = { let tn: *node = nil; if (pt2 != nil) { tn = pt2.lhs; }; let isflt: bool = isfloattype(c, tn); - let wide: bool = isstrtype(c, tn) || isslicetype(c, tn); + let eslot: i32 = tupeslotn(tn); let esz: i32 = 8; if (pt2 != nil) { let eti: *tinfo = pt2.lhs.type_: *tinfo; if (eti != nil) { esz = eti.size: i32; }; }; let bsz: i32 = 8; - if (wide) { bsz = tyslicesize(): i32; }; + if (eslot > 8) { bsz = eslot; }; let off: i32 = localadd(c, lb.str, bsz, tn); if (isflt) { let mov: str = "MOVSD"; @@ -34013,9 +34190,9 @@ fn cgmlet(c: *cgen, n: *node) void = { emitline("\t"); emitline(mov); emitline("\tX0, "); emitoff(off: i64); emitline("(BP)\n"); } else { - if (wide) { + if (eslot > 8) { let k: i32 = 0; - for (k < esz) { + for (k < eslot) { emitline("\tMOVQ\t"); emitoff((scr + foff + k): i64); emitline("(BP), AX\n"); @@ -34035,7 +34212,7 @@ fn cgmlet(c: *cgen, n: *node) void = { emitoff(off: i64); emitline("(BP)\n"); }; }; - foff += esz; + foff += eslot; lb = lb.next; if (pt2 != nil) { pt2 = pt2.next; }; }; @@ -34057,8 +34234,7 @@ fn cgmlet(c: *cgen, n: *node) void = { if (isfloattype(c, tn)) { ssetotal = ssetotal + 1; } else { - let wide: bool = isstrtype(c, tn) || isslicetype(c, tn); - gptotal = gptotal + tupebytes(wide); + gptotal = gptotal + tupeslotn(tn) / 8; }; l = l.next; if (pt != nil) { pt = pt.next; }; @@ -34087,15 +34263,15 @@ fn cgmlet(c: *cgen, n: *node) void = { if (pt != nil) { tn = pt.lhs; }; }; let isflt: bool = isfloattype(c, tn); - let wide: bool = isstrtype(c, tn) || isslicetype(c, tn); + let eslot: i32 = tupeslotn(tn); let sz: i32 = 8; - if (wide) { sz = tyslicesize(): i32; }; + if (eslot > 8) { sz = eslot; }; let off: i32 = localadd(c, l.str, sz, tn); - tupstore(c, gpcur, ssecur, off, wide, tn); + tupstore(c, gpcur, ssecur, off, eslot, tn); if (isflt) { ssecur = ssecur + 1; } else { - gpcur = gpcur + tupebytes(wide); + gpcur = gpcur + eslot / 8; }; l = l.next; if (pt != nil) { pt = pt.next; }; @@ -34745,8 +34921,7 @@ fn cgfnparams(c: *cgen, params: *node) void = { emitline("(BP)\n"); fidx += 1; } else { - let wide: bool = isstrtype(c, et) || isslicetype(c, et); - let eb: i32 = tupebytes(wide); + let eb: i32 = tupeslotn(et) / 8; if (idx + eb > 6) { let msg: str = "tuple param element overflows integer arg regs (DI/SI/DX/CX/R8/R9); stitch out of scope, see #163\n"; os.write(2, msg.ptr, msg.len: u64); @@ -34763,7 +34938,7 @@ fn cgfnparams(c: *cgen, params: *node) void = { k += 1; }; }; - eoff += slotsize(c, et); + eoff += tupeslotn(et); te = te.next; }; p = p.next; @@ -37456,15 +37631,12 @@ fn emitslicedata(c: *cgen, name: str, module: str, slt: *tinfo, fn emittupledata(c: *cgen, name: str, module: str, tt: *node, rhs: *node) bool = { if (tt == nil) { return false; }; if (rhs == nil) { + // #22: slot-sum via the accessor so the zero-fill matches + // the checker size (cstage zero-emits u->size). let zsz: i32 = 0; let p0: *node = tt.list; for (p0 != nil) { - let et0: *node = p0.lhs; - if (isstrtype(c, et0) || isslicetype(c, et0)) { - zsz += (tyslicesize(): i32); - } else { - zsz += 8; - }; + zsz += tupeslotn(p0.lhs); p0 = p0.next; }; emitline("DATAW "); @@ -37486,6 +37658,21 @@ fn emittupledata(c: *cgen, name: str, module: str, tt: *node, rhs: *node) bool = let ev: *node = e; for (ev != nil && ev.kind == nkind.N_CAST) { ev = ev.lhs; }; if (ev == nil) { return false; }; + // #22a (rule 7): a tagged element slot has no static-init + // shape (tag word + payload widening) — reject so the caller + // loud-stops; pre-guard an int init would have emitted one + // 8B word into the 16B+ box (silent layout skew). Mirrors + // cstage emit_tuple_data. + { + let eti: *tinfo = nil; + if (et != nil) { eti = et.type_: *tinfo; }; + for (eti != nil && eti.kind == tykind.TY_NAMED) { + eti = eti.under; + }; + if (eti != nil && eti.kind == tykind.TY_TAGGED) { + return false; + }; + }; let wide: bool = isstrtype(c, et) || isslicetype(c, et); if (wide) { if (ev.kind != nkind.N_STRLIT) { return false; }; @@ -37557,7 +37744,10 @@ fn emittupledata(c: *cgen, name: str, module: str, tt: *node, rhs: *node) bool = }; foff += (tyslicesize(): i32); } else { - foff += 8; + // #22: slot stride via the accessor (tagged is + // rejected upstream; non-wide is 8 today — keeps the + // stride on the accessor scale). + foff += tupeslotn(et); }; e = e.next; if (tp != nil) { tp = tp.next; }; diff --git a/selfhost/cmd/wcc/cgen.ww b/selfhost/cmd/wcc/cgen.ww index 44861287..71aebef5 100644 --- a/selfhost/cmd/wcc/cgen.ww +++ b/selfhost/cmd/wcc/cgen.ww @@ -2208,15 +2208,12 @@ fn emitslicedata(c: *cgen, name: str, module: str, slt: *tinfo, fn emittupledata(c: *cgen, name: str, module: str, tt: *node, rhs: *node) bool = { if (tt == nil) { return false; }; if (rhs == nil) { + // #22: slot-sum via the accessor so the zero-fill matches + // the checker size (cstage zero-emits u->size). let zsz: i32 = 0; let p0: *node = tt.list; for (p0 != nil) { - let et0: *node = p0.lhs; - if (isstrtype(c, et0) || isslicetype(c, et0)) { - zsz += (tyslicesize(): i32); - } else { - zsz += 8; - }; + zsz += tupeslotn(p0.lhs); p0 = p0.next; }; emitline("DATAW "); @@ -2238,6 +2235,21 @@ fn emittupledata(c: *cgen, name: str, module: str, tt: *node, rhs: *node) bool = let ev: *node = e; for (ev != nil && ev.kind == nkind.N_CAST) { ev = ev.lhs; }; if (ev == nil) { return false; }; + // #22a (rule 7): a tagged element slot has no static-init + // shape (tag word + payload widening) — reject so the caller + // loud-stops; pre-guard an int init would have emitted one + // 8B word into the 16B+ box (silent layout skew). Mirrors + // cstage emit_tuple_data. + { + let eti: *tinfo = nil; + if (et != nil) { eti = et.type_: *tinfo; }; + for (eti != nil && eti.kind == tykind.TY_NAMED) { + eti = eti.under; + }; + if (eti != nil && eti.kind == tykind.TY_TAGGED) { + return false; + }; + }; let wide: bool = isstrtype(c, et) || isslicetype(c, et); if (wide) { if (ev.kind != nkind.N_STRLIT) { return false; }; @@ -2309,7 +2321,10 @@ fn emittupledata(c: *cgen, name: str, module: str, tt: *node, rhs: *node) bool = }; foff += (tyslicesize(): i32); } else { - foff += 8; + // #22: slot stride via the accessor (tagged is + // rejected upstream; non-wide is 8 today — keeps the + // stride on the accessor scale). + foff += tupeslotn(et); }; e = e.next; if (tp != nil) { tp = tp.next; }; diff --git a/selfhost/cmd/wcc/cgendecl.ww b/selfhost/cmd/wcc/cgendecl.ww index 8bc450d3..bc041d87 100644 --- a/selfhost/cmd/wcc/cgendecl.ww +++ b/selfhost/cmd/wcc/cgendecl.ww @@ -135,8 +135,7 @@ fn cgfnparams(c: *cgen, params: *node) void = { emitline("(BP)\n"); fidx += 1; } else { - let wide: bool = isstrtype(c, et) || isslicetype(c, et); - let eb: i32 = tupebytes(wide); + let eb: i32 = tupeslotn(et) / 8; if (idx + eb > 6) { let msg: str = "tuple param element overflows integer arg regs (DI/SI/DX/CX/R8/R9); stitch out of scope, see #163\n"; os.write(2, msg.ptr, msg.len: u64); @@ -153,7 +152,7 @@ fn cgfnparams(c: *cgen, params: *node) void = { k += 1; }; }; - eoff += slotsize(c, et); + eoff += tupeslotn(et); te = te.next; }; p = p.next; diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 844ec62d..9880db07 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -134,7 +134,22 @@ fn cgexpr(c: *cgen, n: *node) void = { fn cgtagvariantidx(c: *cgen, tagged: *node, vt: *node) i32 = { if (tagged == nil) { return -1; }; if (vt == nil) { return -1; }; - if (tagged.kind != nkind.N_TTAGGED) { return -1; }; + if (tagged.kind != nkind.N_TTAGGED) { + // #22a: a STAMPED-CARRIER scrutinee (the #67 matchscrutt + // shape — is/as on a tuple element t.N, a struct field, an + // indexed element) is not an N_TTAGGED type-AST node; its + // tagged type rides .type_. Resolve via the tinfo twin + // (flatvariantidxt), the same core the widen-store uses — + // cstage cg_tag_for_variant is type-based for every + // scrutinee shape, so the AST-keyed -1 here was a silent + // tag-0 clamp on wwstage (cs CMPQ $1 vs ww CMPQ $0). + let sti: *tinfo = tagged.type_: *tinfo; + for (sti != nil && sti.kind == tykind.TY_NAMED) { sti = sti.under; }; + if (sti != nil && sti.kind == tykind.TY_TAGGED && vt.type_ != nil) { + return flatvariantidxt(sti, vt.type_: *tinfo); + }; + return -1; + }; // `is []T` / `as []T` — slice-shape lookup routes through the // element-aware helper, which carries the loose first-slice-shape // fallback (cstage type_assignable stand-in) that flatvariantidx's @@ -2644,7 +2659,9 @@ fn cgdot(c: *cgen, n: *node) void = { for (i < idx) { if (tp == nil) { i = idx; } else { - foff += slotsize(c, tp.lhs); + // C-t0/#22: slot stride + // (tupeslot accessor). + foff += tupeslotn(tp.lhs); tp = tp.next; i += 1; }; @@ -2678,6 +2695,25 @@ fn cgdot(c: *cgen, n: *node) void = { emitline("(BP), X0\n"); return; }; + // #22a: tagged element — load the box + // into the tagged value regs (AX=tag, + // DX/CX/R8=payload), the cursor the + // is/as spill + match read. Byte-id + // twin of cstage's N_DOT TY_TUPLE + // tagged arm. + if (istaggedtype(c, tpt)) { + let eslot: i32 = tupeslotn(tpt); + let k: i32 = 0; + for (k < eslot / 8) { + emitline("\tMOVQ\t"); + emitoff((lc.off + foff + k * 8): i64); + emitline("(BP), "); + emitline(tupreg(k)); + emitline("\n"); + k += 1; + }; + return; + }; // C-t0: load at the element's NATURAL // width (narrow MOVL/MOVSXD/... at the // slot base), not the 8B slot width — @@ -2848,7 +2884,9 @@ fn cgdot(c: *cgen, n: *node) void = { for (gi < gidx) { if (gtp == nil) { gi = gidx; } else { - gfoff += slotsize(c, gtp.lhs); + // C-t0/#22: slot stride + // (tupeslot accessor). + gfoff += tupeslotn(gtp.lhs); gtp = gtp.next; gi += 1; }; @@ -6038,7 +6076,9 @@ fn cgcall(c: *cgen, n: *node) void = { for (i < idx) { if (tp == nil) { i = idx; } else { - foff += slotsize(c, tp.lhs); + // C-t0/#22: slot + // stride (tupeslot). + foff += tupeslotn(tp.lhs); tp = tp.next; i += 1; }; @@ -6074,7 +6114,9 @@ fn cgcall(c: *cgen, n: *node) void = { for (gi < gidx) { if (gtp == nil) { gi = gidx; } else { - gfoff += slotsize(c, gtp.lhs); + // C-t0/#22: slot + // stride (tupeslot). + gfoff += tupeslotn(gtp.lhs); gtp = gtp.next; gi += 1; }; @@ -6527,10 +6569,14 @@ fn cgcall(c: *cgen, n: *node) void = { fpidx += 1; popped += 1; } else { + // tagged is guarded loud at the restage, so + // wide-vs-scalar is the full slot split here + // (#22 accessor scale). let wide: bool = false; if (tuplit) { wide = nodeisstr(c, et) || nodeisslice(c, et); } else { wide = isstrtype(c, et) || isslicetype(c, et); }; - let eb: i32 = tupebytes(wide); + let eb: i32 = 1; + if (wide) { eb = (tyslicesize() / 8i64): i32; }; if (intidx + eb > 6) { let msg: str = "tuple arg element overflows integer arg regs (DI/SI/DX/CX/R8/R9); stitch out of scope, see #163\n"; os.write(2, msg.ptr, msg.len: u64); diff --git a/selfhost/cmd/wcc/cgenstmt.ww b/selfhost/cmd/wcc/cgenstmt.ww index ba6dedf9..1672160e 100644 --- a/selfhost/cmd/wcc/cgenstmt.ww +++ b/selfhost/cmd/wcc/cgenstmt.ww @@ -132,9 +132,36 @@ fn tupsse(i: i32) str = { return "X1"; }; -fn tupebytes(wide: bool) i32 = { - if (wide) { return (tyslicesize() / 8i64): i32; }; - return 1; +// tupeslot — THE tuple element-stride accessor (#22): the slot a tuple +// element occupies, in bytes. slot = roundup8(size(elem)), 8B a FLOOR +// not a ceiling (user-ratified 2026-06-04): str/slice carry their 24B +// header, a tagged element its full tag+payload box ((str,str)=48B +// predates this; tagged was the one truncated >8B kind — the #237 +// fieldslotsize precedent), narrow scalars pad UP to one 8B eightbyte. +// Every tuple walk (cursor send/receive, t.N read, destructure, sret +// classify, DATA emit) takes its stride and its eightbyte count +// (eslot/8) from here — the per-site wide=(STR||SLICE)-else-8 +// predicates this absorbs were the #22 neighbor-slot/zeros miscompile. +// Checker twin: check.ww tupleelemslot / check.c N_TTUPLE; cstage twin: +// tuple_eslot (cmd/w6c/cgen.c). +export fn tupeslot(ti: *tinfo) i32 = { + let t: *tinfo = ti; + for (t != nil && t.kind == tykind.TY_NAMED) { t = t.under; }; + if (t == nil) { return 8; }; + if (t.kind == tykind.TY_VOID) { return 0; }; + // a literal tuple's stamped element can be untyped_str (size 0) — + // it occupies the str header slot (the C-t2 type_isstr lesson). + if (t.kind == tykind.TY_UNTYPED_STR) { return tyslicesize(): i32; }; + if (t.kind == tykind.TY_STR || t.kind == tykind.TY_SLICE || + t.kind == tykind.TY_TAGGED) { + return ((t.size + 7u64) & ~7u64): i32; + }; + return 8; +}; + +export fn tupeslotn(n: *node) i32 = { + if (n == nil) { return 8; }; + return tupeslot(n.type_: *tinfo); }; // rettupleof — the N_TTUPLE return-type node of an N_CALL rhs (else nil). @@ -201,29 +228,24 @@ fn nodetuplearg(c: *cgen, a: *node) *node = { }; // tupstore — store the tuple element at register-cursor `cur` into the -// BP-relative slot at `off`. A slice/str stores its 3-word {ptr,len,cap} -// header (ref/hare/rt/ensure.ha:4-8) at off/+8/+16 from consecutive -// INTEGER cursor registers; a float rides the SSE cursor (X0,X1); a -// scalar stores 1 INTEGER word. The caller owns the dual cursor -// (validated + advanced). Byte-identical to the cstage tuple_store -// (cmd/w6c/cgen.c). -fn tupstore(c: *cgen, gpcur: i32, ssecur: i32, off: i32, wide: bool, tn: *node) void = { - if (wide) { - emitline("\tMOVQ\t"); - emitline(tupreg(gpcur + 0)); - emitline(", "); - emitoff(off: i64); - emitline("(BP)\n"); - emitline("\tMOVQ\t"); - emitline(tupreg(gpcur + 1)); - emitline(", "); - emitoff((off + 8): i64); - emitline("(BP)\n"); - emitline("\tMOVQ\t"); - emitline(tupreg(gpcur + 2)); - emitline(", "); - emitoff((off + 16): i64); - emitline("(BP)\n"); +// BP-relative slot at `off`. A >8B element (slice/str 3-word +// {ptr,len,cap} header, ref/hare/rt/ensure.ha:4-8; tagged tag+payload +// box, #22) stores its eslot/8 words from consecutive INTEGER cursor +// registers; a float rides the SSE cursor (X0,X1); a scalar stores 1 +// INTEGER word. The caller owns the dual cursor (validated + +// advanced). Byte-identical to the cstage tuple_store (cmd/w6c/cgen.c). +fn tupstore(c: *cgen, gpcur: i32, ssecur: i32, off: i32, eslot: i32, tn: *node) void = { + if (eslot == 0) { return; }; // void element: the checker's 0-slot + if (eslot > 8) { + let k: i32 = 0; + for (k < eslot / 8) { + emitline("\tMOVQ\t"); + emitline(tupreg(gpcur + k)); + emitline(", "); + emitoff((off + k * 8): i64); + emitline("(BP)\n"); + k += 1; + }; return; }; // #105 / #164 (#107): an f64/f32 element rides the SSE cursor reg @@ -277,6 +299,67 @@ fn tupstore(c: *cgen, gpcur: i32, ssecur: i32, off: i32, wide: bool, tn: *node) emitline("(BP)\n"); }; +// tuplitgpwords — INTEGER cursor words an N_TUPLE literal element +// occupies. MUST mirror the literal push arms (tuplitpushelem) exactly +// — the count drives the POP fill, so a count/push skew silently +// shifts every later element (#22 class). A float rides the SSE row +// (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 = { + if (isfloattype(c, e)) { return 0; }; + if (nodeisstr(c, e) || nodeisslice(c, e)) { + return (tyslicesize() / 8i64): i32; + }; + 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 || + t.kind == tykind.TY_VOID)) { + return tupeslotn(e) / 8; + }; + return 1; +}; + +// tuplitpushelem — evaluate one N_TUPLE literal element and push its +// 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 is +// with #22b's tupstore work, widening literals with #23). Mirror of +// cstage tuple_lit_push_elem — count (tuplitgpwords) and push live or +// die together. +fn tuplitpushelem(c: *cgen, e: *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 eslot: i32 = tupeslotn(e); + let eoff: i32 = 0; + if (e.kind == nkind.N_IDENT) { eoff = localfind(c, e.str); }; + if (eoff == 0) { + let m22: str = "#22a: tagged tuple element from a non-local source shape unwired (ident locals only; rule 7, #22b/#23 follow-ups)\n"; + os.write(2, m22.ptr, m22.len: u64); + os.exit(1); + }; + let k: i32 = 0; + for (k < eslot / 8) { + emitline("\tMOVQ\t"); + emitoff((eoff + k * 8): i64); + emitline("(BP), AX\n"); + emitline("\tPUSHQ\tAX\n"); + k += 1; + }; + return; + }; + cgexpr(c, e); + if (t != nil && t.kind == tykind.TY_VOID) { return; }; + emitline("\tPUSHQ\tAX\n"); + if (nodeisstr(c, e) || nodeisslice(c, e)) { + emitline("\tPUSHQ\tBX\n"); + emitline("\tPUSHQ\tCX\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 @@ -296,8 +379,7 @@ fn cgtuplelittocursor(c: *cgen, tuple: *node) void = { if (isfloattype(c, e)) { ssecount = ssecount + 1; } else { - let wide: bool = nodeisstr(c, e) || nodeisslice(c, e); - gptotal = gptotal + tupebytes(wide); + gptotal = gptotal + tuplitgpwords(c, e); }; e = e.next; }; @@ -314,8 +396,8 @@ fn cgtuplelittocursor(c: *cgen, tuple: *node) void = { e = tuple.list; for (e != nil) { let isflt: bool = isfloattype(c, e); - cgexpr(c, e); if (isflt) { + cgexpr(c, e); let mov: str = "MOVSD"; if (isf32type(c, e)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\tX0, "); @@ -323,11 +405,7 @@ fn cgtuplelittocursor(c: *cgen, tuple: *node) void = { 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"); - }; + tuplitpushelem(c, e); }; e = e.next; }; @@ -374,8 +452,7 @@ fn cgtupleslottocursor(c: *cgen, srcoff: i32, tu: *tinfo) void = { 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); + gptotal = gptotal + tupeslot(el.type_) / 8; }; el = el.tnext; }; @@ -392,7 +469,7 @@ fn cgtupleslottocursor(c: *cgen, srcoff: i32, tu: *tinfo) void = { 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); + let eslot: i32 = tupeslot(el.type_); if (isflt) { let mov: str = "MOVSD"; if (et.kind == tykind.TY_F32) { mov = "MOVSS"; }; @@ -403,9 +480,9 @@ fn cgtupleslottocursor(c: *cgen, srcoff: i32, tu: *tinfo) void = { emitline("\n"); sse = sse + 1; foff += 8; - } else { if (wide) { + } else { let k: i32 = 0; - for (k < 3) { + for (k < eslot / 8) { emitline("\tMOVQ\t"); emitoff((srcoff + foff + k * 8): i64); emitline("(BP), "); @@ -413,17 +490,9 @@ fn cgtupleslottocursor(c: *cgen, srcoff: i32, tu: *tinfo) void = { 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; - }; }; + gp += eslot / 8; + foff += eslot; + }; el = el.tnext; }; }; @@ -443,9 +512,8 @@ fn cgtaggedtuplepayloadshift(c: *cgen, tup: *tinfo) void = { 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"; + if (isflt || tupeslot(el.type_) != 8) { + let msg: str = "tuple-in-union ? unwrap: float/slice/str/tagged 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); }; @@ -502,12 +570,28 @@ fn cgreturn(c: *cgen, n: *node) void = { if (isfloattype(c, e)) { ssecount = ssecount + 1; } else { - let wide: bool = nodeisstr(c, e) || nodeisslice(c, e); - gptotal = gptotal + tupebytes(wide); + gptotal = gptotal + tuplitgpwords(c, e); }; e = e.next; }; if (gptotal > TUPLE_GPCAP || ssecount > ssecap) { + // #22a (rule 7): a tagged element's box can't + // ride the scalar/wide store arms below — the + // MEMORY-class tagged element store is #22b + // (task #28); pre-guard it silently stored + // word0 of the box. + let gq: *node = nil; + if (c.fnret != nil) { gq = c.fnret.list; }; + for (gq != nil) { + let gqt: *tinfo = gq.lhs.type_: *tinfo; + for (gqt != nil && gqt.kind == tykind.TY_NAMED) { gqt = gqt.under; }; + if (gqt != nil && gqt.kind == tykind.TY_TAGGED) { + let m22b: str = "#22b: tagged element in an over-cap (sret) tuple return unwired (rule 7)\n"; + os.write(2, m22b.ptr, m22b.len: u64); + os.exit(1); + }; + gq = gq.next; + }; // #10 Fold A: over-cap tuple returns via sret. The // prologue wired @sretarg (sretretsize agrees on the // caps — TUPLE_GPCAP/TUPLE_SSECAP, the shared SSoT), @@ -567,7 +651,12 @@ fn cgreturn(c: *cgen, n: *node) void = { emitline("\n"); }; }; - foff += esz; + // C-t0: the sret buffer is slot-laid like + // every tuple home (checker size, t.N + // reader, mlet receive agree); esz keeps + // the store WIDTH natural. Mirrors cstage + // cgen.c N_RETURN over-cap arm. + if (wide) { foff += esz; } else { foff += 8; }; we = we.next; if (pt != nil) { pt = pt.next; }; }; @@ -588,8 +677,8 @@ fn cgreturn(c: *cgen, n: *node) void = { e = rhs.list; for (e != nil) { let isflt: bool = isfloattype(c, e); - cgexpr(c, e); if (isflt) { + cgexpr(c, e); let mov: str = "MOVSD"; if (isf32type(c, e)) { mov = "MOVSS"; }; emitline("\t"); @@ -599,11 +688,9 @@ fn cgreturn(c: *cgen, n: *node) void = { emitline("(BP)\n"); sseidx = sseidx + 1; } else { - emitline("\tPUSHQ\tAX\n"); // scalar / .ptr - if (nodeisstr(c, e) || nodeisslice(c, e)) { - emitline("\tPUSHQ\tBX\n"); // .len - emitline("\tPUSHQ\tCX\n"); // .cap - }; + // scalar=AX; slice/str=AX,BX,CX; tagged + // box from its slot (tuplitpushelem) + tuplitpushelem(c, e); }; e = e.next; }; @@ -2044,20 +2131,15 @@ fn cglet(c: *cgen, n: *node) void = { for (q != nil) { let qt: *node = q.lhs; let isflt: bool = isfloattype(c, qt); - let wide: bool = isstrtype(c, qt) - || isslicetype(c, qt); + let eslot: i32 = tupeslotn(qt); tupstore(c, gpcur, ssecur, - off + eoff, wide, qt); + off + eoff, eslot, qt); if (isflt) { ssecur = ssecur + 1; } else { - gpcur = gpcur + tupebytes(wide); - }; - if (wide) { - eoff = eoff + (tyslicesize(): i32); - } else { - eoff = eoff + 8; + gpcur = gpcur + eslot / 8; }; + eoff = eoff + eslot; q = q.next; }; c.lastwasreturn = 0; @@ -2898,7 +2980,10 @@ fn cgmassign(c: *cgen, n: *node) void = { }; }; }; - foff += esz; + // C-t0: slot stride — must mirror the N_RETURN + // over-cap SEND's buffer layout (cstage N_MASSIGN + // twin strides tuple_eslot). + foff += tupeslotn(tn); lb = lb.next; if (pt2 != nil) { pt2 = pt2.next; }; }; @@ -2918,8 +3003,7 @@ fn cgmassign(c: *cgen, n: *node) void = { if (isfloattype(c, tn)) { ssetotal = ssetotal + 1; } else { - let wide: bool = isstrtype(c, tn) || isslicetype(c, tn); - gptotal = gptotal + tupebytes(wide); + gptotal = gptotal + tupeslotn(tn) / 8; }; l = l.next; if (pt != nil) { pt = pt.next; }; @@ -2946,18 +3030,18 @@ fn cgmassign(c: *cgen, n: *node) void = { let tn: *node = nil; if (pt != nil) { tn = pt.lhs; }; let isflt: bool = isfloattype(c, tn); - let wide: bool = isstrtype(c, tn) || isslicetype(c, tn); + let eslot: i32 = tupeslotn(tn); let off: i32 = 0; if (l.kind == nkind.N_IDENT) { off = localfind(c, l.str); }; // harec `_` (off==0): skip the store but CONSUME the cursor // slot so the next element stays aligned. if (off != 0) { - tupstore(c, gpcur, ssecur, off, wide, tn); + tupstore(c, gpcur, ssecur, off, eslot, tn); }; if (isflt) { ssecur = ssecur + 1; } else { - gpcur = gpcur + tupebytes(wide); + gpcur = gpcur + eslot / 8; }; l = l.next; if (pt != nil) { pt = pt.next; }; @@ -3020,13 +3104,13 @@ fn cgmlet(c: *cgen, n: *node) void = { for (lb != nil) { let tn: *node = lb.lhs; let isflt: bool = isfloattype(c, tn); - let wide: bool = isstrtype(c, tn) || isslicetype(c, tn); + let eslot: i32 = tupeslotn(tn); let esz: i32 = 8; let eti: *tinfo = nil; if (tn != nil) { eti = tn.type_: *tinfo; }; if (eti != nil) { esz = eti.size: i32; }; let bsz: i32 = 8; - if (wide) { bsz = tyslicesize(): i32; }; + if (eslot > 8) { bsz = eslot; }; let off: i32 = localadd(c, lb.str, bsz, tn); if (isflt) { let mov: str = "MOVSD"; @@ -3036,9 +3120,9 @@ fn cgmlet(c: *cgen, n: *node) void = { emitline("(BP), X0\n"); emitline("\t"); emitline(mov); emitline("\tX0, "); emitoff(off: i64); emitline("(BP)\n"); - } else { if (wide) { + } else { if (eslot > 8) { let k: i32 = 0; - for (k < esz) { + for (k < eslot) { emitline("\tMOVQ\t"); emitoff((srcoff + foff + k): i64); emitline("(BP), AX\n"); @@ -3056,7 +3140,7 @@ fn cgmlet(c: *cgen, n: *node) void = { emitline("\t"); emitline(sop); emitline("\tAX, "); emitoff(off: i64); emitline("(BP)\n"); }; }; - if (wide) { foff += 24; } else { foff += 8; }; + foff += eslot; lb = lb.next; }; c.lastwasreturn = 0; @@ -3077,14 +3161,14 @@ fn cgmlet(c: *cgen, n: *node) void = { let tn: *node = nil; if (pt2 != nil) { tn = pt2.lhs; }; let isflt: bool = isfloattype(c, tn); - let wide: bool = isstrtype(c, tn) || isslicetype(c, tn); + let eslot: i32 = tupeslotn(tn); let esz: i32 = 8; if (pt2 != nil) { let eti: *tinfo = pt2.lhs.type_: *tinfo; if (eti != nil) { esz = eti.size: i32; }; }; let bsz: i32 = 8; - if (wide) { bsz = tyslicesize(): i32; }; + if (eslot > 8) { bsz = eslot; }; let off: i32 = localadd(c, lb.str, bsz, tn); if (isflt) { let mov: str = "MOVSD"; @@ -3095,9 +3179,9 @@ fn cgmlet(c: *cgen, n: *node) void = { emitline("\t"); emitline(mov); emitline("\tX0, "); emitoff(off: i64); emitline("(BP)\n"); } else { - if (wide) { + if (eslot > 8) { let k: i32 = 0; - for (k < esz) { + for (k < eslot) { emitline("\tMOVQ\t"); emitoff((scr + foff + k): i64); emitline("(BP), AX\n"); @@ -3117,7 +3201,7 @@ fn cgmlet(c: *cgen, n: *node) void = { emitoff(off: i64); emitline("(BP)\n"); }; }; - foff += esz; + foff += eslot; lb = lb.next; if (pt2 != nil) { pt2 = pt2.next; }; }; @@ -3139,8 +3223,7 @@ fn cgmlet(c: *cgen, n: *node) void = { if (isfloattype(c, tn)) { ssetotal = ssetotal + 1; } else { - let wide: bool = isstrtype(c, tn) || isslicetype(c, tn); - gptotal = gptotal + tupebytes(wide); + gptotal = gptotal + tupeslotn(tn) / 8; }; l = l.next; if (pt != nil) { pt = pt.next; }; @@ -3169,15 +3252,15 @@ fn cgmlet(c: *cgen, n: *node) void = { if (pt != nil) { tn = pt.lhs; }; }; let isflt: bool = isfloattype(c, tn); - let wide: bool = isstrtype(c, tn) || isslicetype(c, tn); + let eslot: i32 = tupeslotn(tn); let sz: i32 = 8; - if (wide) { sz = tyslicesize(): i32; }; + if (eslot > 8) { sz = eslot; }; let off: i32 = localadd(c, l.str, sz, tn); - tupstore(c, gpcur, ssecur, off, wide, tn); + tupstore(c, gpcur, ssecur, off, eslot, tn); if (isflt) { ssecur = ssecur + 1; } else { - gpcur = gpcur + tupebytes(wide); + gpcur = gpcur + eslot / 8; }; l = l.next; if (pt != nil) { pt = pt.next; }; diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index 104613b5..a31c8742 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -288,6 +288,22 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node, memphase: bool) i32 = { }; }; }; + // #22a: t.N tuple-element read leaves the + // same AX/DX/CX/R8 box cursor (this arc's + // t.N box load) — without this gate the + // widening scalar branch clamps the + // unresolvable tag to 0 and the callee + // reads variant 0. Stamped-carrier (#67) + // twin of the N_INDEX arm above; cstage + // needs no kind gate (its widen[i] `same` + // check is type-keyed on args[i]->type). + if (arg.kind == nkind.N_DOT) { + if (istaggedtype(c, arg)) { + if (slotsize(c, arg) == slotsize(c, ptype)) { + aistagged = true; + }; + }; + }; if (!aistagged) { widensz = slotsize(c, ptype); let tagged: *node = resolvetagged(c, ptype); @@ -766,13 +782,17 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node, memphase: bool) i32 = { os.exit(1); }; }; + // tagged is guarded loud above, so wide-vs-scalar is + // the full slot split here; eslot keeps the stride + // arithmetic on the accessor scale (#22). let wide: bool = false; if (tuplit) { wide = nodeisstr(c, et) || nodeisslice(c, et); } else { wide = isstrtype(c, et) || isslicetype(c, et); }; + let eslot: i32 = 8; + if (wide) { eslot = tyslicesize(): i32; }; if (isfloattype(c, et)) { sstot += 1; } - else { gptot += tupebytes(wide); }; - if (wide) { tsz += (tyslicesize(): i32); } - else { tsz += 8; }; + else { gptot += eslot / 8; }; + tsz += eslot; p = p.next; }; // The producing cursor fill already satisfied #164's caps; @@ -793,11 +813,12 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node, memphase: bool) i32 = { let wide: bool = false; if (tuplit) { wide = nodeisstr(c, et) || nodeisslice(c, et); } else { wide = isstrtype(c, et) || isslicetype(c, et); }; - tupstore(c, gpcur, ssecur, scr + eoff, wide, et); + let eslot: i32 = 8; + if (wide) { eslot = tyslicesize(): i32; }; + tupstore(c, gpcur, ssecur, scr + eoff, eslot, et); if (isfloattype(c, et)) { ssecur += 1; } - else { gpcur += tupebytes(wide); }; - if (wide) { eoff += (tyslicesize(): i32); } - else { eoff += 8; }; + else { gpcur += eslot / 8; }; + eoff += eslot; p = p.next; }; let w: i32 = tsz - 8; @@ -870,7 +891,13 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node, memphase: bool) i32 = { // arg.type_ is the element tinfo (istaggedtype/slotsize read // .type_) — feed the N_INDEX node directly, dropping the // indexvaluetnode walk. - if (arg.kind == nkind.N_INDEX) { + // #22a: N_DOT rides the same arm — the t.N tuple-element box load + // (this arc) fills the identical AX/DX/CX/R8 cursor. cstage's twin + // is the generic node_istaggedarg push (cgen.c cgcall); wwstage + // keeps the stamped-carrier kind gate (#67 pattern) — the + // remaining kinds (deref/cast/unwrap) are word0-only reads today, + // filed residual. + if (arg.kind == nkind.N_INDEX || arg.kind == nkind.N_DOT) { if (istaggedtype(c, arg)) { let isz: i32 = slotsize(c, arg); if (isz > 24) { emitline("\tPUSHQ\tR8\n"); }; @@ -1676,8 +1703,7 @@ export fn sretretsize(c: *cgen, t: *node) i32 = { if (isfloattype(c, et)) { ssecount = ssecount + 1; } else { - let wide: bool = isstrtype(c, et) || isslicetype(c, et); - gptotal = gptotal + tupebytes(wide); + gptotal = gptotal + tupeslotn(et) / 8; }; pt = pt.next; }; @@ -3453,6 +3479,20 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *tinfo, src: *node, slot_off: i32, slot_s let ttotal: i32 = 0; let ce: *node = tupsrc.list; for (ce != nil) { + // #22a (rule 7): a tagged element's box can't + // ride the scalar/wide store arms below — + // pre-guard it silently stored word0. Nested + // tagged-in-tuple-in-union packing is the + // #242/#22b family. Mirrors cstage. + let ceti: *tinfo = ce.type_: *tinfo; + for (ceti != nil && ceti.kind == tykind.TY_NAMED) { + ceti = ceti.under; + }; + if (ceti != nil && ceti.kind == tykind.TY_TAGGED) { + let m22: str = "cgwidentaggedstore: tagged element in a tuple-in-union payload unwired (see #242/#22b)\n"; + os.write(2, m22.ptr, m22.len: u64); + os.exit(1); + }; if (nodeisstr(c, ce) || nodeisslice(c, ce)) { ttotal += 24; } else { ttotal += 8; }; diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index 8020727d..a6c95419 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -1600,6 +1600,13 @@ fn tupleelemslot(pt: *tinfo) u64 = { if (pk == tykind.TY_VOID) { return 0u64; }; if (pk == tykind.TY_STR) { return t.size; }; if (pk == tykind.TY_SLICE) { return t.size; }; + // #22 (user-ratified 2026-06-04): slot = roundup8(size(elem)) — 8B + // is a FLOOR, not a ceiling. (str,str)=48B predates this; tagged + // was the one truncated >8B kind (the #237 fieldslotsize-missing- + // TY_TUPLE precedent: fieldslotsize below already carried this + // arm). Cstage twin: check.c N_TTUPLE; cgen accessor: tuple_eslot + // / tupeslot. + if (pk == tykind.TY_TAGGED) { return (t.size + 7u64) & ~7u64; }; if (pk == tykind.TY_PTR || pk == tykind.TY_FN || pk == tykind.TY_CHAN || pk == tykind.TY_I64 || pk == tykind.TY_U64 || pk == tykind.TY_INT || diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index b2c4e737..2b46e9c8 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -11864,6 +11864,13 @@ fn tupleelemslot(pt: *tinfo) u64 = { if (pk == tykind.TY_VOID) { return 0u64; }; if (pk == tykind.TY_STR) { return t.size; }; if (pk == tykind.TY_SLICE) { return t.size; }; + // #22 (user-ratified 2026-06-04): slot = roundup8(size(elem)) — 8B + // is a FLOOR, not a ceiling. (str,str)=48B predates this; tagged + // was the one truncated >8B kind (the #237 fieldslotsize-missing- + // TY_TUPLE precedent: fieldslotsize below already carried this + // arm). Cstage twin: check.c N_TTUPLE; cgen accessor: tuple_eslot + // / tupeslot. + if (pk == tykind.TY_TAGGED) { return (t.size + 7u64) & ~7u64; }; if (pk == tykind.TY_PTR || pk == tykind.TY_FN || pk == tykind.TY_CHAN || pk == tykind.TY_I64 || pk == tykind.TY_U64 || pk == tykind.TY_INT || @@ -16273,6 +16280,22 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node, memphase: bool) i32 = { }; }; }; + // #22a: t.N tuple-element read leaves the + // same AX/DX/CX/R8 box cursor (this arc's + // t.N box load) — without this gate the + // widening scalar branch clamps the + // unresolvable tag to 0 and the callee + // reads variant 0. Stamped-carrier (#67) + // twin of the N_INDEX arm above; cstage + // needs no kind gate (its widen[i] `same` + // check is type-keyed on args[i]->type). + if (arg.kind == nkind.N_DOT) { + if (istaggedtype(c, arg)) { + if (slotsize(c, arg) == slotsize(c, ptype)) { + aistagged = true; + }; + }; + }; if (!aistagged) { widensz = slotsize(c, ptype); let tagged: *node = resolvetagged(c, ptype); @@ -16751,13 +16774,17 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node, memphase: bool) i32 = { os.exit(1); }; }; + // tagged is guarded loud above, so wide-vs-scalar is + // the full slot split here; eslot keeps the stride + // arithmetic on the accessor scale (#22). let wide: bool = false; if (tuplit) { wide = nodeisstr(c, et) || nodeisslice(c, et); } else { wide = isstrtype(c, et) || isslicetype(c, et); }; + let eslot: i32 = 8; + if (wide) { eslot = tyslicesize(): i32; }; if (isfloattype(c, et)) { sstot += 1; } - else { gptot += tupebytes(wide); }; - if (wide) { tsz += (tyslicesize(): i32); } - else { tsz += 8; }; + else { gptot += eslot / 8; }; + tsz += eslot; p = p.next; }; // The producing cursor fill already satisfied #164's caps; @@ -16778,11 +16805,12 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node, memphase: bool) i32 = { let wide: bool = false; if (tuplit) { wide = nodeisstr(c, et) || nodeisslice(c, et); } else { wide = isstrtype(c, et) || isslicetype(c, et); }; - tupstore(c, gpcur, ssecur, scr + eoff, wide, et); + let eslot: i32 = 8; + if (wide) { eslot = tyslicesize(): i32; }; + tupstore(c, gpcur, ssecur, scr + eoff, eslot, et); if (isfloattype(c, et)) { ssecur += 1; } - else { gpcur += tupebytes(wide); }; - if (wide) { eoff += (tyslicesize(): i32); } - else { eoff += 8; }; + else { gpcur += eslot / 8; }; + eoff += eslot; p = p.next; }; let w: i32 = tsz - 8; @@ -16855,7 +16883,13 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node, memphase: bool) i32 = { // arg.type_ is the element tinfo (istaggedtype/slotsize read // .type_) — feed the N_INDEX node directly, dropping the // indexvaluetnode walk. - if (arg.kind == nkind.N_INDEX) { + // #22a: N_DOT rides the same arm — the t.N tuple-element box load + // (this arc) fills the identical AX/DX/CX/R8 cursor. cstage's twin + // is the generic node_istaggedarg push (cgen.c cgcall); wwstage + // keeps the stamped-carrier kind gate (#67 pattern) — the + // remaining kinds (deref/cast/unwrap) are word0-only reads today, + // filed residual. + if (arg.kind == nkind.N_INDEX || arg.kind == nkind.N_DOT) { if (istaggedtype(c, arg)) { let isz: i32 = slotsize(c, arg); if (isz > 24) { emitline("\tPUSHQ\tR8\n"); }; @@ -17661,8 +17695,7 @@ export fn sretretsize(c: *cgen, t: *node) i32 = { if (isfloattype(c, et)) { ssecount = ssecount + 1; } else { - let wide: bool = isstrtype(c, et) || isslicetype(c, et); - gptotal = gptotal + tupebytes(wide); + gptotal = gptotal + tupeslotn(et) / 8; }; pt = pt.next; }; @@ -19438,6 +19471,20 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *tinfo, src: *node, slot_off: i32, slot_s let ttotal: i32 = 0; let ce: *node = tupsrc.list; for (ce != nil) { + // #22a (rule 7): a tagged element's box can't + // ride the scalar/wide store arms below — + // pre-guard it silently stored word0. Nested + // tagged-in-tuple-in-union packing is the + // #242/#22b family. Mirrors cstage. + let ceti: *tinfo = ce.type_: *tinfo; + for (ceti != nil && ceti.kind == tykind.TY_NAMED) { + ceti = ceti.under; + }; + if (ceti != nil && ceti.kind == tykind.TY_TAGGED) { + let m22: str = "cgwidentaggedstore: tagged element in a tuple-in-union payload unwired (see #242/#22b)\n"; + os.write(2, m22.ptr, m22.len: u64); + os.exit(1); + }; if (nodeisstr(c, ce) || nodeisslice(c, ce)) { ttotal += 24; } else { ttotal += 8; }; @@ -20570,7 +20617,22 @@ fn cgexpr(c: *cgen, n: *node) void = { fn cgtagvariantidx(c: *cgen, tagged: *node, vt: *node) i32 = { if (tagged == nil) { return -1; }; if (vt == nil) { return -1; }; - if (tagged.kind != nkind.N_TTAGGED) { return -1; }; + if (tagged.kind != nkind.N_TTAGGED) { + // #22a: a STAMPED-CARRIER scrutinee (the #67 matchscrutt + // shape — is/as on a tuple element t.N, a struct field, an + // indexed element) is not an N_TTAGGED type-AST node; its + // tagged type rides .type_. Resolve via the tinfo twin + // (flatvariantidxt), the same core the widen-store uses — + // cstage cg_tag_for_variant is type-based for every + // scrutinee shape, so the AST-keyed -1 here was a silent + // tag-0 clamp on wwstage (cs CMPQ $1 vs ww CMPQ $0). + let sti: *tinfo = tagged.type_: *tinfo; + for (sti != nil && sti.kind == tykind.TY_NAMED) { sti = sti.under; }; + if (sti != nil && sti.kind == tykind.TY_TAGGED && vt.type_ != nil) { + return flatvariantidxt(sti, vt.type_: *tinfo); + }; + return -1; + }; // `is []T` / `as []T` — slice-shape lookup routes through the // element-aware helper, which carries the loose first-slice-shape // fallback (cstage type_assignable stand-in) that flatvariantidx's @@ -23080,7 +23142,9 @@ fn cgdot(c: *cgen, n: *node) void = { for (i < idx) { if (tp == nil) { i = idx; } else { - foff += slotsize(c, tp.lhs); + // C-t0/#22: slot stride + // (tupeslot accessor). + foff += tupeslotn(tp.lhs); tp = tp.next; i += 1; }; @@ -23114,6 +23178,25 @@ fn cgdot(c: *cgen, n: *node) void = { emitline("(BP), X0\n"); return; }; + // #22a: tagged element — load the box + // into the tagged value regs (AX=tag, + // DX/CX/R8=payload), the cursor the + // is/as spill + match read. Byte-id + // twin of cstage's N_DOT TY_TUPLE + // tagged arm. + if (istaggedtype(c, tpt)) { + let eslot: i32 = tupeslotn(tpt); + let k: i32 = 0; + for (k < eslot / 8) { + emitline("\tMOVQ\t"); + emitoff((lc.off + foff + k * 8): i64); + emitline("(BP), "); + emitline(tupreg(k)); + emitline("\n"); + k += 1; + }; + return; + }; // C-t0: load at the element's NATURAL // width (narrow MOVL/MOVSXD/... at the // slot base), not the 8B slot width — @@ -23284,7 +23367,9 @@ fn cgdot(c: *cgen, n: *node) void = { for (gi < gidx) { if (gtp == nil) { gi = gidx; } else { - gfoff += slotsize(c, gtp.lhs); + // C-t0/#22: slot stride + // (tupeslot accessor). + gfoff += tupeslotn(gtp.lhs); gtp = gtp.next; gi += 1; }; @@ -26474,7 +26559,9 @@ fn cgcall(c: *cgen, n: *node) void = { for (i < idx) { if (tp == nil) { i = idx; } else { - foff += slotsize(c, tp.lhs); + // C-t0/#22: slot + // stride (tupeslot). + foff += tupeslotn(tp.lhs); tp = tp.next; i += 1; }; @@ -26510,7 +26597,9 @@ fn cgcall(c: *cgen, n: *node) void = { for (gi < gidx) { if (gtp == nil) { gi = gidx; } else { - gfoff += slotsize(c, gtp.lhs); + // C-t0/#22: slot + // stride (tupeslot). + gfoff += tupeslotn(gtp.lhs); gtp = gtp.next; gi += 1; }; @@ -26963,10 +27052,14 @@ fn cgcall(c: *cgen, n: *node) void = { fpidx += 1; popped += 1; } else { + // tagged is guarded loud at the restage, so + // wide-vs-scalar is the full slot split here + // (#22 accessor scale). let wide: bool = false; if (tuplit) { wide = nodeisstr(c, et) || nodeisslice(c, et); } else { wide = isstrtype(c, et) || isslicetype(c, et); }; - let eb: i32 = tupebytes(wide); + let eb: i32 = 1; + if (wide) { eb = (tyslicesize() / 8i64): i32; }; if (intidx + eb > 6) { let msg: str = "tuple arg element overflows integer arg regs (DI/SI/DX/CX/R8/R9); stitch out of scope, see #163\n"; os.write(2, msg.ptr, msg.len: u64); @@ -31050,9 +31143,36 @@ fn tupsse(i: i32) str = { return "X1"; }; -fn tupebytes(wide: bool) i32 = { - if (wide) { return (tyslicesize() / 8i64): i32; }; - return 1; +// tupeslot — THE tuple element-stride accessor (#22): the slot a tuple +// element occupies, in bytes. slot = roundup8(size(elem)), 8B a FLOOR +// not a ceiling (user-ratified 2026-06-04): str/slice carry their 24B +// header, a tagged element its full tag+payload box ((str,str)=48B +// predates this; tagged was the one truncated >8B kind — the #237 +// fieldslotsize precedent), narrow scalars pad UP to one 8B eightbyte. +// Every tuple walk (cursor send/receive, t.N read, destructure, sret +// classify, DATA emit) takes its stride and its eightbyte count +// (eslot/8) from here — the per-site wide=(STR||SLICE)-else-8 +// predicates this absorbs were the #22 neighbor-slot/zeros miscompile. +// Checker twin: check.ww tupleelemslot / check.c N_TTUPLE; cstage twin: +// tuple_eslot (cmd/w6c/cgen.c). +export fn tupeslot(ti: *tinfo) i32 = { + let t: *tinfo = ti; + for (t != nil && t.kind == tykind.TY_NAMED) { t = t.under; }; + if (t == nil) { return 8; }; + if (t.kind == tykind.TY_VOID) { return 0; }; + // a literal tuple's stamped element can be untyped_str (size 0) — + // it occupies the str header slot (the C-t2 type_isstr lesson). + if (t.kind == tykind.TY_UNTYPED_STR) { return tyslicesize(): i32; }; + if (t.kind == tykind.TY_STR || t.kind == tykind.TY_SLICE || + t.kind == tykind.TY_TAGGED) { + return ((t.size + 7u64) & ~7u64): i32; + }; + return 8; +}; + +export fn tupeslotn(n: *node) i32 = { + if (n == nil) { return 8; }; + return tupeslot(n.type_: *tinfo); }; // rettupleof — the N_TTUPLE return-type node of an N_CALL rhs (else nil). @@ -31119,29 +31239,24 @@ fn nodetuplearg(c: *cgen, a: *node) *node = { }; // tupstore — store the tuple element at register-cursor `cur` into the -// BP-relative slot at `off`. A slice/str stores its 3-word {ptr,len,cap} -// header (ref/hare/rt/ensure.ha:4-8) at off/+8/+16 from consecutive -// INTEGER cursor registers; a float rides the SSE cursor (X0,X1); a -// scalar stores 1 INTEGER word. The caller owns the dual cursor -// (validated + advanced). Byte-identical to the cstage tuple_store -// (cmd/w6c/cgen.c). -fn tupstore(c: *cgen, gpcur: i32, ssecur: i32, off: i32, wide: bool, tn: *node) void = { - if (wide) { - emitline("\tMOVQ\t"); - emitline(tupreg(gpcur + 0)); - emitline(", "); - emitoff(off: i64); - emitline("(BP)\n"); - emitline("\tMOVQ\t"); - emitline(tupreg(gpcur + 1)); - emitline(", "); - emitoff((off + 8): i64); - emitline("(BP)\n"); - emitline("\tMOVQ\t"); - emitline(tupreg(gpcur + 2)); - emitline(", "); - emitoff((off + 16): i64); - emitline("(BP)\n"); +// BP-relative slot at `off`. A >8B element (slice/str 3-word +// {ptr,len,cap} header, ref/hare/rt/ensure.ha:4-8; tagged tag+payload +// box, #22) stores its eslot/8 words from consecutive INTEGER cursor +// registers; a float rides the SSE cursor (X0,X1); a scalar stores 1 +// INTEGER word. The caller owns the dual cursor (validated + +// advanced). Byte-identical to the cstage tuple_store (cmd/w6c/cgen.c). +fn tupstore(c: *cgen, gpcur: i32, ssecur: i32, off: i32, eslot: i32, tn: *node) void = { + if (eslot == 0) { return; }; // void element: the checker's 0-slot + if (eslot > 8) { + let k: i32 = 0; + for (k < eslot / 8) { + emitline("\tMOVQ\t"); + emitline(tupreg(gpcur + k)); + emitline(", "); + emitoff((off + k * 8): i64); + emitline("(BP)\n"); + k += 1; + }; return; }; // #105 / #164 (#107): an f64/f32 element rides the SSE cursor reg @@ -31195,6 +31310,67 @@ fn tupstore(c: *cgen, gpcur: i32, ssecur: i32, off: i32, wide: bool, tn: *node) emitline("(BP)\n"); }; +// tuplitgpwords — INTEGER cursor words an N_TUPLE literal element +// occupies. MUST mirror the literal push arms (tuplitpushelem) exactly +// — the count drives the POP fill, so a count/push skew silently +// shifts every later element (#22 class). A float rides the SSE row +// (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 = { + if (isfloattype(c, e)) { return 0; }; + if (nodeisstr(c, e) || nodeisslice(c, e)) { + return (tyslicesize() / 8i64): i32; + }; + 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 || + t.kind == tykind.TY_VOID)) { + return tupeslotn(e) / 8; + }; + return 1; +}; + +// tuplitpushelem — evaluate one N_TUPLE literal element and push its +// 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 is +// with #22b's tupstore work, widening literals with #23). Mirror of +// cstage tuple_lit_push_elem — count (tuplitgpwords) and push live or +// die together. +fn tuplitpushelem(c: *cgen, e: *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 eslot: i32 = tupeslotn(e); + let eoff: i32 = 0; + if (e.kind == nkind.N_IDENT) { eoff = localfind(c, e.str); }; + if (eoff == 0) { + let m22: str = "#22a: tagged tuple element from a non-local source shape unwired (ident locals only; rule 7, #22b/#23 follow-ups)\n"; + os.write(2, m22.ptr, m22.len: u64); + os.exit(1); + }; + let k: i32 = 0; + for (k < eslot / 8) { + emitline("\tMOVQ\t"); + emitoff((eoff + k * 8): i64); + emitline("(BP), AX\n"); + emitline("\tPUSHQ\tAX\n"); + k += 1; + }; + return; + }; + cgexpr(c, e); + if (t != nil && t.kind == tykind.TY_VOID) { return; }; + emitline("\tPUSHQ\tAX\n"); + if (nodeisstr(c, e) || nodeisslice(c, e)) { + emitline("\tPUSHQ\tBX\n"); + emitline("\tPUSHQ\tCX\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 @@ -31214,8 +31390,7 @@ fn cgtuplelittocursor(c: *cgen, tuple: *node) void = { if (isfloattype(c, e)) { ssecount = ssecount + 1; } else { - let wide: bool = nodeisstr(c, e) || nodeisslice(c, e); - gptotal = gptotal + tupebytes(wide); + gptotal = gptotal + tuplitgpwords(c, e); }; e = e.next; }; @@ -31232,8 +31407,8 @@ fn cgtuplelittocursor(c: *cgen, tuple: *node) void = { e = tuple.list; for (e != nil) { let isflt: bool = isfloattype(c, e); - cgexpr(c, e); if (isflt) { + cgexpr(c, e); let mov: str = "MOVSD"; if (isf32type(c, e)) { mov = "MOVSS"; }; emitline("\t"); emitline(mov); emitline("\tX0, "); @@ -31241,11 +31416,7 @@ fn cgtuplelittocursor(c: *cgen, tuple: *node) void = { 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"); - }; + tuplitpushelem(c, e); }; e = e.next; }; @@ -31292,8 +31463,7 @@ fn cgtupleslottocursor(c: *cgen, srcoff: i32, tu: *tinfo) void = { 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); + gptotal = gptotal + tupeslot(el.type_) / 8; }; el = el.tnext; }; @@ -31310,7 +31480,7 @@ fn cgtupleslottocursor(c: *cgen, srcoff: i32, tu: *tinfo) void = { 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); + let eslot: i32 = tupeslot(el.type_); if (isflt) { let mov: str = "MOVSD"; if (et.kind == tykind.TY_F32) { mov = "MOVSS"; }; @@ -31321,9 +31491,9 @@ fn cgtupleslottocursor(c: *cgen, srcoff: i32, tu: *tinfo) void = { emitline("\n"); sse = sse + 1; foff += 8; - } else { if (wide) { + } else { let k: i32 = 0; - for (k < 3) { + for (k < eslot / 8) { emitline("\tMOVQ\t"); emitoff((srcoff + foff + k * 8): i64); emitline("(BP), "); @@ -31331,17 +31501,9 @@ fn cgtupleslottocursor(c: *cgen, srcoff: i32, tu: *tinfo) void = { 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; - }; }; + gp += eslot / 8; + foff += eslot; + }; el = el.tnext; }; }; @@ -31361,9 +31523,8 @@ fn cgtaggedtuplepayloadshift(c: *cgen, tup: *tinfo) void = { 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"; + if (isflt || tupeslot(el.type_) != 8) { + let msg: str = "tuple-in-union ? unwrap: float/slice/str/tagged 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); }; @@ -31420,12 +31581,28 @@ fn cgreturn(c: *cgen, n: *node) void = { if (isfloattype(c, e)) { ssecount = ssecount + 1; } else { - let wide: bool = nodeisstr(c, e) || nodeisslice(c, e); - gptotal = gptotal + tupebytes(wide); + gptotal = gptotal + tuplitgpwords(c, e); }; e = e.next; }; if (gptotal > TUPLE_GPCAP || ssecount > ssecap) { + // #22a (rule 7): a tagged element's box can't + // ride the scalar/wide store arms below — the + // MEMORY-class tagged element store is #22b + // (task #28); pre-guard it silently stored + // word0 of the box. + let gq: *node = nil; + if (c.fnret != nil) { gq = c.fnret.list; }; + for (gq != nil) { + let gqt: *tinfo = gq.lhs.type_: *tinfo; + for (gqt != nil && gqt.kind == tykind.TY_NAMED) { gqt = gqt.under; }; + if (gqt != nil && gqt.kind == tykind.TY_TAGGED) { + let m22b: str = "#22b: tagged element in an over-cap (sret) tuple return unwired (rule 7)\n"; + os.write(2, m22b.ptr, m22b.len: u64); + os.exit(1); + }; + gq = gq.next; + }; // #10 Fold A: over-cap tuple returns via sret. The // prologue wired @sretarg (sretretsize agrees on the // caps — TUPLE_GPCAP/TUPLE_SSECAP, the shared SSoT), @@ -31485,7 +31662,12 @@ fn cgreturn(c: *cgen, n: *node) void = { emitline("\n"); }; }; - foff += esz; + // C-t0: the sret buffer is slot-laid like + // every tuple home (checker size, t.N + // reader, mlet receive agree); esz keeps + // the store WIDTH natural. Mirrors cstage + // cgen.c N_RETURN over-cap arm. + if (wide) { foff += esz; } else { foff += 8; }; we = we.next; if (pt != nil) { pt = pt.next; }; }; @@ -31506,8 +31688,8 @@ fn cgreturn(c: *cgen, n: *node) void = { e = rhs.list; for (e != nil) { let isflt: bool = isfloattype(c, e); - cgexpr(c, e); if (isflt) { + cgexpr(c, e); let mov: str = "MOVSD"; if (isf32type(c, e)) { mov = "MOVSS"; }; emitline("\t"); @@ -31517,11 +31699,9 @@ fn cgreturn(c: *cgen, n: *node) void = { emitline("(BP)\n"); sseidx = sseidx + 1; } else { - emitline("\tPUSHQ\tAX\n"); // scalar / .ptr - if (nodeisstr(c, e) || nodeisslice(c, e)) { - emitline("\tPUSHQ\tBX\n"); // .len - emitline("\tPUSHQ\tCX\n"); // .cap - }; + // scalar=AX; slice/str=AX,BX,CX; tagged + // box from its slot (tuplitpushelem) + tuplitpushelem(c, e); }; e = e.next; }; @@ -32962,20 +33142,15 @@ fn cglet(c: *cgen, n: *node) void = { for (q != nil) { let qt: *node = q.lhs; let isflt: bool = isfloattype(c, qt); - let wide: bool = isstrtype(c, qt) - || isslicetype(c, qt); + let eslot: i32 = tupeslotn(qt); tupstore(c, gpcur, ssecur, - off + eoff, wide, qt); + off + eoff, eslot, qt); if (isflt) { ssecur = ssecur + 1; } else { - gpcur = gpcur + tupebytes(wide); - }; - if (wide) { - eoff = eoff + (tyslicesize(): i32); - } else { - eoff = eoff + 8; + gpcur = gpcur + eslot / 8; }; + eoff = eoff + eslot; q = q.next; }; c.lastwasreturn = 0; @@ -33816,7 +33991,10 @@ fn cgmassign(c: *cgen, n: *node) void = { }; }; }; - foff += esz; + // C-t0: slot stride — must mirror the N_RETURN + // over-cap SEND's buffer layout (cstage N_MASSIGN + // twin strides tuple_eslot). + foff += tupeslotn(tn); lb = lb.next; if (pt2 != nil) { pt2 = pt2.next; }; }; @@ -33836,8 +34014,7 @@ fn cgmassign(c: *cgen, n: *node) void = { if (isfloattype(c, tn)) { ssetotal = ssetotal + 1; } else { - let wide: bool = isstrtype(c, tn) || isslicetype(c, tn); - gptotal = gptotal + tupebytes(wide); + gptotal = gptotal + tupeslotn(tn) / 8; }; l = l.next; if (pt != nil) { pt = pt.next; }; @@ -33864,18 +34041,18 @@ fn cgmassign(c: *cgen, n: *node) void = { let tn: *node = nil; if (pt != nil) { tn = pt.lhs; }; let isflt: bool = isfloattype(c, tn); - let wide: bool = isstrtype(c, tn) || isslicetype(c, tn); + let eslot: i32 = tupeslotn(tn); let off: i32 = 0; if (l.kind == nkind.N_IDENT) { off = localfind(c, l.str); }; // harec `_` (off==0): skip the store but CONSUME the cursor // slot so the next element stays aligned. if (off != 0) { - tupstore(c, gpcur, ssecur, off, wide, tn); + tupstore(c, gpcur, ssecur, off, eslot, tn); }; if (isflt) { ssecur = ssecur + 1; } else { - gpcur = gpcur + tupebytes(wide); + gpcur = gpcur + eslot / 8; }; l = l.next; if (pt != nil) { pt = pt.next; }; @@ -33938,13 +34115,13 @@ fn cgmlet(c: *cgen, n: *node) void = { for (lb != nil) { let tn: *node = lb.lhs; let isflt: bool = isfloattype(c, tn); - let wide: bool = isstrtype(c, tn) || isslicetype(c, tn); + let eslot: i32 = tupeslotn(tn); let esz: i32 = 8; let eti: *tinfo = nil; if (tn != nil) { eti = tn.type_: *tinfo; }; if (eti != nil) { esz = eti.size: i32; }; let bsz: i32 = 8; - if (wide) { bsz = tyslicesize(): i32; }; + if (eslot > 8) { bsz = eslot; }; let off: i32 = localadd(c, lb.str, bsz, tn); if (isflt) { let mov: str = "MOVSD"; @@ -33954,9 +34131,9 @@ fn cgmlet(c: *cgen, n: *node) void = { emitline("(BP), X0\n"); emitline("\t"); emitline(mov); emitline("\tX0, "); emitoff(off: i64); emitline("(BP)\n"); - } else { if (wide) { + } else { if (eslot > 8) { let k: i32 = 0; - for (k < esz) { + for (k < eslot) { emitline("\tMOVQ\t"); emitoff((srcoff + foff + k): i64); emitline("(BP), AX\n"); @@ -33974,7 +34151,7 @@ fn cgmlet(c: *cgen, n: *node) void = { emitline("\t"); emitline(sop); emitline("\tAX, "); emitoff(off: i64); emitline("(BP)\n"); }; }; - if (wide) { foff += 24; } else { foff += 8; }; + foff += eslot; lb = lb.next; }; c.lastwasreturn = 0; @@ -33995,14 +34172,14 @@ fn cgmlet(c: *cgen, n: *node) void = { let tn: *node = nil; if (pt2 != nil) { tn = pt2.lhs; }; let isflt: bool = isfloattype(c, tn); - let wide: bool = isstrtype(c, tn) || isslicetype(c, tn); + let eslot: i32 = tupeslotn(tn); let esz: i32 = 8; if (pt2 != nil) { let eti: *tinfo = pt2.lhs.type_: *tinfo; if (eti != nil) { esz = eti.size: i32; }; }; let bsz: i32 = 8; - if (wide) { bsz = tyslicesize(): i32; }; + if (eslot > 8) { bsz = eslot; }; let off: i32 = localadd(c, lb.str, bsz, tn); if (isflt) { let mov: str = "MOVSD"; @@ -34013,9 +34190,9 @@ fn cgmlet(c: *cgen, n: *node) void = { emitline("\t"); emitline(mov); emitline("\tX0, "); emitoff(off: i64); emitline("(BP)\n"); } else { - if (wide) { + if (eslot > 8) { let k: i32 = 0; - for (k < esz) { + for (k < eslot) { emitline("\tMOVQ\t"); emitoff((scr + foff + k): i64); emitline("(BP), AX\n"); @@ -34035,7 +34212,7 @@ fn cgmlet(c: *cgen, n: *node) void = { emitoff(off: i64); emitline("(BP)\n"); }; }; - foff += esz; + foff += eslot; lb = lb.next; if (pt2 != nil) { pt2 = pt2.next; }; }; @@ -34057,8 +34234,7 @@ fn cgmlet(c: *cgen, n: *node) void = { if (isfloattype(c, tn)) { ssetotal = ssetotal + 1; } else { - let wide: bool = isstrtype(c, tn) || isslicetype(c, tn); - gptotal = gptotal + tupebytes(wide); + gptotal = gptotal + tupeslotn(tn) / 8; }; l = l.next; if (pt != nil) { pt = pt.next; }; @@ -34087,15 +34263,15 @@ fn cgmlet(c: *cgen, n: *node) void = { if (pt != nil) { tn = pt.lhs; }; }; let isflt: bool = isfloattype(c, tn); - let wide: bool = isstrtype(c, tn) || isslicetype(c, tn); + let eslot: i32 = tupeslotn(tn); let sz: i32 = 8; - if (wide) { sz = tyslicesize(): i32; }; + if (eslot > 8) { sz = eslot; }; let off: i32 = localadd(c, l.str, sz, tn); - tupstore(c, gpcur, ssecur, off, wide, tn); + tupstore(c, gpcur, ssecur, off, eslot, tn); if (isflt) { ssecur = ssecur + 1; } else { - gpcur = gpcur + tupebytes(wide); + gpcur = gpcur + eslot / 8; }; l = l.next; if (pt != nil) { pt = pt.next; }; @@ -34745,8 +34921,7 @@ fn cgfnparams(c: *cgen, params: *node) void = { emitline("(BP)\n"); fidx += 1; } else { - let wide: bool = isstrtype(c, et) || isslicetype(c, et); - let eb: i32 = tupebytes(wide); + let eb: i32 = tupeslotn(et) / 8; if (idx + eb > 6) { let msg: str = "tuple param element overflows integer arg regs (DI/SI/DX/CX/R8/R9); stitch out of scope, see #163\n"; os.write(2, msg.ptr, msg.len: u64); @@ -34763,7 +34938,7 @@ fn cgfnparams(c: *cgen, params: *node) void = { k += 1; }; }; - eoff += slotsize(c, et); + eoff += tupeslotn(et); te = te.next; }; p = p.next; @@ -37456,15 +37631,12 @@ fn emitslicedata(c: *cgen, name: str, module: str, slt: *tinfo, fn emittupledata(c: *cgen, name: str, module: str, tt: *node, rhs: *node) bool = { if (tt == nil) { return false; }; if (rhs == nil) { + // #22: slot-sum via the accessor so the zero-fill matches + // the checker size (cstage zero-emits u->size). let zsz: i32 = 0; let p0: *node = tt.list; for (p0 != nil) { - let et0: *node = p0.lhs; - if (isstrtype(c, et0) || isslicetype(c, et0)) { - zsz += (tyslicesize(): i32); - } else { - zsz += 8; - }; + zsz += tupeslotn(p0.lhs); p0 = p0.next; }; emitline("DATAW "); @@ -37486,6 +37658,21 @@ fn emittupledata(c: *cgen, name: str, module: str, tt: *node, rhs: *node) bool = let ev: *node = e; for (ev != nil && ev.kind == nkind.N_CAST) { ev = ev.lhs; }; if (ev == nil) { return false; }; + // #22a (rule 7): a tagged element slot has no static-init + // shape (tag word + payload widening) — reject so the caller + // loud-stops; pre-guard an int init would have emitted one + // 8B word into the 16B+ box (silent layout skew). Mirrors + // cstage emit_tuple_data. + { + let eti: *tinfo = nil; + if (et != nil) { eti = et.type_: *tinfo; }; + for (eti != nil && eti.kind == tykind.TY_NAMED) { + eti = eti.under; + }; + if (eti != nil && eti.kind == tykind.TY_TAGGED) { + return false; + }; + }; let wide: bool = isstrtype(c, et) || isslicetype(c, et); if (wide) { if (ev.kind != nkind.N_STRLIT) { return false; }; @@ -37557,7 +37744,10 @@ fn emittupledata(c: *cgen, name: str, module: str, tt: *node, rhs: *node) bool = }; foff += (tyslicesize(): i32); } else { - foff += 8; + // #22: slot stride via the accessor (tagged is + // rejected upstream; non-wide is 8 today — keeps the + // stride on the accessor scale). + foff += tupeslotn(et); }; e = e.next; if (tp != nil) { tp = tp.next; }; diff --git a/test/wcc/941_tuple_slot_layout_run.c b/test/wcc/941_tuple_slot_layout_run.c index 26c80a83..ac5aec0a 100644 --- a/test/wcc/941_tuple_slot_layout_run.c +++ b/test/wcc/941_tuple_slot_layout_run.c @@ -209,6 +209,285 @@ static const struct row rows[] = { " return 0;\n" "};\n", 0, K_RUN, NULL }, + /* ---- #22a: TAGGED elements in tuples. slot = roundup8(size(elem)) + * — 8B is a FLOOR, not a ceiling (user-ratified 2026-06-04). + * Pre-#22a the checker truncated a tagged element to one 8B slot + * (cstage SIZE 16 vs wwstage astsize 24, cs≠ww) and every cgen + * transport walk strode wide=(STR||SLICE)-else-8 — cstage read the + * NEIGHBOR slot, wwstage read ZEROS (both-wrong-differently, + * byte-cmp-blind). tuple_eslot/tupeslot is now the one stride + * accessor. In-cap shapes work end-to-end; over-cap (>4 GP + * eightbytes — the fold-5b 3-elem shape) stays LOUD until #22b. + * Tagged inits use the CAST form (`5: size`): the bare untyped-int + * widen-store mis-tag is a separate pre-existing bug (task #33). */ + { "t22_sizefold_tagged", + "package main;\n" + "export fn main() i32 = {\n" + " if (size(((void | size), size)) != 24) { return 1; };\n" + " if (size((size, (void | size))) != 24) { return 2; };\n" + " if (size(((void | size), (void | size), size)) != 40) { return 3; };\n" + " if (align(((void | size), size)) != 8) { return 4; };\n" + " if (size((u64, void)) != 8) { return 5; };\n" + " return 0;\n" + "};\n", 0, K_RUN, NULL }, + { "t22_roundtrip", + "package main;\n" + "fn mk() ((void | size), size) = {\n" + " let mn: (void | size) = 5: size;\n" + " return (mn, 4);\n" + "};\n" + "fn mk2() (size, (void | size)) = {\n" + " let mx: (void | size) = 7: size;\n" + " return (9, mx);\n" + "};\n" + "fn mkv() ((void | size), size) = {\n" + " let mn: (void | size) = void;\n" + " return (mn, 3);\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 != 4) { return 3; };\n" + " let u = mk2();\n" + " if (u.0 != 9) { return 4; };\n" + " if (!(u.1 is size)) { return 5; };\n" + " if (u.1 as size != 7) { return 6; };\n" + " let v = mkv();\n" + " if (!(v.0 is void)) { return 7; };\n" + " if (v.1 != 3) { return 8; };\n" + " let (a, b) = mk();\n" + " if (!(a is size)) { return 9; };\n" + " if (a as size != 5) { return 10; };\n" + " if (b != 4) { return 11; };\n" + " let mn: (void | size) = 6: size;\n" + " let w: ((void | size), size) = (mn, 2);\n" + " if (!(w.0 is size)) { return 12; };\n" + " if (w.0 as size != 6) { return 13; };\n" + " if (w.1 != 2) { return 14; };\n" + " return 0;\n" + "};\n", 0, K_RUN, NULL }, + /* mixed SSE/GP with a tagged box: the f64 rides X0 on the SSE + * counter while the tagged element takes 2 consecutive GP + * eightbytes (GP total 3, in-cap) — pins the independent-counter + * interplay the scalar-only rows can't. */ + { "t22_float_mix", + "package main;\n" + "fn mk() (f64, (void | size), i64) = {\n" + " let mn: (void | size) = 6: size;\n" + " return (2.5, mn, 9);\n" + "};\n" + "export fn main() i32 = {\n" + " let t = mk();\n" + " if (t.0 != 2.5) { return 1; };\n" + " if (!(t.1 is size)) { return 2; };\n" + " if (t.1 as size != 6) { return 3; };\n" + " if (t.2 != 9) { return 4; };\n" + " let (x, y, z) = mk();\n" + " if (x != 2.5) { return 5; };\n" + " if (!(y is size)) { return 6; };\n" + " if (y as size != 6) { return 7; };\n" + " if (z != 9) { return 8; };\n" + " return 0;\n" + "};\n", 0, K_RUN, NULL }, + /* TWO tagged elements in-cap (2+2 GP eightbytes == TUPLE_GPCAP): + * narrow u32 payload, negative i64 payload, void variant — + * regression-pins ken's k1 probe (probes evaporate, rows don't). */ + { "t22_two_tagged", + "package main;\n" + "fn mk() ((void | u32), (void | i64)) = {\n" + " let a: (void | u32) = 9: u32;\n" + " let b: (void | i64) = -5: i64;\n" + " return (a, b);\n" + "};\n" + "fn mkv() ((void | u32), (void | i64)) = {\n" + " let a: (void | u32) = void;\n" + " let b: (void | i64) = -41: i64;\n" + " return (a, b);\n" + "};\n" + "export fn main() i32 = {\n" + " let t = mk();\n" + " if (!(t.0 is u32)) { return 1; };\n" + " if (t.0 as u32 != 9) { return 2; };\n" + " if (!(t.1 is i64)) { return 3; };\n" + " if (t.1 as i64 != -5) { return 4; };\n" + " let v = mkv();\n" + " if (!(v.0 is void)) { return 5; };\n" + " if (v.1 as i64 != -41) { return 6; };\n" + " let (a, b) = mk();\n" + " if (a as u32 != 9) { return 7; };\n" + " if (b as i64 != -5) { return 8; };\n" + " return 0;\n" + "};\n", 0, K_RUN, NULL }, + /* tagged FIRST, float second — the SSE/GP counter interplay in the + * other element order (t22_float_mix covers float-first; ken k2 + * proved both orders, this pins the reverse). */ + { "t22_float_mix_rev", + "package main;\n" + "fn mk() ((void | size), f64) = {\n" + " let mn: (void | size) = 6: size;\n" + " return (mn, 0.25);\n" + "};\n" + "export fn main() i32 = {\n" + " let t = mk();\n" + " if (!(t.0 is size)) { return 1; };\n" + " if (t.0 as size != 6) { return 2; };\n" + " if (t.1 != 0.25) { return 3; };\n" + " let (y, z) = mk();\n" + " if (y as size != 6) { return 4; };\n" + " if (z != 0.25) { return 5; };\n" + " return 0;\n" + "};\n", 0, K_RUN, NULL }, + /* tagged tuple element as a CALL ARG — the call-boundary SEND of + * the ruled condition-(c) matrix. cstage rides its generic + * node_istaggedarg cursor push; wwstage's kind-gated aistagged + * missed N_DOT and mis-routed t.0 into the widening branch + * (clamped tag 0 — callee read `void`, silent). reviewer-22 fix: + * cgenutil.ww N_DOT arm (aistagged + pushargsrev). */ + { "t22_elem_arg", + "package main;\n" + "fn takes(v: (void | size)) size = {\n" + " if (v is size) { return v as size; };\n" + " return 0;\n" + "};\n" + "export fn main() i32 = {\n" + " let mn: (void | size) = 5: size;\n" + " let t: ((void | size), size) = (mn, 4);\n" + " if (takes(t.0) != 5) { return 1; };\n" + " if (takes(t.0) + t.1 != 9) { return 2; };\n" + " return 0;\n" + "};\n", 0, K_RUN, NULL }, + + /* rule-7 louds: every tagged-tuple route that is not + * correct-via-accessor must die LOUD (the #22a exit invariant). */ + { "t22_reject_arg", + "package main;\n" + "fn send(t: ((void | size), size)) size = { return t.1; };\n" + "export fn main() i32 = {\n" + " let mn: (void | size) = 5: size;\n" + " let t: ((void | size), size) = (mn, 4);\n" + " return send(t): i32;\n" + "};\n", 0, + K_BUILDERR, "tuple arg element kind unsupported" }, + { "t22_reject_overcap_return", + "package main;\n" + "fn pr() ((void | size), (void | size), size) = {\n" + " let a: (void | size) = 1: size;\n" + " let b: (void | size) = 2: size;\n" + " return (a, b, 7);\n" + "};\n" + "export fn main() i32 = {\n" + " let t = pr();\n" + " return t.2: i32;\n" + "};\n", 0, + K_BUILDERR, "#22b: tagged element in an over-cap (sret) tuple " + "return unwired" }, + /* ken R1: an OVER-CAP tuple literal init (a tagged box >16B pushes + * the shape past TUPLE_GPCAP) fell past every cstage N_LET store + * arm to NOTHING — silent uninitialized-frame reads — while + * wwstage loud-rejected. Pre-existing for (str,str) literals; the + * #22a tagged slots routed tagged shapes into it. Both pins. */ + { "t22_reject_overcap_lit_tagged", + "package main;\n" + "export fn main() i32 = {\n" + " let e: (void | u32 | str) = \"abc\";\n" + " let t: (u64, (void | u32 | str)) = (7u64, e);\n" + " if (t.0 != 7u64) { return 1; };\n" + " return 0;\n" + "};\n", 0, + K_BUILDERR, "tuple literal exceeds register-return ABI capacity" }, + { "t22_reject_overcap_lit_strs", + "package main;\n" + "export fn main() i32 = {\n" + " let t: (str, str) = (\"ab\", \"cde\");\n" + " if (t.0.len != 2) { return 1; };\n" + " return 0;\n" + "};\n", 0, + K_BUILDERR, "tuple literal exceeds register-return ABI capacity" }, + { "t22_reject_lit_call_elem", + "package main;\n" + "fn g() (void | size) = { return 5: size; };\n" + "fn mk() ((void | size), size) = { return (g(), 4); };\n" + "export fn main() i32 = {\n" + " let t = mk();\n" + " return t.1: i32;\n" + "};\n", 0, + K_BUILDERR, "tagged tuple element from a non-local source shape " + "unwired" }, + { "t22_reject_global_init", + "package main;\n" + "let g: ((void | size), i64) = (5, 4);\n" + "export fn main() i32 = { return g.1: i32; };\n", 0, + K_BUILDERR, "unsupported element init (int/str literals only; " + "rule 7)" }, + /* pre-existing loud (no-regress pin): tuple element WRITE. */ + { "t22_reject_elem_write", + "package main;\n" + "export fn main() i32 = {\n" + " let mn: (void | size) = 5: size;\n" + " let t: ((void | size), size) = (mn, 4);\n" + " t.1 = 9;\n" + " return t.1: i32;\n" + "};\n", 0, + K_BUILDERR, "unsupported assign target shape" }, + + /* ---- over-cap (sret) NARROW-scalar stride: the sret buffer is + * slot-laid (C-t0) on EVERY side — send, let-receive, MLET, and + * MASSIGN copy-out. Pre-review the wwstage SEND and MASSIGN + * receive strode packed esz (4 for u32): self-consistent at base + * (ran right, byte-diff), but #22a's MLET-receive slot flip alone + * made ww read c at 32 while its send wrote 28 — a ww runtime + * REGRESSION the suite had no row for (reviewer-22). (str,u32,str) + * = 7 GP words, over-cap. ---- */ + { "sret_narrow_mix_let", + "package main;\n" + "fn f() (str, u32, str) = {\n" + " let a: str = \"abc\";\n" + " let b: u32 = 7;\n" + " let c: str = \"wxyz\";\n" + " return (a, b, c);\n" + "};\n" + "export fn main() i32 = {\n" + " let t = f();\n" + " if (len(t.0) != 3) { return 1; };\n" + " if (t.1 != 7) { return 2; };\n" + " if (len(t.2) != 4) { return 3; };\n" + " return 0;\n" + "};\n", 0, K_RUN, NULL }, + { "sret_narrow_mix_mlet", + "package main;\n" + "fn f() (str, u32, str) = {\n" + " let a: str = \"abc\";\n" + " let b: u32 = 7;\n" + " let c: str = \"wxyz\";\n" + " return (a, b, c);\n" + "};\n" + "export fn main() i32 = {\n" + " let (a, b, c) = f();\n" + " if (len(a) != 3) { return 1; };\n" + " if (b != 7) { return 2; };\n" + " if (len(c) != 4) { return 3; };\n" + " return 0;\n" + "};\n", 0, K_RUN, NULL }, + { "sret_narrow_mix_massign", + "package main;\n" + "fn f() (str, u32, str) = {\n" + " let a: str = \"abc\";\n" + " let b: u32 = 7;\n" + " let c: str = \"wxyz\";\n" + " return (a, b, c);\n" + "};\n" + "export fn main() i32 = {\n" + " let a: str = \"\";\n" + " let b: u32 = 0;\n" + " let c: str = \"\";\n" + " a, b, c = f();\n" + " if (len(a) != 3) { return 1; };\n" + " if (b != 7) { return 2; };\n" + " if (len(c) != 4) { return 3; };\n" + " return 0;\n" + "};\n", 0, K_RUN, NULL }, + /* ---- C-t1 (#33): the let RECEIVE re-keyed onto the declared * type's register classify. Pre-C-t1 wwstage keyed on producer * SHAPE (mixed-str syntactic / rettupleof N_CALL) so a diff --git a/test/wcc/989_lib_byteid.c b/test/wcc/989_lib_byteid.c index eb66dd0a..f8506502 100644 --- a/test/wcc/989_lib_byteid.c +++ b/test/wcc/989_lib_byteid.c @@ -67,6 +67,11 @@ static const struct ent ents[] = { { .fixture = "lib/bytes/bytestest.ww", .mode = M_ID }, { .fixture = "lib/dirs/dirstest.ww", .mode = M_ID }, { .fixture = "lib/encoding/base32/base32_test.ww", .mode = M_ID }, + /* graduated from #59.4 DIVERGE by the #22a N_DOT tagged-arg + * cursor arm (ww widened t.N args with a clamped tag 0 — the + * base .s diff is exactly that PUSHQ pair, 2 sites); + * runtime-correct per 979_hex_run */ + { .fixture = "lib/encoding/hex/hextest.ww", .mode = M_ID }, { .fixture = "lib/encoding/utf8/utf8test.ww", .mode = M_ID }, { .fixture = "lib/getopt/getopttest.ww", .mode = M_ID }, { .fixture = "lib/hash/adler32/adler32_test.ww", .mode = M_ID }, @@ -118,8 +123,7 @@ static const struct ent ents[] = { .mode = M_DIVERGE, .cite = "#59.2" }, { .fixture = "lib/encoding/base64/base64_test.ww", .mode = M_DIVERGE, .cite = "#59.3" }, - { .fixture = "lib/encoding/hex/hextest.ww", - .mode = M_DIVERGE, .cite = "#59.4" }, + /* #59.4 hextest graduated to M_ID above (#22a reviewer fixes) */ { .fixture = "lib/errors/errnotest.ww", .mode = M_DIVERGE, .cite = "#59.5" }, { .fixture = "lib/fmt/fmttest.ww",