w6c+w6c_ww: over-cap (sret) tuple returns carry tagged elements — MEMORY-class box copy (#22b)

The over-cap SEND gains a per-element tagged arm: a declared-TAGGED
element copies its whole box mem-to-mem through the sret pointer
(@sretarg reloaded into DX, eslot/8 AX-bounced word pairs) from its
ident-local slot — cgexpr can't source it (tagged ident load is
word0-only; the AX/DX/CX/R8 box cursor would collide with the DX
dest-base reload). Non-ident/widening sources stay loud (call-source
is task #41 per rob's loud+filed ruling, widening #23/#40). The
receive legs were already eslot-generic from #22a except the wwstage
MASSIGN sret copy-out, whose wide=(str||slice) key dropped a tagged
element to the scalar 8B branch — re-keyed to the accessor's eslot>8.

The over-cap DECISION now rides cg_sret_retsize/sretretsize on the
DECLARED return type — the same SSoT the prologue @sretarg wiring and
the caller use. The old expr-shape count disagreed with the classifier
for declared-tagged elements with unwidened-payload exprs: the emit
took the register path against an sret-reading caller — built clean,
ran garbage, BOTH stages, byte-id, gate-blind (941 b22_reject_widen_
skew). A rule-7 net behind the branch catches any future inverse skew.
The same classify/emit class on the LET side: wwstage's cgexpr cursor
materialiser counts a void literal elem as 0 words, so a void-bearing
over-cap literal init slipped its loud and fell past every store arm
to NOTHING (cs louded via the ken-R1 net; ww ran silent-wrong) —
wwstage gains the same net. foff advances ride tuple_eslot/tupeslotn
(THE accessor): ken's K4 proved the old wide?esz:8 void-elem 8-advance
was a LIVE silent miscompile at base (declared-void-elem over-cap
return, send wrote +8 where every receive walks the 0-slot).

The unwiring also made >32B boxes transportable, exposing the t.N
tagged read's unbounded k<eslot/8 cursor walk: cstage OOB-indexed
tuple_rseq[4] (invalid asm), wwstage tupreg-clamped k>=3 to R8 (silent
payload-word-3 drop). Bounded loud both stages; the mem-based box read
is #37's fold (transport itself is size-generic and correct — 941
b22_big_box_mlet). One commit by the #61 route-sharing precedent:
unwiring the send is the only thing that makes the ww massign arm,
both skew nets, and the >32B read reachable — the one split order
that avoids a transient silent-wrong build (send last) lands those
arms DEAD, unexercisable by any test until the unwiring commit, so a
defect in them would bisect to the wrong commit; fused, every arm
lands with the 941 rows that pin it, preserving the #22a exit
invariant (zero silent tagged-tuple paths: correct-via-accessor or
LOUD with a task cite; proof-greps in-tree — tuple_ebytes/tupebytes 0
hits, all remaining tuple_rseq/tupreg walks cap-validated upstream).

Driver: regex fold-5b parse_repetition ((void|size),(void|size),size)
= 5 GP eightbytes > TUPLE_GPCAP — pinned end-to-end (construct, send,
let/MLET/MASSIGN receive, t.N is/as, destructure) plus head/mid/tail,
str+tagged and SSE-over-cap mixes, `_` skip, and the reject pins: 941
165/165 (15 new rows), test-unit 281/281. ken runtime-oracle PASS x2
(independent hand-math layout audit, K1-K5 probes); reviewer-22b
amendments (test-only): ken-K5 32B-exact box-at-cap row, both-tagged
2-elem row, chained-sret row, #37-family cite pinned in the big-box
experr. Residuals filed: #39 return-forward divergence, #41
call-source tripwire, #37 mem-based box read.
This commit is contained in:
2026-06-05 02:26:06 +09:00
parent f88dbb01e2
commit a72e8158c7
6 changed files with 803 additions and 112 deletions

View File

