cgen: #57 in-cap tuple cursor fill keys on the DECLARED element type — tagged elems from concrete rvalues widen, both stages

The N_TUPLE literal's stamped type is CONSTRUCTED from its elements
(check.c N_TUPLE keeps untyped/concrete element types; assignability
is consumer-side), so the in-cap cursor fill — count
(tuple_lit_gpwords/tuplitgpwords) + push (tuple_lit_push_elem/
tuplitpushelem) — never saw the DECLARED tuple type. A declared-TAGGED
element whose expr is a concrete rvalue (`return (5: size, 9)` into
(un16, size)) counted ONE word and skipped the widen entirely: 2 words
sent against the receiver's declared 3-word walk, every later element
read garbage. Both stages, byte-identical, gate-blind (ken /tmp/ken57
p8/p9: t.1 read entry-junk). The let-literal twin
(`let t: (un16, size) = (5: size, 9)`) and the tagged-SECOND-elem
shift broke identically (probes q1/q2). The over-cap (sret) arm
already walks declared params (#240/#22b) — only the in-cap path was
declared-blind.

Fix threads the declared tuple type into the ONE shared helper pair
and its two loop sites:
  - tuple_lit_gpwords/tuplitpushelem take the declared elem type;
    declared-TAGGED + concrete rvalue widens into the shared tagged
    scratch (cg_tagscr_slot/tagscradd + cg_widen_tagged_store/
    cgwidentaggedstore, the cgreturn tagged-@retscr shape) and pushes
    the box words; declared-TAGGED gates the SSE row off (a (void|f64)
    box rides INTEGER eightbytes). Tagged->tagged subset (eslot
    mismatch) louds — the #23/#40 widening-remap family.
  - cg_tuple_lit_to_cursor/cgtuplelittocursor grow a decl param;
    cgreturn's in-cap N_TUPLE loops thread cg_ret_type/c.fnret.list
    (the same pp/pt walk its over-cap arm does); the N_LET in-cap
    tuple arm passes the declared type for an N_TUPLE rhs; the bare
    cgexpr route passes NULL/nil (emission unchanged).

Ident-elem sources keep the existing slot-load push byte-identically
(t57_ident_no_regress); the CALL-elem tripwire stays loud (#41,
t57_loud_call_elem). RESIDUAL FILED, not folded (rule 11): the
N_MASSIGN destructure-reassign literal rhs routes through the bare
cgexpr path (decl=NULL) and stays silent-wrong — probe q5_massign,
task #64, cited at the massign arm both stages. The annotated
multi-let spelling (`let (a, b): (un, size) = lit`) does not parse
(both stages), so N_MLET has no declared-literal route.

941 rows t57_*: return (named + inline union), let-literal, tagged
second elem, float payload, bare-untyped payload (rides the #33
chooser through the new wire), ident anchor, loud CALL tripwire;
ken's adversarial shapes (tagged-MID elem, two tagged rvalue elems
incl. void, plain-f64 SSE coexisting with a declared-tagged box), the
in-cap/over-cap boundary loud (k57d), and the NEW #57 tag-remap loud
pinned. Pre-fix at e8977a4: p8/p9 rows exit 1, q1_let exit 1,
q2_mixed exit 2.

Task #57.
This commit is contained in:
2026-06-05 09:05:01 +09:00
parent 80e7096f25
commit d14a23b85e
6 changed files with 796 additions and 105 deletions

View File

