selfhost/cmd/wcc: type-key tagged variant match (#66, Phase-N step 3)

The user-ruled B-full semantic change: flip tagged-union variant matching
from surface-NAME to TYPE-identity (typeeq over tinfo.params), mirroring
cstage cg_variant_match (cmd/w6c/cgen.c:451). A cross-module `a.T` != `b.T`
and `type linerr=!str` != str are now distinguished by the per-decl TY_NAMED
pointer (Phase-N #64). ww has no type_assignable, so the untyped/loose arm
keeps the str/slice shape fallback (rule-10 align-down). The 5 helpers
(flatvariantidx, flatslicevariantidx, taggedvariantindex, cgtagvariantidx,
cgmatch dispatch) flip; nomem propagation (NAMED-name scan, no source value)
and the f64 widen arm (float-kind classification, no pattern node) are not
arm-by-value discrimination and stay name/kind-keyed.

The flip requires value nodes to carry nominal identity. exprtype's
N_STRUCTLIT arm stamped the flattened body, so `overflow{}` (overflow=!void)
got TY_VOID and missed its variant -- fixed to stamp the per-decl NAMED
(mktname(lhs.str) -> tinfofornode reuses the #64 NAMED build/cache, same ptr
the union variant resolved to), mirroring the N_CAST/N_IDENT arms + cstage.
Returns the body node unchanged (only e.type_ rides NAMED); struct-lit layout
is unaffected -- cgstructlitfill is structlookup(name)-keyed, never reads
NAMED.fields. The fix now hits all `T{}` stamps, kept byte-id by the #63/#65
structural-walker peels.

931_variant_typekey_run: table-driven, both stages, /tmp-isolated. Two rows
widen an alias-FIRST variant from a call (no surface name): `(linerr|str)`
str-via-call -> idx 1, `(ec|i32)` i32-via-call -> idx 1. Empirically
discriminating: FAILS pre-flip (wwstage falls to the leading-shape variant,
exit 10; cstage exit 0) and PASSES post-flip -- locking in the capability
byte-id can't reach (the corpus has no name-key/type-key-disagreeing
co-variant, which is why name-keying survived).

make test 134/134 (byte-id 990-997 green; 995 self-rebuild green).
This commit is contained in:
2026-05-23 21:22:24 +09:00
parent 0fde4beeb4
commit 3036ba766d
8 changed files with 634 additions and 369 deletions

View File

@@ -2194,19 +2194,21 @@ fn taggedvariantindex(c: *cgen, tagged: *node, rhs: *node) i32 = {
// a single canonicalization. Same shape of fix as nodeisstr.
let resolved: *node = resolvetagged(c, tagged);
if (resolved != nil) { tagged = resolved; };
let wantname: str = rhstargetname(c, rhs);
if (wantname.len > 0) {
let r: i32 = flatvariantidx(c, tagged, wantname);
if (r >= 0) { return r; };
};
// #66 Phase-N step 3: match the value's stamped type against the
// variant types by typeeq (flatvariantidx), replacing the
// rhstargetname surface-name compare. Untyped/loose values (whose
// .type_ is untyped_* and can't typeeq a concrete variant) return
// -1 here and drop to the str/slice shape scan below — ww has no
// type_assignable to mirror cg_variant_match's untyped-src arm.
let r: i32 = flatvariantidx(c, tagged, rhs);
if (r >= 0) { return r; };
// Shape fallback: classify rhs as (str, slice, scalar/other) and
// pick the first variant of matching shape. Cstage's type_eq
// distinguishes a `[]u8` arm from a `u8` arm at type-build; the
// name-only flatvariantidx pass above can't see `[]T`, so without
// the slice axis a (u8 | []u8) widen / match collapses every
// non-str rhs onto the leading scalar variant (task #19). Walks
// the spread-flattened list so a `(...inner | str)` outer agrees
// with the inner's str / slice positions.
// pick the first variant of matching shape. Stands in for cstage's
// type_assignable on an untyped src — the typeeq pass above can't
// match untyped_* against a concrete variant, and the slice axis
// keeps a (u8 | []u8) widen off the leading scalar variant (task
// #19). Walks the spread-flattened list so a `(...inner | str)`
// outer agrees with the inner's str / slice positions.
let wantstr: bool = nodeisstr(c, rhs);
let wantslice: bool = nodeisslice(c, rhs);
let v: *node = tagged.list;
@@ -2243,118 +2245,76 @@ fn taggedvariantindex(c: *cgen, tagged: *node, rhs: *node) i32 = {
return -1;
};
// flatvariantidx — walk `tagged`'s variant list (with spread `...inner`
// expansion) and return the flat 0-based index where `want` matches.
// Mirrors check.c's spread flatten at type resolution: an outer
// `(...inner | T)` has the inner's variants inlined in declaration
// order, so the tag indices stay in sync between cstage (which
// resolves types upfront) and wwstage (which doesn't). Returns -1 if
// no variant matches.
fn flatvariantidx(c: *cgen, tagged: *node, want: str) i32 = {
// flatvariantidx — flat 0-based index of the variant whose type matches
// the pattern node `pat`, by typeeq on the stamped tinfos. Reads the
// pre-flattened variant chain off tinfo.params (#61a — `...inner`
// spreads already inlined in declaration order); peels TY_NAMED then
// gates TY_TAGGED.
//
// #66 Phase-N step 3 (THE FLIP, user-ruled B-full): match by
// typeeq(p.type_, pat.type_) — nominal identity carried by the per-decl
// TY_NAMED ptr — instead of the surface-name compare. So `type linerr =
// !str` ≠ str and a cross-module `a.T` ≠ `b.T` are now distinguished.
// Mirrors cstage cg_variant_match (cmd/w6c/cgen.c:451): both-NAMED →
// ptr-id (typeeq, typ.ww:514), one-NAMED → kind mismatch → false. ww has
// no type_assignable, so the untyped/loose arm (cg_variant_match's first
// branch) lives in the caller's str/slice shape fallback, not here.
fn flatvariantidx(c: *cgen, tagged: *node, pat: *node) i32 = {
if (tagged == nil) { return -1; };
if (tagged.kind != nkind.N_TTAGGED) { return -1; };
if (want.len == 0) { return -1; };
let v: *node = tagged.list;
if (pat == nil) { return -1; };
let want: *tinfo = pat.type_: *tinfo;
if (want == nil) { return -1; };
let ti: *tinfo = tagged.type_: *tinfo;
for (ti != nil && ti.kind == tykind.TY_NAMED) { ti = ti.under; };
if (ti == nil) { return -1; };
if (ti.kind != tykind.TY_TAGGED) { return -1; };
let p: *tparam = ti.params;
let idx: i32 = 0;
for (v != nil) {
let isspread: bool = (v.op == tkind.TK_ELLIPSIS);
if (isspread) {
let inner: *node = v;
if (inner.kind == nkind.N_TNAME) {
let a: *node = aliaslookup(c, inner.str);
if (a != nil) { inner = a; };
};
if (inner != nil) {
if (inner.kind == nkind.N_TTAGGED) {
let iv: *node = inner.list;
for (iv != nil) {
if (iv.kind == nkind.N_TNAME) {
if (variantnamematch(iv.str, want)) {
return idx;
};
};
iv = iv.next;
idx += 1;
};
v = v.next;
continue;
};
};
};
if (v.kind == nkind.N_TNAME) {
if (variantnamematch(v.str, want)) { return idx; };
};
v = v.next;
for (p != nil) {
if (typeeq(p.type_, want)) { return idx; };
p = p.tnext;
idx += 1;
};
return -1;
};
// flatslicevariantidx — flat 0-based index of the first slice-shape
// variant in `tagged` (`...inner` spread expanded). When `elem` is an
// N_TNAME, prefer a `[]<elem.str>` variant; falls back to the first
// slice slot if no element match is found. Cstage walks resolved
// Type pointers and dispatches via cg_tag_for_variant / type_eq;
// wwstage's name-keyed flatvariantidx can't see a `[]u8` variant
// (pat.str == ""), collapsing every (scalar | []T) match arm and
// widen-to-tagged call onto tag 0. Task #19. Returns -1 when no
// slice variant exists.
// flatslicevariantidx — flat 0-based index of a slice-shape variant in
// `tagged`. Prefers the variant whose element typeeq's the pattern
// element `elem`; falls back to the first slice-shape slot when no exact
// element match is found (the untyped/loose arm — ww has no
// type_assignable). Reads the flattened tinfo.params chain (#61a); peels
// TY_NAMED then gates TY_TAGGED. The slice axis exists because a scalar-
// vs-`[]T` distinction has no surface name to key on (task #19).
//
// #66 Phase-N step 3: element compare flips from surface-name to
// typeeq(p.type_.sub, elem.type_). Mirrors cstage cg_tag_for_variant
// over Type->params. Returns -1 when no slice variant exists.
fn flatslicevariantidx(c: *cgen, tagged: *node, elem: *node) i32 = {
if (tagged == nil) { return -1; };
if (tagged.kind != nkind.N_TTAGGED) { return -1; };
let elemname: str;
elemname.ptr = nil; elemname.len = 0;
if (elem != nil) {
if (elem.kind == nkind.N_TNAME) { elemname = elem.str; };
};
let ti: *tinfo = tagged.type_: *tinfo;
for (ti != nil && ti.kind == tykind.TY_NAMED) { ti = ti.under; };
if (ti == nil) { return -1; };
if (ti.kind != tykind.TY_TAGGED) { return -1; };
let want: *tinfo = nil;
if (elem != nil) { want = elem.type_: *tinfo; };
let fallback: i32 = -1;
let v: *node = tagged.list;
let p: *tparam = ti.params;
let idx: i32 = 0;
for (v != nil) {
let isspread: bool = (v.op == tkind.TK_ELLIPSIS);
if (isspread) {
let inner: *node = v;
if (inner.kind == nkind.N_TNAME) {
let a: *node = aliaslookup(c, inner.str);
if (a != nil) { inner = a; };
};
if (inner != nil) {
if (inner.kind == nkind.N_TTAGGED) {
let iv: *node = inner.list;
for (iv != nil) {
if (isslicetype(c, iv)) {
if (fallback < 0) { fallback = idx; };
if (elemname.len > 0) {
if (iv.kind == nkind.N_TSLICE) {
if (iv.lhs != nil) {
if (iv.lhs.kind == nkind.N_TNAME) {
if (variantnamematch(iv.lhs.str, elemname)) { return idx; };
};
};
};
};
};
iv = iv.next;
idx += 1;
};
v = v.next;
continue;
};
};
};
if (isslicetype(c, v)) {
if (fallback < 0) { fallback = idx; };
if (elemname.len > 0) {
if (v.kind == nkind.N_TSLICE) {
if (v.lhs != nil) {
if (v.lhs.kind == nkind.N_TNAME) {
if (variantnamematch(v.lhs.str, elemname)) { return idx; };
};
for (p != nil) {
let vt: *tinfo = p.type_;
if (vt != nil) {
if (typeisslice(vt)) {
if (fallback < 0) { fallback = idx; };
if (want != nil) {
let su: *tinfo = vt;
for (su != nil && su.kind == tykind.TY_NAMED) { su = su.under; };
if (su != nil) {
if (typeeq(su.sub, want)) { return idx; };
};
};
};
};
v = v.next;
p = p.tnext;
idx += 1;
};
return fallback;
@@ -2948,15 +2908,36 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *node, src: *node, slot_off: i32, slot_sz
let fkind: i32 = exprfloatkind(c, src);
if (fkind != 0) {
let fmov: str = "MOVSD";
let fname: str = "f64";
if (fkind == 1) { fmov = "MOVSS"; fname = "f32"; };
if (fkind == 1) { fmov = "MOVSS"; };
cgexpr(c, src);
emitline("\t");
emitline(fmov);
emitline("\tX0, ");
emitoff((slot_off + 8): i64);
emitline("(BP)\n");
let ftag: i32 = flatvariantidx(c, dt, fname);
// #66 Phase-N step 3: the float arm has no pattern node to ride
// the typeeq flatvariantidx path, so pick the variant by float
// kind (f32 vs f64) over tinfo.params — a shape classification
// like the slice axis, not nominal identity.
let wantf32: bool = (fkind == 1);
let ftag: i32 = -1;
let fti: *tinfo = dt.type_: *tinfo;
for (fti != nil && fti.kind == tykind.TY_NAMED) { fti = fti.under; };
if (fti != nil) { if (fti.kind == tykind.TY_TAGGED) {
let fp: *tparam = fti.params;
let fidx: i32 = 0;
for (fp != nil) {
let fvt: *tinfo = fp.type_;
for (fvt != nil && fvt.kind == tykind.TY_NAMED) { fvt = fvt.under; };
if (fvt != nil) {
if (typeisfloat(fvt)) {
if (typeisf32(fvt) == wantf32) { ftag = fidx; break; };
};
};
fp = fp.tnext;
fidx += 1;
};
}; };
if (ftag < 0) { ftag = 0; };
emitline("\tMOVQ\t$");
emitint(ftag: i64);