w6c+wwstage: cgexpr materializes tuple rvalues + unwrap-shift for tuple-payload destructure (#241)

cgexpr could not produce a tuple VALUE, so a destructure / let bind of an
RVALUE tuple read garbage past the first element (cstage) or left an untyped
binder aborting wwstage's asserttyped gate — a DANGEROUS gate-blind cs!=ww,
and the strconv-int blocker (Hare's stoi64/stou64 require
`let (sign, u) = parseint(s, base)?`). Three feeders, all routed at the same
SysV register-return cursor the cgmlet/cgmassign consumers already read:

  - an N_TUPLE literal fell to the `cgexpr_int(0)` / `MOVQ $0, AX` default;
  - a tuple-typed IDENT loaded only word0 into AX (`yield t`, `return t`,
    `let q = t`), leaving DX/CX stale;
  - the `?`/`!` unwrap of a tuple-in-union payload lifted only word0->AX,
    stranding word1 in CX (the scalar/str success ABI).

Fix (both stages, byte-identical per rule 10):

  - cgexpr packs an N_TUPLE literal into the cursor (cg_tuple_lit_to_cursor /
    cgtuplelittocursor — a byte-identical reuse of cgreturn's in-register
    N_TUPLE arm) and a tuple IDENT from its slot at the register-ABI stride
    (cg_tuple_slot_to_cursor / cgtupleslottocursor);
  - the ?/! unwrap shifts a tuple success payload down one integer reg past
    the tag (cg_tagged_tuple_payload_shift / cgtaggedtuplepayloadshift),
    loud-stopping a float/slice/str payload element (the SysV per-eightbyte
    tagged-tuple-payload classification is #243);
  - wwstage's checker recovers the popped match-arm binder type for a
    `yield <binder>` operand (matchyieldtype's scope-free fallback to the
    arm's declared type), so the destructured binders stamp — cstage reads
    the operand's already-stamped ->type, wwstage caches only a tinfo.

Over-cap rvalue-tuple materialisation (no slot to sret a bare expression
value into) loud-stops both stages — the #10 follow-up.

NOT closed (distinct root, deferred to #238/task #6): single-var
`let q = (true, 9u64)` then `q.N` — the N_LET tuple-init sz==16||32 gate
drops a narrow-first mixed tuple, and the N_DOT tuple-field PACKED-offset
reader disagrees with tuple_store's 8B stride. Not the rvalue-into-cursor
fix and not a strconv blocker (strconv destructures); documented at the test
header.

Test 945_rvalue_tuple_destructure_run: literal destructure, match-yield
destructure, and the ?-call strconv shape, each run + cs==ww byte-id on both
drivers (9 checks). Embedded w6c/wwdump combined.ww regenerated.
This commit is contained in:
2026-06-01 21:27:49 +09:00
parent 6fc85f9aaf
commit 5d023c0ef0
8 changed files with 1229 additions and 22 deletions

View File

@@ -2568,6 +2568,163 @@ cg_structlit_fill_bp(Cg *c, Local **locals_p, Type *lu, Node *lit, int bp_off)
cg_structlit_fill(c, locals_p, lu, lit, DST_BP, 0, NULL, bp_off);
}
/* cg_tuple_lit_to_cursor — #241: materialise an N_TUPLE literal's elements
* into the SysV register-return cursor — integer words L→R over tuple_rseq
* (AX,DX,CX,R8), floats over tuple_sse_seq (X0,X1), a slice/str's
* {ptr,len,cap} header over three consecutive INTEGER regs — the SAME ABI a
* tuple-returning CALL leaves, which every tuple consumer (tuple_store at
* the N_LET/N_MLET sites) already reads. cgexpr otherwise can't make a tuple
* value (the default arm zeroed AX), so a literal/yield rvalue tuple bound
* or destructured read garbage past word0. Each element's cgexpr clobbers
* AX/X0, so integer words spill L→R and pop into the cursor reversed, floats
* spill to @tupfscr and reload by SSE index — INDEPENDENT counters (ref/qbe/
* 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. */
static void
cg_tuple_lit_to_cursor(Cg *c, Local **locals, Node *tuple)
{
int f32;
int gptotal = 0, ssecount = 0;
for (Node *e = tuple->list; e; e = e->next) {
if (fld_isfloat(e->type, &f32))
ssecount++;
else
gptotal += tuple_ebytes(node_isstr(e)
|| node_isslice(e));
}
if (gptotal > TUPLE_GPCAP || ssecount > TUPLE_SSECAP)
fatal("tuple literal exceeds register-return ABI capacity "
"(integer %d/%d, SSE %d/%d); over-cap rvalue-tuple "
"materialisation is the #10 sret follow-up",
gptotal, TUPLE_GPCAP, ssecount, TUPLE_SSECAP);
int fscr = 0;
if (ssecount > 0) {
if (cg_tupfscr != 0)
fscr = cg_tupfscr;
else {
fscr = local_alloc(c, locals, "@tupfscr",
TUPLE_SSECAP * 8, cg_frame);
cg_tupfscr = fscr;
}
}
int sseidx = 0;
for (Node *e = tuple->list; e; e = e->next) {
int isflt = fld_isfloat(e->type, &f32);
cgexpr(c, e, *locals);
if (isflt) {
ins2(c, f32 ? A_MOVSS : A_MOVSD, areg(D_X0),
amem(D_BP, fscr + sseidx * 8));
sseidx++;
continue;
}
ins1(c, A_PUSHQ, areg(D_AX));
if (node_isstr(e) || node_isslice(e)) {
ins1(c, A_PUSHQ, areg(D_BX));
ins1(c, A_PUSHQ, areg(D_CX));
}
}
for (int i = gptotal - 1; i >= 0; i--)
ins1(c, A_POPQ, areg(tuple_rseq[i]));
int j = 0;
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++;
}
}
/* cg_tuple_slot_to_cursor — #241: load a tuple already materialised in a
* BP-relative slot (a tuple-typed IDENT: a let-bound tuple, a match-bound
* union payload) into the SAME register-return cursor. The slot uses the
* register-ABI stride the tuple-init / #242 destructure write (a scalar 8B,
* a slice/str its 3-word header), NOT the packed t.N field layout (#238).
* All sources are memory, so each word loads straight into its cursor reg —
* no spill dance (unlike the literal arm whose element cgexpr clobbers). So
* `yield t` / `return t` / `let q = t` over a tuple ident leave the whole
* tuple in the cursor, not just word0 in AX. Over-cap loud-stops (rule 7;
* the #10 sret follow-up). */
static void
cg_tuple_slot_to_cursor(Cg *c, int srcoff, Type *tu)
{
int f32;
int gptotal = 0, ssecount = 0;
for (Tparam *p = tu->params; p; p = p->next) {
Type *pu = type_chase_named(p->type);
int wide = pu && (pu->kind == TY_SLICE || pu->kind == TY_STR);
if (fld_isfloat(p->type, &f32))
ssecount++;
else
gptotal += tuple_ebytes(wide);
}
if (gptotal > TUPLE_GPCAP || ssecount > TUPLE_SSECAP)
fatal("tuple ident exceeds register-return ABI capacity "
"(integer %d/%d, SSE %d/%d); over-cap rvalue-tuple "
"materialisation is the #10 sret follow-up",
gptotal, TUPLE_GPCAP, ssecount, TUPLE_SSECAP);
int gp = 0, sse = 0, foff = 0;
for (Tparam *p = tu->params; p; p = p->next) {
Type *pu = type_chase_named(p->type);
int wide = pu && (pu->kind == TY_SLICE || pu->kind == TY_STR);
int isflt = fld_isfloat(p->type, &f32);
if (isflt) {
ins2(c, f32 ? A_MOVSS : A_MOVSD,
amem(D_BP, srcoff + foff),
areg(tuple_sse_seq[sse]));
sse++;
foff += 8;
} else if (wide) {
for (int k = 0; k < 3; k++)
ins2(c, A_MOVQ,
amem(D_BP, srcoff + foff + k * 8),
areg(tuple_rseq[gp + k]));
gp += 3;
foff += (int)pu->size;
} else {
ins2(c, A_MOVQ, amem(D_BP, srcoff + foff),
areg(tuple_rseq[gp]));
gp += 1;
foff += 8;
}
}
}
/* cg_tagged_tuple_payload_shift — #241: a `?`-unwrapped tuple payload is an
* rvalue tuple that must fill the register cursor the let/destructure
* consumer reads. A tagged return leaves AX=tag, DX=word0, CX=word1,
* R8=word2; the scalar/str unwrap lifts only word0->AX, stranding word1+ in
* CX/R8. Shift the whole payload DOWN one INTEGER reg so element i lands in
* tuple_rseq[i]. A float/slice/str payload element rides a different SysV
* class (X regs / 3-word header) the flat down-shift can't place — loud-stop
* (rule 7); the per-eightbyte tagged-tuple-payload classification is the
* #243 follow-up. */
static void
cg_tagged_tuple_payload_shift(Cg *c, Type *tup)
{
static const int seq[] = { D_AX, D_DX, D_CX, D_R8 };
int f32;
int words = 0;
for (Tparam *p = tup->params; p; p = p->next) {
Type *pu = type_chase_named(p->type);
int wide = pu && (pu->kind == TY_SLICE || pu->kind == TY_STR);
if (fld_isfloat(p->type, &f32) || wide)
fatal("tuple-in-union ? unwrap: float/slice/str payload "
"element needs SysV per-eightbyte classification "
"(see #243); only integer tuple payloads supported");
words += tuple_ebytes(0);
}
/* tag occupies AX, so only DX/CX/R8 carry payload words. */
if (words > (int)nelem(seq) - 1)
fatal("tuple-in-union ? unwrap payload exceeds the 3 integer "
"return regs past the tag (%d words); see #10/#243", words);
for (int i = 0; i < words; i++)
ins2(c, A_MOVQ, areg(seq[i + 1]), areg(seq[i]));
}
static void
cgexpr(Cg *c, Node *n, Local *locals)
{
@@ -2612,7 +2769,13 @@ cgexpr(Cg *c, Node *n, Local *locals)
case N_IDENT: {
int off = localfind(locals, n->str);
if (off != 0) {
if (node_isfloat(n)) {
Type *itu = type_chase_named(n->type);
if (itu && itu->kind == TY_TUPLE) {
/* #241: a tuple ident is a value — leave the whole
* tuple in the register cursor (`yield t` / `return
* t` / `let q = t`), not just word0 in AX. */
cg_tuple_slot_to_cursor(c, off, itu);
} else if (node_isfloat(n)) {
int op = op_for(n, A_MOVSD, A_MOVSS);
ins2(c, op, amem(D_BP, off), areg(D_X0));
} else if (node_isstr(n)) {
@@ -6635,6 +6798,16 @@ cgexpr(Cg *c, Node *n, Local *locals)
ins1(c, A_POPQ, areg(D_BP));
ins0(c, A_RET);
label(c, cont);
{
/* #241: a tuple success payload is an rvalue tuple — fill
* the cursor (shift past the tag) so the destructure /
* let consumer reads every element, not just word0. */
Type *stu = type_chase_named(succ_t);
if (stu && stu->kind == TY_TUPLE) {
cg_tagged_tuple_payload_shift(c, stu);
break;
}
}
if (success_is_str) {
/* str IS []u8: success value arrives in the tagged
* ABI as DX=ptr, CX=len, R8=cap (slot 32B). Move len
@@ -6677,6 +6850,15 @@ cgexpr(Cg *c, Node *n, Local *locals)
ins2(c, A_MOVQ, aimm(60), areg(D_AX));
ins0(c, A_SYSCALL);
label(c, cont);
{
/* #241: tuple success payload fills the cursor (shift past
* the tag) — same rvalue-tuple-into-cursor story. */
Type *stu = type_chase_named(succ_t);
if (stu && stu->kind == TY_TUPLE) {
cg_tagged_tuple_payload_shift(c, stu);
break;
}
}
if (success_is_str) {
/* str IS []u8: success arrives DX=ptr, CX=len, R8=cap
* (slot 32B). Move len out before cap clobbers CX
@@ -7965,6 +8147,13 @@ cgexpr(Cg *c, Node *n, Local *locals)
}
break;
}
case N_TUPLE:
/* #241: a literal tuple rvalue `(a, b)` is a value — pack its
* 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);
break;
default:
cgexpr_int(c, 0);
break;