@@ -3114,8 +3114,8 @@ tuple_lit_gpwords(Node *e)
* 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
* for call results rides the #35 non-ident-source family, widening
* literals #23). Shared by cg_tuple_lit_to_cursor and the cgreturn
* N_TUPLE arm — count (tuple_lit_gpwords) and push live or die
* together. */
static void
@@ -3129,7 +3129,8 @@ tuple_lit_push_elem(Cg *c, Local *locals, Node *e)
if (eoff == 0)
fatal("#22a: tagged tuple element from a non-local "
"source shape unwired (ident locals only; "
"rule 7, #22b/#23 follow-ups)");
"rule 7; call-source is task #41, "
"widening #23, deref/cast #35)");
for (int k = 0; k < eslot / 8; k++) {
ins2(c, A_MOVQ, amem(D_BP, eoff + k * 8),
areg(D_AX));
@@ -10013,6 +10014,18 @@ cgexpr(Cg *c, Node *n, Local *locals)
* the ascending walk can't clobber the base. */
if (fu && fu->kind == TY_TAGGED) {
int eslot = tuple_eslot(tp->type);
/* #22b (rule 7): a >32B box overruns the
* 4-reg cursor — pre-bound the walk
* indexed past tuple_rseq (invalid asm)
* while wwstage clamped to R8 (silent
* payload drop). Reachable only since
* the over-cap sret send unwired; the
* mem-based box read is the #37 family. */
if (eslot > TUPLE_GPCAP * 8)
fatal("tagged tuple element read "
"exceeds the AX/DX/CX/R8 box "
"cursor (mem-based read is the "
"#37 family; rule 7)");
for (int k = 0; k < eslot / 8; k++)
ins2(c, A_MOVQ,
amem(base_reg,
@@ -12212,7 +12225,21 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
else
gptotal += tuple_lit_gpwords(e);
}
if (gptotal > TUPLE_GPCAP || ssecount > ssecap) {
/* #22b: classify and emit MUST agree (the #10 SSoT
* note at TUPLE_GPCAP). The over-cap DECISION rides
* cg_sret_retsize on the DECLARED return type — the
* same predicate the prologue (@sretarg) and the
* caller key on. The expr-shape count above only
* pairs the in-cap push/pop: a declared-tagged
* element whose expr is the unwidened payload counts
* 1 word here vs 2+ declared eightbytes, so the emit
* took the register path against an sret-classified
* caller — silent garbage, both stages, gate-blind
* (probe /tmp/i22b/p2). */
int overcap = cg_ret_type != NULL
? cg_sret_retsize(cg_ret_type) > 0
: (gptotal > TUPLE_GPCAP || ssecount > ssecap);
if (overcap) {
/* #10 Fold A: over-cap tuple returns via sret. The
* prologue wired @sretarg (cg_sret_retsize agrees on
* the caps — the shared SSoT), holding the caller-
@@ -12237,20 +12264,56 @@ 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) {
Type *du = pp
? type_chase_named(pp->type) : NULL;
if (du && du->kind == TY_TAGGED) {
/* #22b (task #28): MEMORY-class
* tagged element — the whole box
* copies through the sret pointer
* mem-to-mem from the element's
* local slot. cgexpr can't source
* it: the tagged ident load is
* word0-only (every tagged
* consumer reads memory) and the
* AX/DX/CX/R8 box cursor would
* collide with the DX dest-base
* reload. Ident-only, mirroring
* tuple_lit_push_elem; widening /
* non-ident sources stay loud
* (#23/#40 follow-ups). */
int eslot = tuple_eslot(pp->type);
Type *eu = type_chase_named(e->type);
int eoff = 0;
if (e->kind == N_IDENT && e->str
&& eu && eu->kind == TY_TAGGED
&& tuple_eslot(e->type) == eslot)
eoff = localfind(*locals,
e->str);
if (eoff == 0)
fatal("#22b: tagged element "
"in an over-cap (sret) "
"tuple return from a "
"non-ident or widening "
"source unwired (ident "
"locals only; rule 7; "
"call-source is task "
"#41, widening #23/#40)");
ins2(c, A_MOVQ,
amem(D_BP, cg_sret_arg_off),
areg(D_DX));
for (int k = 0; k < eslot; k += 8) {
ins2(c, A_MOVQ,
amem(D_BP, eoff + k),
areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX),
amem(D_DX, foff + k));
}
foff += eslot;
pp = pp->next;
continue;
}
int isflt = fld_isfloat(e->type, &f32);
int wide = node_isstr(e) || node_isslice(e);
int esz = 8;
@@ -12274,11 +12337,16 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
} else
ins2(c, fldstoreop(e->type, esz),
areg(D_AX), amem(D_DX, foff));
/* C-t0: the sret buffer is slot-laid
/* C-t0/#22: the sret buffer is slot-laid
* like every tuple home (checker size,
* t.N reader, mlet receive agree); esz
* keeps the store WIDTH natural. */
foff += wide ? esz : 8;
* t.N reader, mlet receive agree) — the
* stride is THE accessor's (a declared
* void element's 0-slot included; the
* old wide?esz:8 advanced 8 where every
* receive walks 0). esz keeps the store
* WIDTH natural. */
foff += tuple_eslot(pp ? pp->type
: e->type);
if (pp) pp = pp->next;
}
ins2(c, A_MOVQ, amem(D_BP, cg_sret_arg_off),
@@ -12288,6 +12356,15 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
ins0(c, A_RET);
break;
}
/* rule-7 net: register-classified by the declared type
* but the expr-shape count overflows the cursor — the
* pops below would index past tuple_rseq. Unreachable
* while expr counts never exceed declared counts;
* loud, not OOB, if a future shape breaks that. */
if (gptotal > TUPLE_GPCAP || ssecount > ssecap)
fatal("register-classified tuple return "
"exceeds the cursor (classify/emit skew; "
"rule 7, #22b)");
int fscr = 0;
if (ssecount > 0) {
if (cg_tupfscr != 0)