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

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