wcc/cgen: #64+#68 tuple-literal cursor-fill decl-blind — massign + call-arg widen (both-stage)

A tuple LITERAL with a declared-tagged element reached the cursor-fill
helper (cg_tuple_lit_to_cursor) through the generic cgexpr(N_TUPLE) arm
with no declared type, so the element was stored stamped-keyed at its
constructed scalar width rather than widened into the declared tagged box.
Both consumers ran silent and wrong on both stages (#263 gate-blind:
cs==ww byte-identical, both wrong — runtime is the only net).

#64 massign: N_MASSIGN derives a declared tuple type from the lvalue
binding types and threads it into cg_tuple_lit_to_cursor + the receive
loop (mirror of the #57 N_LET wire); a `_` target falls back to the rhs
literal element type for cursor stride.

#68 call-arg: the send is made param-aware (fill over the PARAM tuple) and
the restage guard graduates a declared-tagged element to a real widen
(reusing cg_widen_tagged_store); nested tuple/struct/array elements and
tagged elements with no param decl stay rule-7 loud. The matching
pop/drain is made param-aware too so push count == pop count: a
param-aware send pushes the box's N words, so the drain must pop N or the
SysV arg sequence skews. This is a push/pop balance requirement of the
send change, not a separate latent under-drain (the standalone trailing-
arg drain is already correct at HEAD).

Closed by construction: the only remaining cg_tuple_lit_to_cursor caller
passing NULL/nil is the generic cgexpr(N_TUPLE) arm, provably non-widening
(constructed type == governing type). The four widening consumers — LET,
RETURN, MASSIGN, call-arg — are all decl-wired. Whole-tuple single-ident
reassign from a tuple literal is rule-7 loud (task #49), not a silent
widening consumer, so the residual NULL arm stays non-widening.

Pin: 945_tuple_lit_declblind_run — massign / call-arg / `_`-control /
call-arg-drain / nested-tuple-ERR rows, each base-fail at abd97e6 and
post-pass with cs==ww byte-id.
This commit is contained in:
2026-06-07 11:23:48 +09:00
parent abd97e6c57
commit 39432f717c
8 changed files with 825 additions and 141 deletions

View File

@@ -829,22 +829,40 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node, memphase: bool) i32 = {
emitline("\tX0, (SP)\n");
return rest + 1;
};
cgexpr(c, arg);
// #163/#32 (C-t2): tuple ARG (param twin of #164's return). cgexpr
// left the tuple in the return-ABI cursor (AX/DX/CX/R8 + X0/X1) for
// every nodetuplearg producer — call (return ABI), ident
// (cgtupleslottocursor), literal (cgtuplelittocursor), `?`/`!`
// #68: a tuple-LITERAL arg derives its DECLARED tuple type from the
// callee PARAM type node (the #57 decl wire, extended to call-arg
// send), so a declared-tagged element's concrete rvalue widens into
// the box cursor; the restage + push then key on the param tuple type
// node (declared element widths), not the literal's element-
// constructed types. Mirrors cstage cgcall paramtup.
let paramtt: *node = nil;
if (arg.kind == nkind.N_TUPLE) { if (param != nil) {
if (param.kind == nkind.N_PARAM && param.lhs != nil) {
let ptn: *node = param.lhs;
for (ptn != nil && ptn.kind == nkind.N_TNAME) {
ptn = aliaslookup(c, ptn.str);
};
if (ptn != nil) { if (ptn.kind == nkind.N_TTUPLE) { paramtt = ptn; }; };
};
}; };
if (paramtt != nil) { cgtuplelittocursor(c, arg, paramtt); }
else { cgexpr(c, arg); };
// #163/#32 (C-t2): tuple ARG (param twin of #164's return). cgexpr /
// the decl-aware fill left the tuple in the return-ABI cursor (AX/DX/
// CX/R8 + X0/X1) for every nodetuplearg producer — call (return ABI),
// ident (cgtupleslottocursor), literal (cgtuplelittocursor), `?`/`!`
// unwrap (payload shift); restage it into @tupargscr by SysV class
// (tupstore, the #164 helper) and push the slot words high->low so
// the pop drains slot+0 first into the SysV ARG cursor. The frame
// slot decouples the return-class regs from the overlapping
// arg-class regs. An N_TUPLE literal's elements are VALUE exprs —
// classify them the way cgtuplelittocursor does (nodeisstr/
// nodeisslice), kind-discriminated; the stride is the slot formula
// (wide ? header : 8), the same number slotsize() gives a type node.
let tuparg: *node = nodetuplearg(c, arg);
// arg-class regs. When the PARAM tuple type is known (#68), walk its
// declared element type nodes (p.lhs); else an N_TUPLE literal's
// elements are VALUE exprs classified the way cgtuplelittocursor does.
let tuparg: *node = paramtt;
if (tuparg == nil) { tuparg = nodetuplearg(c, arg); };
if (tuparg != nil) {
let tuplit: bool = tuparg.kind == nkind.N_TUPLE;
let tuplit: bool = false;
if (paramtt == nil) { if (tuparg.kind == nkind.N_TUPLE) { tuplit = true; }; };
let gptot: i32 = 0;
let sstot: i32 = 0;
let tsz: i32 = 0;
@@ -863,23 +881,34 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node, memphase: bool) i32 = {
let eti: *tinfo = et.type_: *tinfo;
eti = tichase(eti);
if (eti != nil) {
if (eti.kind == tykind.TY_TUPLE
let bad: bool = eti.kind == tykind.TY_TUPLE
|| eti.kind == tykind.TY_STRUCT
|| eti.kind == tykind.TY_ARRAY
|| eti.kind == tykind.TY_TAGGED) {
|| eti.kind == tykind.TY_ARRAY;
// #68: a declared-tagged element graduates to a real
// widen — the decl-aware send (cgtuplelittocursor over
// the param tuple type) left the box words in the
// cursor, so tupstore below carries them. A tagged
// element with no param decl stays rule-7 loud (the
// cursor was filled stamped-keyed).
if (eti.kind == tykind.TY_TAGGED && paramtt == nil) { bad = true; };
if (bad) {
let mne: str = "#32: tuple arg element kind unsupported (nested tuple/struct/array/tagged; rule 7)\n";
os.write(2, mne.ptr, mne.len: u64);
os.exit(1);
};
};
// tagged is guarded loud above, so wide-vs-scalar is
// the full slot split here; eslot keeps the stride
// arithmetic on the accessor scale (#22).
let wide: bool = false;
if (tuplit) { wide = nodeisstr(c, et) || nodeisslice(c, et); }
else { wide = isstrtype(c, et) || isslicetype(c, et); };
// eslot — the full slot stride (str/slice header, tagged
// box, else 8); on the declared (non-tuplit) path tupeslotn
// reads the element TYPE node directly (#68 box-aware,
// mirrors cstage tuple_eslot). A literal element rides the
// wide-vs-scalar split off its VALUE node.
let eslot: i32 = 8;
if (wide) { eslot = tyslicesize(): i32; };
if (tuplit) {
let wide: bool = nodeisstr(c, et) || nodeisslice(c, et);
if (wide) { eslot = tyslicesize(): i32; };
} else {
eslot = tupeslotn(et);
};
if (isfloattype(c, et)) { sstot += 1; }
else { gptot += eslot / 8; };
tsz += eslot;
@@ -900,11 +929,13 @@ fn pushargsrev(c: *cgen, arg: *node, param: *node, memphase: bool) i32 = {
for (p != nil) {
let et: *node = p.lhs;
if (tuplit) { et = p; };
let wide: bool = false;
if (tuplit) { wide = nodeisstr(c, et) || nodeisslice(c, et); }
else { wide = isstrtype(c, et) || isslicetype(c, et); };
let eslot: i32 = 8;
if (wide) { eslot = tyslicesize(): i32; };
if (tuplit) {
let wide: bool = nodeisstr(c, et) || nodeisslice(c, et);
if (wide) { eslot = tyslicesize(): i32; };
} else {
eslot = tupeslotn(et);
};
tupstore(c, gpcur, ssecur, scr + eoff, eslot, et);
if (isfloattype(c, et)) { ssecur += 1; }
else { gpcur += eslot / 8; };