w6c+wwstage: tag the outer widen of a nested multi-variant union (#218)

The outer widen of a NAMED multi-variant union value into an enclosing union
mis-tagged: the store took the tagged-subset path (inner value at slot+0 plus
a sub-variant remap, collapsing every inner sub-variant onto outer tag 0),
while the match-extract reads the nested layout (outer tag at +0, inner 16B
value at +8). Store and extract disagreed, so the match selected the first
arm. Pre-existing silent miscompile, latent because error-origination sites
(`let e: io.error = <leaf>; return e`) were gate-blind — no test discriminated
a freshly-originated error at a branched caller; the io vstream surface is the
first to do so.

Fix, both stages, byte-identical: cg_variant_match (cmd/w6c/cgen.c) and its
wwstage mirror cgvariantmatch (cgenutil.ww) fall back to structural equality
of the unwrapped tagged unions when the alias collapse loses nominal identity
(a NAMED outer variant vs an unwrapped-tagged source); the widen store now
writes the inner value at slot+8 and the outer tag at +0, matching the
extract. The inner union's build/payload/extract already worked (a destructure
through the outer round-trip recovers the inner payload) — only the
outer-widen store was wrong.

Collision guard (the fallback is unsound without it): structural matching
cannot disambiguate two nominally-distinct same-shape variants in one outer
union. That is unreachable under today's nominal-lossy collapse but inverts
the moment #199b lands the nominal layer, so if >=2 outer variants
structurally match the source we hard-error at compile time citing #199b —
both stages, an enforced invariant rather than a "rare, trust it" assumption.

Folds #219: the wwstage tinfo typeeq (lib/ww/typ.ww) had no TY_TAGGED branch
and fell through to `return true` (any two tagged unions compared equal);
cstage type_eq (type.c:269) has the structural branch. The structural fallback
above is the first and only caller to compare two bare tagged unions, so #219
is unexercised — and therefore ungateable — in isolation; it folds here per
the rule-11 couldn't-split carve-out (same structural reason as #206's
N_TTUPLE fold). The added branch mirrors cstage type_eq, tightening wwstage
into alignment.

test/wcc/925_nested_union_widen_run: outer-arm select, destructure-after-
propagation (payload survives the round-trip), destructure-let, single-variant
control, and the collision-guard compile-error, each with a cstage==wwstage
byte-id check (the path is gate-blind). Interim until #199b/B-full lands the
true nominal wrapped-slot layout.
This commit is contained in:
2026-05-29 23:19:36 +09:00
parent 10cb835f99
commit 176904dffc
7 changed files with 908 additions and 4 deletions

View File

@@ -574,10 +574,40 @@ cg_variant_match(Type *vt, Type *src)
if (vt == NULL || src == NULL) return 0;
if (type_isuntyped(src)) return type_assignable(vt, src);
if (vt->kind == TY_NAMED && src->kind == TY_NAMED) return vt == src;
if (vt->kind == TY_NAMED || src->kind == TY_NAMED) return 0;
if (vt->kind == TY_NAMED || src->kind == TY_NAMED) {
/* #218: nominal identity is lost when the source's stamped
* type was collapsed to its unwrapped tagged (project
* tinfo_lossy_nominal). A NAMED multi-variant union variant vs
* an unwrapped-tagged source can still be THE nested variant —
* fall back to structural equality of the two unwrapped tagged
* unions so the outer widen tag (cg_tag_for_variant) computes.
* Sound only while the model is nominal-lossy; the collision
* guard at the widen site (cg_widen_tagged_store) enforces the
* invariant for when #199b/B-full lands true nominal layout. */
Type *vu = (vt->kind == TY_NAMED) ? vt->under : vt;
Type *su = (src->kind == TY_NAMED) ? src->under : src;
if (vu && su && vu->kind == TY_TAGGED && su->kind == TY_TAGGED)
return type_eq(vu, su);
return 0;
}
return type_eq(vt, src);
}
/* cg_variant_struct_match — structural equality of two variants ignoring
* nominal identity (peel NAMED, then type_eq). #218: the collision guard
* at the nested-widen site counts how many du variants share the source's
* *shape*; ≥2 means the structural fallback could not disambiguate them
* once nominal identity is lost. cg_variant_match (pointer-id for both-
* NAMED) would under-count here, so the guard needs the shape-only view. */
static int
cg_variant_struct_match(Type *vt, Type *src)
{
Type *vu = (vt && vt->kind == TY_NAMED) ? vt->under : vt;
Type *su = (src && src->kind == TY_NAMED) ? src->under : src;
if (vu == NULL || su == NULL) return 0;
return type_eq(vu, su);
}
/* cg_tagged_success_tag — index of the success variant in a tagged
* union. Mirrors check.c tagged_success_type: explicit-flag mode
* picks the first non-`!`-marked variant; legacy mode picks index 0. */
@@ -1771,6 +1801,63 @@ cg_widen_tagged_store(Cg *c, Local **locals_p, Type *dst, Node *src,
/* Tagged → tagged subset: copy slot words then tag-remap. */
if (su && su->kind == TY_TAGGED) {
int ssz = (int)su->size;
/* #218: is the source itself a single NESTED variant of du
* (its whole tagged type matches one du variant), rather than
* a flattened SUBSET whose members spread into du? If so, the
* inner tagged value is the payload: store it at slot+8 with
* the outer tag at slot+0, exactly like the scalar/struct/str
* single-variant arms below — NOT a copy-to-+0 + sub-variant
* remap. cg_tag_for_variant's structural fallback (cgen.c
* cg_variant_match) is what recovers the index after the
* nominal-lossy collapse. */
int nested = cg_tag_for_variant(du, st);
if (nested >= 0) {
/* drew collision guard: the structural fallback over-
* matches if ≥2 nominally-distinct du variants share the
* source's shape. Unreachable under today's nominal-lossy
* model, but INVERTS when #199b/B-full lands the nominal
* layer — hard-error NOW so a future collision STOPS the
* compiler instead of silently mis-tagging. */
int nmatch = 0;
for (Tparam *p = du->params; p; p = p->next)
if (cg_variant_struct_match(p->type, st))
nmatch++;
if (nmatch >= 2)
fatal("cg_widen_tagged_store: structural fallback "
"cannot disambiguate nominally-distinct same-"
"shape variants without nominal layout "
"(#218/#199b/B-full)");
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));
if (src->kind == N_IDENT) {
int soff = localfind(*locals_p, src->str);
for (int k = 0; k < ssz; k += 8) {
ins2(c, A_MOVQ, amem(D_BP, soff + k),
areg(D_AX));
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, write_off + 8 + k));
}
} else {
cgexpr(c, src, *locals_p);
ins2(c, A_MOVQ, areg(D_AX),
amem(D_BP, write_off + 8));
if (ssz > 8)
ins2(c, A_MOVQ, areg(D_DX),
amem(D_BP, write_off + 16));
if (ssz > 16)
ins2(c, A_MOVQ, areg(D_CX),
amem(D_BP, write_off + 24));
if (ssz > 24)
ins2(c, A_MOVQ, areg(D_R8),
amem(D_BP, write_off + 32));
}
ins2(c, A_MOVQ, aimm(nested),
amem(D_BP, write_off + 0));
if (via_outer) goto copy_out;
return;
}
if (src->kind == N_IDENT) {
int soff = localfind(*locals_p, src->str);
for (int k = 0; k < ssz; k += 8) {