@@ -3289,10 +3289,20 @@ cg_structlit_fill_bp(Cg *c, Local **locals_p, Type *lu, Node *lit, int bp_off)
* 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. */
* disagreed with the checker here, latent, no consumer); a scalar 1.
*
* #57: `dt` is the DECLARED tuple element type (NULL when the consumer
* has none). The N_TUPLE literal's stamped type is CONSTRUCTED from
* its elements (check.c N_TUPLE), so a concrete rvalue under a
* declared-TAGGED slot counted ONE word here while the receive walks
* the declared eslot — the cursor shifted and every later element
* read garbage. Declared-tagged keys the count on the DECLARED box. */
static int
tuple_lit_gpwords(Node *e)
tuple_lit_gpwords(Node *e, Type *dt)
{
Type *du = dt ? type_chase_named(dt) : NULL;
if (du && du->kind == TY_TAGGED)
return tuple_eslot(dt) / 8;
int f32;
if (fld_isfloat(e->type, &f32)) return 0;
if (node_isstr(e) || node_isslice(e)) return (int)(ty_str->size / 8);
@@ -3306,20 +3316,49 @@ tuple_lit_gpwords(Node *e)
* 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 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. */
* tagged consumer reads memory), so the cursor fill must too. Shared
* by cg_tuple_lit_to_cursor and the cgreturn N_TUPLE arm — count
* (tuple_lit_gpwords) and push live or die together.
*
* #57: a DECLARED-tagged element whose expr is a concrete rvalue
* (`return (5: size, 9)` — cast, literal, call) skipped the widen
* entirely: the stamped-keyed arm below saw a scalar and pushed ONE
* word, the receiver read the declared box words — silent shift, both
* stages, gate-blind (ken /tmp/ken57). Such an element now widens
* into the shared tagged scratch (cg_widen_tagged_store, the cgreturn
* tagged-@retscr shape) and pushes the box words. A tagged→tagged
* SUBSET element (eslot mismatch) needs a tag remap on the way into
* the slot — loud (rule 7, the #23/#40 widening family). */
static void
tuple_lit_push_elem(Cg *c, Local *locals, Node *e)
tuple_lit_push_elem(Cg *c, Local **locals_p, Node *e, Type *dt)
{
Type *du = dt ? type_chase_named(dt) : NULL;
Type *eu = type_chase_named(e->type);
if (du && du->kind == TY_TAGGED
&& !(eu && eu->kind == TY_TAGGED)) {
int eslot = tuple_eslot(dt);
int scr = cg_tagscr_slot(c, locals_p, eslot);
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
for (int k = 0; k < eslot; k += 8)
ins2(c, A_MOVQ, areg(D_AX), amem(D_BP, scr + k));
cg_widen_tagged_store(c, locals_p, du, e, D_BP, scr,
eslot);
for (int k = 0; k < eslot / 8; k++) {
ins2(c, A_MOVQ, amem(D_BP, scr + k * 8),
areg(D_AX));
ins1(c, A_PUSHQ, areg(D_AX));
}
return;
}
if (du && du->kind == TY_TAGGED && eu && eu->kind == TY_TAGGED
&& tuple_eslot(dt) != tuple_eslot(e->type))
fatal("#57: tagged tuple element widening into a wider "
"declared union slot needs a tag remap (rule 7; "
"the #23/#40 widening family)");
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;
? localfind(*locals_p, e->str) : 0;
if (eoff == 0)
fatal("#22a: tagged tuple element from a non-local "
"source shape unwired (ident locals only; "
@@ -3332,7 +3371,7 @@ tuple_lit_push_elem(Cg *c, Local *locals, Node *e)
}
return;
}
cgexpr(c, e, locals);
cgexpr(c, e, *locals_p);
if (eu && eu->kind == TY_VOID)
return;
ins1(c, A_PUSHQ, areg(D_AX));
@@ -3355,17 +3394,30 @@ tuple_lit_push_elem(Cg *c, Local *locals, Node *e)
* amd64/sysv.c retr). Byte-identical extraction of cgreturn's N_TUPLE arm,
* now shared with cgexpr. Over-cap loud-stops (rule 7); a bare expression
* value can't sret, so the >cap rvalue-tuple materialisation is the #10
* follow-up. */
* follow-up.
*
* #57: `decl` is the consumer's DECLARED tuple type (NULL when it has
* none — the bare cgexpr route). A declared-TAGGED element gates the
* SSE row off (its payload may be float-stamped but the BOX rides
* INTEGER eightbytes) and keys count + push on the declared eslot —
* see tuple_lit_gpwords / tuple_lit_push_elem. */
static void
cg_tuple_lit_to_cursor(Cg *c, Local **locals, Node *tuple)
cg_tuple_lit_to_cursor(Cg *c, Local **locals, Node *tuple, Type *decl)
{
Type *du = decl ? type_chase_named(decl) : NULL;
Tparam *dp0 = (du && du->kind == TY_TUPLE) ? du->params : NULL;
int f32;
int gptotal = 0, ssecount = 0;
Tparam *dp = dp0;
for (Node *e = tuple->list; e; e = e->next) {
if (fld_isfloat(e->type, &f32))
Type *dtu = dp ? type_chase_named(dp->type) : NULL;
int dtagged = dtu && dtu->kind == TY_TAGGED;
if (!dtagged && fld_isfloat(e->type, &f32))
ssecount++;
else
gptotal += tuple_lit_gpwords(e);
gptotal += tuple_lit_gpwords(e,
dp ? dp->type : NULL);
if (dp) dp = dp->next;
}
if (gptotal > TUPLE_GPCAP || ssecount > TUPLE_SSECAP)
fatal("tuple literal exceeds register-return ABI capacity "
@@ -3383,27 +3435,36 @@ cg_tuple_lit_to_cursor(Cg *c, Local **locals, Node *tuple)
}
}
int sseidx = 0;
dp = dp0;
for (Node *e = tuple->list; e; e = e->next) {
int isflt = fld_isfloat(e->type, &f32);
Type *dtu = dp ? type_chase_named(dp->type) : NULL;
int dtagged = dtu && dtu->kind == TY_TAGGED;
int isflt = !dtagged && fld_isfloat(e->type, &f32);
if (isflt) {
cgexpr(c, e, *locals);
ins2(c, f32 ? A_MOVSS : A_MOVSD, areg(D_X0),
amem(D_BP, fscr + sseidx * 8));
sseidx++;
continue;
} else {
tuple_lit_push_elem(c, locals, e,
dp ? dp->type : NULL);
}
tuple_lit_push_elem(c, *locals, e);
if (dp) dp = dp->next;
}
for (int i = gptotal - 1; i >= 0; i--)
ins1(c, A_POPQ, areg(tuple_rseq[i]));
int j = 0;
dp = dp0;
for (Node *e = tuple->list; e; e = e->next) {
if (!fld_isfloat(e->type, &f32))
continue;
ins2(c, f32 ? A_MOVSS : A_MOVSD,
amem(D_BP, fscr + j * 8),
areg(tuple_sse_seq[j]));
j++;
Type *dtu = dp ? type_chase_named(dp->type) : NULL;
int dtagged = dtu && dtu->kind == TY_TAGGED;
if (!dtagged && fld_isfloat(e->type, &f32)) {
ins2(c, f32 ? A_MOVSS : A_MOVSD,
amem(D_BP, fscr + j * 8),
areg(tuple_sse_seq[j]));
j++;
}
if (dp) dp = dp->next;
}
}
@@ -11474,7 +11535,7 @@ cgexpr(Cg *c, Node *n, Local *locals)
* elements into the register cursor (mirror cgreturn's N_TUPLE
* arm) so a let-bind / destructure consumer reads every element,
* not just AX = 0 from the default arm below. */
cg_tuple_lit_to_cursor(c, &locals, n);
cg_tuple_lit_to_cursor(c, &locals, n, NULL);
break;
default:
cgexpr_int(c, 0);
@@ -11636,7 +11697,17 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
* receive below, exactly as before. */
if (n->rhs && lu && lu->kind == TY_TUPLE
&& cg_sret_retsize(lt) == 0) {
cgexpr(c, n->rhs, *locals);
/* #57: a tuple LITERAL rhs carries the DECLARED type
* into the cursor fill — its stamped type is element-
* constructed, so a declared-tagged element's concrete
* rvalue skipped the widen and the fill/receive cursor
* walks skewed (let-twin of the return-position bug;
* probe /tmp/p57/q1_let). Same emission as the cgexpr
* route for every declared-tagged-free literal. */
if (n->rhs->kind == N_TUPLE)
cg_tuple_lit_to_cursor(c, locals, n->rhs, lu);
else
cgexpr(c, n->rhs, *locals);
int gpcur = 0, ssecur = 0, eoff = 0, ef32;
for (Tparam *p = lu->params; p; p = p->next) {
int isflt = fld_isfloat(p->type, &ef32);
@@ -12857,11 +12928,29 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
* drives the receive sites. */
int ssecap = TUPLE_SSECAP;
int gptotal = 0, ssecount = 0, f32;
/* #57: count + push key on the DECLARED return-type
* element (cg_ret_type tuple params) — the literal's
* stamped type is element-constructed, so a declared-
* TAGGED element's concrete rvalue counted 1 word and
* skipped the widen while the caller's receive walks
* the declared eslot (2 words sent for a 3-word shape;
* ken /tmp/ken57 p8/p9). Same pp walk the over-cap arm
* already does (#240/#22b). */
Type *rttc = cg_ret_type
? type_chase_named(cg_ret_type) : NULL;
Tparam *rp0 = (rttc && rttc->kind == TY_TUPLE)
? rttc->params : NULL;
Tparam *rp = rp0;
for (Node *e = n->lhs->list; e; e = e->next) {
if (fld_isfloat(e->type, &f32))
Type *rdu = rp
? type_chase_named(rp->type) : NULL;
int rdtag = rdu && rdu->kind == TY_TAGGED;
if (!rdtag && fld_isfloat(e->type, &f32))
ssecount++;
else
gptotal += tuple_lit_gpwords(e);
gptotal += tuple_lit_gpwords(e,
rp ? rp->type : NULL);
if (rp) rp = rp->next;
}
/* #22b: classify and emit MUST agree (the #10 SSoT
* note at TUPLE_GPCAP). The over-cap DECISION rides
@@ -13014,29 +13103,42 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
}
}
int sseidx = 0;
rp = rp0;
for (Node *e = n->lhs->list; e; e = e->next) {
int isflt = fld_isfloat(e->type, &f32);
Type *rdu = rp
? type_chase_named(rp->type) : NULL;
int rdtag = rdu && rdu->kind == TY_TAGGED;
int isflt = !rdtag
&& fld_isfloat(e->type, &f32);
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;
} else {
/* scalar=AX; slice/str=AX,BX,CX; tagged
* box from its slot or widened scratch
* (tuple_lit_push_elem) */
tuple_lit_push_elem(c, locals, e,
rp ? rp->type : NULL);
}
/* scalar=AX; slice/str=AX,BX,CX; tagged box
* from its slot (tuple_lit_push_elem) */
tuple_lit_push_elem(c, *locals, e);
if (rp) rp = rp->next;
}
for (int i = gptotal - 1; i >= 0; i--)
ins1(c, A_POPQ, areg(tuple_rseq[i]));
int j = 0;
rp = rp0;
for (Node *e = n->lhs->list; e; e = e->next) {
if (!fld_isfloat(e->type, &f32))
continue;
ins2(c, f32 ? A_MOVSS : A_MOVSD,
amem(D_BP, fscr + j * 8),
areg(tuple_sse_seq[j]));
j++;
Type *rdu = rp
? type_chase_named(rp->type) : NULL;
int rdtag = rdu && rdu->kind == TY_TAGGED;
if (!rdtag && fld_isfloat(e->type, &f32)) {
ins2(c, f32 ? A_MOVSS : A_MOVSD,
amem(D_BP, fscr + j * 8),
areg(tuple_sse_seq[j]));
j++;
}
if (rp) rp = rp->next;
}
} else if (n->lhs) {
cgexpr(c, n->lhs, *locals);
@@ -13461,6 +13563,10 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
* SAME producer source the SEND walks. */
int sret_recv = (n->rhs && n->rhs->kind == N_CALL)
? cg_sret_retsize(n->rhs->type) : 0;
/* #64 (filed, rule 7): an N_TUPLE literal rhs rides this
* decl-less cgexpr route, so a declared-TAGGED element's
* concrete rvalue still fills the cursor stamped-keyed
* (silent skew) — the #57 decl wire stops at return/let. */
cgexpr(c, n->rhs, *locals);
Type *rt = n->rhs ? n->rhs->type : NULL;
Type *ru = (rt && rt->kind == TY_NAMED) ? rt->under : rt;