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:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user