w6c+wwstage: construct + bind tuple-in-union payload (#242)

A mixed-scalar tuple WRAPPED IN A TAGGED UNION (the (neg, n) shape Hare's
strconv parseint returns, ((bool,u64)|invalid|overflow)) miscompiled three
ways, all gate-blind (no bootstrap tuple-in-union):

(a) cstage CONSTRUCTION: a tuple variant fell through the N_RETURN scalar
    shuffle, which ZEROED tag + payload — the operands were never packed.
    Route the tuple variant through the scratch-slot widen path; add a
    TY_TUPLE arm to cg_widen_tagged_store that packs each element into the
    union payload at the register-ABI 8B stride + sets the variant tag.

(b) wwstage CHECKER: `let (a,b)=t` over a plain tuple ident (the match-
    bound union payload) left the un-annotated binders UNTYPED, so the bin
    node reading them was untyped -> asserttyped abort. The element-type
    distribution only fired for an N_CALL rhs. Consume the rhs tuple type
    for ANY rhs (mirror cstage check.c:2017).

(c) BOTH stages DESTRUCTURE: the register-cursor receive assumes the rhs
    left every element in AX/DX/CX (a call's tuple-return ABI). For a tuple
    IDENT cgexpr loads only word0->AX, so the 2nd binder read a STALE DX.
    Copy each element from the ident's slot at the 8B stride.

Construction is correct at ANY variant position (the resolved tag, not a
default 0); wwstage resolves it via the typeeq core (flatvariantidxt), not
taggedvariantindext whose str/slice shape-fallback would mask a mismatch.

Two rule-7 loud-stops cover shapes this slotted packing can't yet handle,
on BOTH stages, so neither silently miscompiles:

  - a tuple with a SysV-eightbyte-sharing narrow pair (e.g. (i32,i32,u64)),
    caught by the 8+payload > slot-size guard (the eightbyte tuple
    classification is #243);

  - a tuple built from a BARE LITERAL element (`true`/`false`, suffix-less
    `7`). cstage's cg_tag_for_variant can't type the literal (#241), returns
    -1, and loud-stops. wwstage types `true` as bool and `7` as untyped_int,
    so flatvariantidxt WOULD resolve the variant — a program cstage rejects
    but wwstage accepts is the cs!=ww divergence rule 10 forbids. wwstage
    mirrors cstage's CONDITION (a bare-literal element), not its -1
    mechanism, with an explicit guard that aligns the richer side DOWN. Lift
    BOTH guards together when #241 lands cstage literal typing -> symmetric
    accept.

Test 940_tuple_in_union: 4 K_RUN rows (variant 0, void arm, tuple at
variant 1 two ways) x cstage-run + wwstage-run + cs==ww byte-id, plus 2
K_BUILDERR rows (eightbyte-share, bare-literal) asserting a loud stop with
the #242 diagnostic on BOTH drivers = 16 ok.
This commit is contained in:
2026-06-01 19:56:52 +09:00
parent b79f005489
commit 6fc85f9aaf
8 changed files with 1114 additions and 16 deletions

View File

@@ -2034,6 +2034,74 @@ cg_widen_tagged_store(Cg *c, Local **locals_p, Type *dst, Node *src,
if (via_outer) goto copy_out;
return;
}
/* #242: tuple payload. Each element rides ONE register-ABI
* eightbyte — scalar/float a single 8B word, a slice/str its 3-word
* {ptr,len,cap} header (24B) — matching the tagged-return load
* (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) {
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
* variant tag can't resolve — the supported shape is a tuple of
* TYPED expressions (the strconv parseint `(neg, n)` shape).
* Loud-stop rather than silently mis-tag (tag 0) — rule 7.
* Untyped tuple-element coercion is the #241 literal-init
* family. */
if (tag < 0)
fatal("cg_widen_tagged_store: tuple-in-union variant tag "
"unresolved (untyped/literal tuple element; "
"see #242 / #241)");
/* #242: this 8B-per-eightbyte packing is correct only when no
* two scalar elements share a SysV eightbyte — e.g. (bool,u64),
* where the sub-8 bool is padded out by u64's 8-alignment. A
* tuple whose natural aligned layout packs two narrows into one
* eightbyte (e.g. (i32,i32,u64)) would overflow the union
* 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)
total += (node_isstr(e) || node_isslice(e)) ? 24 : 8;
if (8 + total > sz)
fatal("cg_widen_tagged_store: tuple-in-union payload needs "
"SysV eightbyte packing (narrow elements share an "
"eightbyte; see #242 follow-up)");
ins2(c, A_XORQ, areg(D_AX), areg(D_AX));
for (int k = 0; k < sz; k += 8)
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) {
int e_isf32 = 0;
int isflt = fld_isfloat(e->type, &e_isf32);
int wide = node_isstr(e) || node_isslice(e);
int esz = e->type ? (int)e->type->size : 8;
cgexpr(c, e, *locals_p);
if (isflt) {
ins2(c, e_isf32 ? A_MOVSS : A_MOVSD,
areg(D_X0),
amem(D_BP, write_off + 8 + foff));
} else if (wide) {
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, write_off + 8 + foff + 0));
ins2(c, A_MOVQ, areg(D_BX),
amem(D_BP, write_off + 8 + foff + 8));
ins2(c, A_MOVQ, areg(D_CX),
amem(D_BP, write_off + 8 + foff + 16));
} else {
ins2(c, fldstoreop(e->type, esz),
areg(D_AX),
amem(D_BP, write_off + 8 + foff));
}
foff += wide ? 24 : 8;
}
ins2(c, A_MOVQ, aimm(tag < 0 ? 0 : tag),
amem(D_BP, write_off + 0));
if (via_outer) goto copy_out;
return;
}
/* Struct payload: zero the whole slot, then write fields/words
* at slot+8+ — keeping the tag word at slot+0 from the zero-fill,
* then patch it with the variant tag. */
@@ -8399,12 +8467,19 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
int passthrough = istagged && (vu == rt ||
type_eq(vt, cg_ret_type));
int isstruct = vu && vu->kind == TY_STRUCT;
/* #242: a tuple variant must be PACKED into the union
* payload (tag + per-element words), not shuffled like a
* bare scalar — route it through the scratch-slot widen
* path (cg_widen_tagged_store TY_TUPLE arm). The scalar
* arm below zeroed the whole value (never packed the
* operands). */
int istuple = vu && vu->kind == TY_TUPLE;
if (rt->nullable) {
cgexpr(c, n->lhs, *locals);
} else if (passthrough) {
/* same tagged type: forward AX/DX/CX. */
cgexpr(c, n->lhs, *locals);
} else if (!istagged && !isstruct) {
} else if (!istagged && !isstruct && !istuple) {
/* str / slice / scalar variant: synthesise
* the tag in AX and shuffle the value into
* DX[/CX[/R8]]. Direct register path keeps
@@ -9123,6 +9198,55 @@ cgstmt(Cg *c, Node *n, Local **locals, int *frame)
* at its NATURAL width (#169). The receive has no single lvalue
* dest, so it reuses the same per-fn @sretscr slot a discarded
* sret call would; the in-reg path below is unchanged. */
/* #242: rhs is a tuple already materialised in a local slot (a
* match-bound union payload, `let (a,b)=t`), NOT a register-
* returning call. cgexpr(tuple ident) loads only word0->AX, so
* the register-cursor path below reads DX/CX stale. Copy each
* element from the ident's slot at the register-ABI 8B stride
* (24B for a slice/str header) — the SAME layout the tagged
* construct + match payload-bind write. */
if (n->rhs && n->rhs->kind == N_IDENT) {
Type *rty = type_chase_named(n->rhs->type);
if (rty && rty->kind == TY_TUPLE) {
int srcoff = localfind(*locals, n->rhs->str);
int lf32b;
int foff = 0;
for (Node *l = n->list; l; l = l->next) {
Type *t = l->type;
Type *u = type_chase_named(t);
int wide = u && (u->kind == TY_SLICE
|| u->kind == TY_STR);
int isflt = fld_isfloat(t, &lf32b);
int esz = t ? (int)t->size : 8;
int bsz = wide ? esz : 8;
int off = localoff(c, locals, l->str,
bsz, frame);
if (isflt) {
ins2(c, lf32b ? A_MOVSS : A_MOVSD,
amem(D_BP, srcoff + foff),
areg(D_X0));
ins2(c, lf32b ? A_MOVSS : A_MOVSD,
areg(D_X0), amem(D_BP, off));
} else if (wide) {
for (int k = 0; k < esz; k += 8) {
ins2(c, A_MOVQ,
amem(D_BP, srcoff + foff + k),
areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, off + k));
}
} else {
ins2(c, fldloadop(t, esz),
amem(D_BP, srcoff + foff),
areg(D_AX));
ins2(c, fldstoreop(t, esz),
areg(D_AX), amem(D_BP, off));
}
foff += wide ? 24 : 8;
}
break;
}
}
int sret_recv = (n->rhs && n->rhs->kind == N_CALL)
? cg_sret_retsize(n->rhs->type) : 0;
cgexpr(c, n->rhs, *locals);