w6c+w6c_ww: cast-wrapped tuple literal widens its whole payload into a tagged slot (#66)

The #242 tuple arm of the widen choke-point (cg_widen_tagged_store /
cgwidentaggedstorebp) gated on a BARE N_TUPLE source. The cast-to-
CONCRETE-VARIANT wrapper ((a, b): range_alias) — the only spelling
real code uses (ref/hare/regex/regex.ha:213) — is not a widen-cast
(its destination is the variant, not the union), so the peel left it
intact and it fell to the SCALAR arm: cursor word 0 stored, payload
slot 1+ silently zero-filled. Both stages, byte-id, gate-blind.

Fix at the choke-point: peel N_CAST(lhs=N_TUPLE) where the NAMED-
peeled cast type is TY_TUPLE and iterate the inner element list; the
variant tag keeps resolving from the CAST's type (exact named match),
so the #241 untyped-element loud-stop stays scoped to the bare form
on both stages.

Closure by construction needed two more arms (reviewer proof-grep):
cg_widen_tagged_push's direct-push fast path classified a tuple-typed
ARG source as scalar — pushed word 0 only AND coerced an unresolved
tag to 0 — so f(((a,b): rng)) bypassed the fixed arm entirely (and
the bare typed (a,b) arg dropped slot 1 the same way). Tuple-typed
sources now route through the scratch store. The remaining non-
literal tuple sources (ident / call result / match binding) have no
word-copy arm in the store and fell to its scalar arm — loud-stop
(rule 7) until #72 wires them. Every tagged-payload materialisation
now funnels through cg_widen_tagged_store, which handles or rejects
every tuple shape: let/assign/return/append (cgen.c:7511) directly,
arg push via the scratch route.

test/936: cast-tuple matrix (let / append local+index-place+deref-
place+ptr-field-place / ident+float+str elements / 3-member layout-neutrality /
direct-arg) + bare-form no-regress (return + arg) + bare-literal and
tuple-ident reject pins, per-row cs==ww byte-id; verified failing
24/40 at parent 8578ad0.

Unblocks regex fold-4 (charset_range_item construction).
This commit is contained in:
2026-06-04 20:06:21 +09:00
parent 8578ad0533
commit bb8a44a564
6 changed files with 762 additions and 29 deletions

View File

@@ -2456,8 +2456,32 @@ cg_widen_tagged_store(Cg *c, Local **locals_p, Type *dst, Node *src,
* (AX=tag, DX=word0, CX=word1, R8=word2) and the cgmlet receive
* cursor. NOT the packed-by-size t.N field layout (#238). Mirror of
* the struct-literal field-flow below, but 8B-slotted, not field-
* offset. */
if (su && su->kind == TY_TUPLE && src->kind == N_TUPLE) {
* offset.
*
* #66: the cast-wrapped tuple literal `((a, b): range_alias)` is
* the spelling real code uses (regex.ha:213) — the cast targets the
* CONCRETE variant, so the widen-cast peel above leaves it intact
* and pre-#66 it fell to the scalar arm, silently dropping payload
* slot 1+. Peel to the inner tuple here; st stays the CAST's type,
* which resolves the variant tag by exact named match, so the #241
* untyped-element un-matchability does not arise for this form. */
Node *tupsrc = NULL;
if (su && su->kind == TY_TUPLE) {
if (src->kind == N_TUPLE)
tupsrc = src;
else if (src->kind == N_CAST && src->lhs
&& src->lhs->kind == N_TUPLE)
tupsrc = src->lhs;
/* #72: any OTHER tuple-typed source (ident, call result,
* match binding) would fall to the scalar arm below and
* silently drop payload slot 1+ — loud-stop (rule 7) until
* the word-copy / cursor-receive arms are wired. */
if (tupsrc == NULL)
fatal("cg_widen_tagged_store: tuple-typed source "
"shape unwired (only the bare/cast tuple literal "
"carries a full payload; see #72)");
}
if (tupsrc != NULL) {
int tag = cg_tag_for_variant(du, st);
/* #242: a tuple built from UNTYPED/literal elements (`(true,7)`)
* leaves the src tuple type un-matchable by type_eq, so the
@@ -2478,7 +2502,7 @@ 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 = src->list; e; e = e->next)
for (Node *e = tupsrc->list; e; e = e->next)
total += (node_isstr(e) || node_isslice(e)) ? 24 : 8;
if (8 + total > sz)
fatal("cg_widen_tagged_store: tuple-in-union payload needs "
@@ -2489,7 +2513,7 @@ cg_widen_tagged_store(Cg *c, Local **locals_p, Type *dst, Node *src,
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, write_off + k));
int foff = 0;
for (Node *e = src->list; e; e = e->next) {
for (Node *e = tupsrc->list; e; e = e->next) {
int e_isf32 = 0;
int isflt = fld_isfloat(e->type, &e_isf32);
int wide = node_isstr(e) || node_isslice(e);
@@ -2702,11 +2726,17 @@ cg_widen_tagged_push(Cg *c, Local **locals_p, Type *dst, Node *src, int sz)
Type *su = (st && st->kind == TY_NAMED) ? st->under : st;
int src_is_struct = su && su->kind == TY_STRUCT;
int src_is_tagged = su && su->kind == TY_TAGGED;
/* #66: a tuple-typed source has no direct-push shape — the scalar
* fast arm below would push word 0 only (payload slot 1+ dropped)
* and coerce an unresolved tag to 0. Route through the scratch
* store, whose #242/#66 tuple arm handles the literal/cast forms
* and loud-stops the rest (#72). */
int src_is_tuple = su && su->kind == TY_TUPLE;
/* #38b: a MEMORY-class (>48B) dst slot always routes through the
* scratch path — the str/slice fast arms push exactly 4 words,
* short of the slot's msz/8 the mem pre-pass accounts for. */
int dst_is_mem = tagged_memarg_size(dst) > 0;
if (!src_is_struct && !src_is_tagged && !dst_is_mem) {
if (!src_is_struct && !src_is_tagged && !src_is_tuple && !dst_is_mem) {
/* Direct-push fast path: str / slice / scalar / pointer. */
cgexpr(c, src, *locals_p);
int tag = cg_tag_for_variant(du, st);