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

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

View File

@@ -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",