wcc+w6c+w6c_ww: tagged tuple elements take their full slot — tuple_eslot accessor (#22a)

slot = roundup8(size(elem)) — 8B is a FLOOR, not a ceiling (user-
ratified 2026-06-04; the #237 fieldslotsize precedent; (str,str)=48B
predates this — tagged was the one truncated >8B kind). Pre-fix the
checkers truncated a tagged element to one 8B slot and every cgen
transport walk strode wide=(STR||SLICE)-else-8: cstage read the
NEIGHBOR slot, wwstage read ZEROS — both-wrong-differently, so the
byte-id gates were blind (prober-9 PG1, /tmp/p9).

One stride accessor per stage — tuple_eslot (cgen.c) / tupeslot
(cgenstmt.ww) — now feeds every tuple walk; the per-site predicates
are deleted as absorbed. Sizer: check.c N_TTUPLE + check.ww
tupleelemslot gain the TY_TAGGED arm (astsize already rides the type
table since commit 0). Routes flipped to the accessor, both stages
symmetric: cursor producers (lit/slot-to-cursor; tagged ident
elements load their box from the slot — cgexpr's tagged ident load
is word0-only), let-receive (tuple_store/tupstore generalized to
eslot/8 words), N_RETURN send, by-value param receive, arg
restage/drain (tagged stays loud per C-t2), destructure
(MLET/MASSIGN, ident + sret + in-cap), t.N read + len(t.N) +
global-g.N offset walks (t.N gains the tagged box load: AX=tag,
DX/CX/R8=payload — the is/as spill cursor), sret classifier, DATA
emit. wwstage cgtagvariantidx gains the #67 stamped-carrier arm
(flatvariantidxt on .type_) — its AST-only key silently clamped
't.0 is size' to tag 0; fused here because the tuple-element read
this commit wires is its only exercisable consumer.

Exit invariant: zero silent tagged-tuple paths — in-cap shapes
(<=4 GP eightbytes) are correct end-to-end; everything else is LOUD:
over-cap sret return (#22b, task #28), call-arg (C-t2 #32),
non-local literal element sources (#22b/#23), tuple-in-union payload
(#242/#22b), global static-init, element write (pre-existing).
Closure proof-grep at HEAD: 'tuple_ebytes|tupebytes' -> 0 hits;
'wide.*=.*(TY_SLICE|TY_STR)' tuple-walk survivors are all behind
tagged loud-guards (cgen.c:2535/2568/12013 widen-store + over-cap
send; cgenutil.ww:3527 twin).

Latent cross-checks closed by the accessor: wwstage MLET-sret strode
esz (4 for i32) vs cstage 8; wwstage param-receive strode slotsize
(composite slotsize) vs cstage 8; both now the accessor's floor-8.
Tagged inits in pins use the CAST form (5: size) — the bare
untyped-int widen-store mis-tag is pre-existing at master and filed
(task #33).

941 gains 13 rows: t22_* size/align folds (+ void-elem 0-slot pin),
the full runtime round-trip (read, is/as, both element orders, void
variant, destructure, literal-let), two-tagged-elem in-cap and
float+tagged in BOTH orders (ken k1/k2 regression-pinned as rows),
t.N-as-call-arg, 5 loud pins (arg, over-cap return, call-source
element, global init, element write), and the sret_narrow_mix_*
triple below. Runtime rows exit-checked under both drivers +
byte-id. 129/129; unit tests green; sizelint clean.

ken R1 (re-validation fix): an OVER-CAP tuple init whose rhs is not a
CALL fell past every cstage N_LET store arm to NOTHING — silent
uninitialized-frame reads — while wwstage loud-rejected the same
shape. Pre-existing for (str,str) literals; the #22a tagged slots
routed >16B-box tagged shapes into it (k5b/k5c/k5d, base-correct
because base truncated them in-cap). cstage now routes the rhs
through cgexpr (the cursor materialisers carry the exact wwstage
loud texts) with a trailing fatal as the net; 941 pins both the
tagged and the (str,str) spellings of the hole.

reviewer-22 (review fixes, folded): the MLET-receive slot flip above
landed ALONE on the wwstage over-cap sret family — the ww RETURN-send
and MASSIGN-receive still strode packed esz (4 for u32), so
`let (a,b,c) = f()` over (str,u32,str) read c at slot offset 32 while
ww's send wrote it packed at 28: a ww runtime REGRESSION vs base
(base was packed-consistent on both sides — ran right, byte-diff).
Both walks now stride the slot (send mirrors cstage's
`wide ? esz : 8`; MASSIGN strides tupeslotn) — closing, in the same
stroke, the pre-existing base skews: ww `let t = f()` over-cap
narrow-mix read (reader slot-laid vs send packed, runtime-wrong at
base) and the cs≠ww asm on all three routings. Second find: t.N
tagged element as a CALL ARG — cstage rides its generic
node_istaggedarg cursor push, but wwstage's kind-gated aistagged
missed N_DOT and mis-routed the box into the widening branch
(taggedvariantindex -1 clamped to 0: callee read variant 0, silent,
ww-only). cgenutil.ww gains the N_DOT arm (aistagged + pushargsrev),
the #67 stamped-carrier twin of the N_INDEX (#12) arm. The N_DOT arm
was a LIVE wwstage stdlib miscompile: hextest's import graph carried
two t.N tagged-arg sites (base .s diff = exactly the clamped-tag
PUSHQ pair) — the 989 lib-byteid ratchet caught the convergence and
#59.4 graduates to M_ID (runtime-correct per 979_hex_run). Probes:
/tmp/r22. Residual filed: tagged arg from deref/cast/unwrap sources
is a word0-only read on BOTH stages (pre-existing, base-confirmed) —
generalizing wwstage's kind gate to cstage's type-keyed check rides
that task.
This commit is contained in:
2026-06-05 00:32:35 +09:00
parent ed62e8199f
commit 66991585d6
12 changed files with 1449 additions and 523 deletions

View File

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

View File

@@ -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 */
}