diff --git a/Makefile b/Makefile index 1cb9c9ff..31d6bb62 100644 --- a/Makefile +++ b/Makefile @@ -261,6 +261,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_modcall_widen_slice \ $(BIN)/test_match_4arm_cross_module \ $(BIN)/test_match_4arm_cross_module_run \ + $(BIN)/test_variant_typekey_run \ $(BIN)/test_enum_modshadow \ $(BIN)/test_struct_modshadow \ $(BIN)/test_def_modshadow \ @@ -785,6 +786,12 @@ $(BIN)/test_match_4arm_cross_module_run: test/wcc/929_match_4arm_cross_module_ru $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_variant_typekey_run: test/wcc/931_variant_typekey_run.c \ + $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ + $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ + $(LIB)/libwwrt.a | $(BIN) + $(CC) $(CFLAGS) -o $@ $< + $(BIN)/test_use_promote_alias: test/wcc/699_use_promote_alias.c \ $(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(LIB)/libwwrt.a | $(BIN) diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 092cdd44..49b35211 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -9225,7 +9225,26 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { if (ms != nil) { if (ms.skind == skind.SK_TYPE) { if (ms.decl != nil) { let tn: *node = ms.decl.lhs; if (tn != nil) { - e.type_ = tinfofornode(c, tn): *void; + // #66 Phase-N step 3: stamp e.type_ to the NOMINAL + // per-decl NAMED, not the flattened body. `overflow{}` + // where `type overflow = !void` must carry + // NAMED(overflow) so the typeeq variant match + // (cgenutil flatvariantidx) selects the overflow arm + // instead of falling to the scalar shape fallback; + // stamping tinfofornode(tn) gave the body (TY_VOID) + // and lost nominal identity. Resolve through a + // synthesized TNAME to reuse tinfofornode's TY_NAMED + // build/cache (check.ww:1157) — the SAME NAMED ptr + // the union variant resolved to. Mirrors cstage + // resolving overflow{} to the overflow Type, and ww's + // own N_CAST / N_IDENT arms which already stamp NAMED. + // Return the body node tn unchanged: byte-id rides + // e.type_ (cgen), while the checker's AST-level + // assign/return checks keep their prior input. + let tnm: *node = mktname(c, e.lhs.str); + let nti: *tinfo = tinfofornode(c, tnm); + if (nti != nil) { e.type_ = nti: *void; } + else { e.type_ = tinfofornode(c, tn): *void; }; return tn; }; }; }; }; @@ -12720,19 +12739,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; @@ -12769,118 +12790,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 `[]` 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; @@ -13474,15 +13453,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); @@ -14116,17 +14116,16 @@ fn cgtagvariantidx(c: *cgen, tagged: *node, vt: *node) i32 = { if (tagged == nil) { return -1; }; if (vt == nil) { return -1; }; if (tagged.kind != nkind.N_TTAGGED) { return -1; }; - // `is []T` / `as []T` — slice-shape variant lookup routes through - // the shape-aware helper so non-N_TNAME variant nodes (which - // flatvariantidx's name key can't see) resolve. Task #19. + // `is []T` / `as []T` — slice-shape lookup routes through the + // element-aware helper, which carries the loose first-slice-shape + // fallback (cstage type_assignable stand-in) that flatvariantidx's + // strict typeeq below doesn't. Task #19; #66 refresh. if (vt.kind == nkind.N_TSLICE) { return flatslicevariantidx(c, tagged, vt.lhs); }; - let want: str; - want.ptr = nil; want.len = 0; - if (vt.kind == nkind.N_TNAME) { want = vt.str; }; - if (want.len == 0) { return -1; }; - return flatvariantidx(c, tagged, want); + // #66 Phase-N step 3: match by typeeq on vt's stamped tinfo + // (flatvariantidx), not vt's surface name. + return flatvariantidx(c, tagged, vt); }; // cgtryprop — `e?` propagates the error variant up the stack. @@ -15119,7 +15118,7 @@ fn cgmatch(c: *cgen, n: *node) void = { if (scrutt.kind == nkind.N_TTAGGED) { let r: i32 = -1; if (pat.kind == nkind.N_TNAME) { - r = flatvariantidx(c, scrutt, pat.str); + r = flatvariantidx(c, scrutt, pat); } else { if (pat.kind == nkind.N_TSLICE) { // `case let s: []T =>` — pat.str is empty // because the variant is a composite, so @@ -20495,7 +20494,28 @@ fn cglet(c: *cgen, n: *node) void = { emitline("\tJNE\t"); emitline(okl); emitline("\n"); - let nidx: i32 = flatvariantidx(c, c.fnret, "nomem"); + // #66 Phase-N step 3: nomem propagation has no + // pattern node, so it can't ride the typeeq + // flatvariantidx path. cstage passes the ty_nomem + // singleton to cg_tag_for_variant; the wwstage cgen + // holds no tinfo singleton, so find the nomem + // variant by its NAMED name over tinfo.params. + let nidx: i32 = -1; + let nti: *tinfo = nil; + if (c.fnret != nil) { nti = c.fnret.type_: *tinfo; }; + for (nti != nil && nti.kind == tykind.TY_NAMED) { nti = nti.under; }; + if (nti != nil) { if (nti.kind == tykind.TY_TAGGED) { + let np: *tparam = nti.params; + let nidx2: i32 = 0; + for (np != nil) { + let nvt: *tinfo = np.type_; + if (nvt != nil) { + if (variantnamematch(nvt.name, "nomem")) { nidx = nidx2; break; }; + }; + np = np.tnext; + nidx2 += 1; + }; + }; }; if (nidx < 0) { nidx = 1; }; emitline("\tMOVQ\t$"); emitint(nidx: i64); diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 15674293..dd2f1565 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -117,17 +117,16 @@ fn cgtagvariantidx(c: *cgen, tagged: *node, vt: *node) i32 = { if (tagged == nil) { return -1; }; if (vt == nil) { return -1; }; if (tagged.kind != nkind.N_TTAGGED) { return -1; }; - // `is []T` / `as []T` — slice-shape variant lookup routes through - // the shape-aware helper so non-N_TNAME variant nodes (which - // flatvariantidx's name key can't see) resolve. Task #19. + // `is []T` / `as []T` — slice-shape lookup routes through the + // element-aware helper, which carries the loose first-slice-shape + // fallback (cstage type_assignable stand-in) that flatvariantidx's + // strict typeeq below doesn't. Task #19; #66 refresh. if (vt.kind == nkind.N_TSLICE) { return flatslicevariantidx(c, tagged, vt.lhs); }; - let want: str; - want.ptr = nil; want.len = 0; - if (vt.kind == nkind.N_TNAME) { want = vt.str; }; - if (want.len == 0) { return -1; }; - return flatvariantidx(c, tagged, want); + // #66 Phase-N step 3: match by typeeq on vt's stamped tinfo + // (flatvariantidx), not vt's surface name. + return flatvariantidx(c, tagged, vt); }; // cgtryprop — `e?` propagates the error variant up the stack. @@ -1120,7 +1119,7 @@ fn cgmatch(c: *cgen, n: *node) void = { if (scrutt.kind == nkind.N_TTAGGED) { let r: i32 = -1; if (pat.kind == nkind.N_TNAME) { - r = flatvariantidx(c, scrutt, pat.str); + r = flatvariantidx(c, scrutt, pat); } else { if (pat.kind == nkind.N_TSLICE) { // `case let s: []T =>` — pat.str is empty // because the variant is a composite, so diff --git a/selfhost/cmd/wcc/cgenstmt.ww b/selfhost/cmd/wcc/cgenstmt.ww index 8714edfe..e868cab9 100644 --- a/selfhost/cmd/wcc/cgenstmt.ww +++ b/selfhost/cmd/wcc/cgenstmt.ww @@ -667,7 +667,28 @@ fn cglet(c: *cgen, n: *node) void = { emitline("\tJNE\t"); emitline(okl); emitline("\n"); - let nidx: i32 = flatvariantidx(c, c.fnret, "nomem"); + // #66 Phase-N step 3: nomem propagation has no + // pattern node, so it can't ride the typeeq + // flatvariantidx path. cstage passes the ty_nomem + // singleton to cg_tag_for_variant; the wwstage cgen + // holds no tinfo singleton, so find the nomem + // variant by its NAMED name over tinfo.params. + let nidx: i32 = -1; + let nti: *tinfo = nil; + if (c.fnret != nil) { nti = c.fnret.type_: *tinfo; }; + for (nti != nil && nti.kind == tykind.TY_NAMED) { nti = nti.under; }; + if (nti != nil) { if (nti.kind == tykind.TY_TAGGED) { + let np: *tparam = nti.params; + let nidx2: i32 = 0; + for (np != nil) { + let nvt: *tinfo = np.type_; + if (nvt != nil) { + if (variantnamematch(nvt.name, "nomem")) { nidx = nidx2; break; }; + }; + np = np.tnext; + nidx2 += 1; + }; + }; }; if (nidx < 0) { nidx = 1; }; emitline("\tMOVQ\t$"); emitint(nidx: i64); diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index 47898dd1..e867f69c 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -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 `[]` 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); diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index b94c5c5d..f38fa4a4 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -2128,7 +2128,26 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { if (ms != nil) { if (ms.skind == skind.SK_TYPE) { if (ms.decl != nil) { let tn: *node = ms.decl.lhs; if (tn != nil) { - e.type_ = tinfofornode(c, tn): *void; + // #66 Phase-N step 3: stamp e.type_ to the NOMINAL + // per-decl NAMED, not the flattened body. `overflow{}` + // where `type overflow = !void` must carry + // NAMED(overflow) so the typeeq variant match + // (cgenutil flatvariantidx) selects the overflow arm + // instead of falling to the scalar shape fallback; + // stamping tinfofornode(tn) gave the body (TY_VOID) + // and lost nominal identity. Resolve through a + // synthesized TNAME to reuse tinfofornode's TY_NAMED + // build/cache (check.ww:1157) — the SAME NAMED ptr + // the union variant resolved to. Mirrors cstage + // resolving overflow{} to the overflow Type, and ww's + // own N_CAST / N_IDENT arms which already stamp NAMED. + // Return the body node tn unchanged: byte-id rides + // e.type_ (cgen), while the checker's AST-level + // assign/return checks keep their prior input. + let tnm: *node = mktname(c, e.lhs.str); + let nti: *tinfo = tinfofornode(c, tnm); + if (nti != nil) { e.type_ = nti: *void; } + else { e.type_ = tinfofornode(c, tn): *void; }; return tn; }; }; }; }; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 49a24f82..836c1813 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -9225,7 +9225,26 @@ fn exprtype(c: *checker, e: *node, hint: *node) *node = { if (ms != nil) { if (ms.skind == skind.SK_TYPE) { if (ms.decl != nil) { let tn: *node = ms.decl.lhs; if (tn != nil) { - e.type_ = tinfofornode(c, tn): *void; + // #66 Phase-N step 3: stamp e.type_ to the NOMINAL + // per-decl NAMED, not the flattened body. `overflow{}` + // where `type overflow = !void` must carry + // NAMED(overflow) so the typeeq variant match + // (cgenutil flatvariantidx) selects the overflow arm + // instead of falling to the scalar shape fallback; + // stamping tinfofornode(tn) gave the body (TY_VOID) + // and lost nominal identity. Resolve through a + // synthesized TNAME to reuse tinfofornode's TY_NAMED + // build/cache (check.ww:1157) — the SAME NAMED ptr + // the union variant resolved to. Mirrors cstage + // resolving overflow{} to the overflow Type, and ww's + // own N_CAST / N_IDENT arms which already stamp NAMED. + // Return the body node tn unchanged: byte-id rides + // e.type_ (cgen), while the checker's AST-level + // assign/return checks keep their prior input. + let tnm: *node = mktname(c, e.lhs.str); + let nti: *tinfo = tinfofornode(c, tnm); + if (nti != nil) { e.type_ = nti: *void; } + else { e.type_ = tinfofornode(c, tn): *void; }; return tn; }; }; }; }; @@ -12720,19 +12739,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; @@ -12769,118 +12790,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 `[]` 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; @@ -13474,15 +13453,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); @@ -14116,17 +14116,16 @@ fn cgtagvariantidx(c: *cgen, tagged: *node, vt: *node) i32 = { if (tagged == nil) { return -1; }; if (vt == nil) { return -1; }; if (tagged.kind != nkind.N_TTAGGED) { return -1; }; - // `is []T` / `as []T` — slice-shape variant lookup routes through - // the shape-aware helper so non-N_TNAME variant nodes (which - // flatvariantidx's name key can't see) resolve. Task #19. + // `is []T` / `as []T` — slice-shape lookup routes through the + // element-aware helper, which carries the loose first-slice-shape + // fallback (cstage type_assignable stand-in) that flatvariantidx's + // strict typeeq below doesn't. Task #19; #66 refresh. if (vt.kind == nkind.N_TSLICE) { return flatslicevariantidx(c, tagged, vt.lhs); }; - let want: str; - want.ptr = nil; want.len = 0; - if (vt.kind == nkind.N_TNAME) { want = vt.str; }; - if (want.len == 0) { return -1; }; - return flatvariantidx(c, tagged, want); + // #66 Phase-N step 3: match by typeeq on vt's stamped tinfo + // (flatvariantidx), not vt's surface name. + return flatvariantidx(c, tagged, vt); }; // cgtryprop — `e?` propagates the error variant up the stack. @@ -15119,7 +15118,7 @@ fn cgmatch(c: *cgen, n: *node) void = { if (scrutt.kind == nkind.N_TTAGGED) { let r: i32 = -1; if (pat.kind == nkind.N_TNAME) { - r = flatvariantidx(c, scrutt, pat.str); + r = flatvariantidx(c, scrutt, pat); } else { if (pat.kind == nkind.N_TSLICE) { // `case let s: []T =>` — pat.str is empty // because the variant is a composite, so @@ -20495,7 +20494,28 @@ fn cglet(c: *cgen, n: *node) void = { emitline("\tJNE\t"); emitline(okl); emitline("\n"); - let nidx: i32 = flatvariantidx(c, c.fnret, "nomem"); + // #66 Phase-N step 3: nomem propagation has no + // pattern node, so it can't ride the typeeq + // flatvariantidx path. cstage passes the ty_nomem + // singleton to cg_tag_for_variant; the wwstage cgen + // holds no tinfo singleton, so find the nomem + // variant by its NAMED name over tinfo.params. + let nidx: i32 = -1; + let nti: *tinfo = nil; + if (c.fnret != nil) { nti = c.fnret.type_: *tinfo; }; + for (nti != nil && nti.kind == tykind.TY_NAMED) { nti = nti.under; }; + if (nti != nil) { if (nti.kind == tykind.TY_TAGGED) { + let np: *tparam = nti.params; + let nidx2: i32 = 0; + for (np != nil) { + let nvt: *tinfo = np.type_; + if (nvt != nil) { + if (variantnamematch(nvt.name, "nomem")) { nidx = nidx2; break; }; + }; + np = np.tnext; + nidx2 += 1; + }; + }; }; if (nidx < 0) { nidx = 1; }; emitline("\tMOVQ\t$"); emitint(nidx: i64); diff --git a/test/wcc/931_variant_typekey_run.c b/test/wcc/931_variant_typekey_run.c new file mode 100644 index 00000000..d490d8ae --- /dev/null +++ b/test/wcc/931_variant_typekey_run.c @@ -0,0 +1,198 @@ +/* + * 931_variant_typekey_run — Class B semantic sentinel for #66 + * (Phase-N step 3, the user-ruled type-keying flip). + * + * Locks in the behavioral capability the corpus does NOT exercise: + * distinguishing two co-variants that share a SHAPE (and, pre-flip, + * a name key) by NOMINAL TYPE identity. cstage's cg_variant_match + * (cmd/w6c/cgen.c:451) is what keeps `(str | linerr)` separable even + * though `type linerr = !str` unwraps to str; #66 brings wwstage's + * flatvariantidx to the same typeeq discipline (per-decl TY_NAMED ptr). + * + * Discriminating shape: a tagged union with the alias variant FIRST, + * widened from a value with no surface type name (a call return). + * Pre-#66 wwstage routed the value→tag direction through rhstargetname + * (surface name) + a str/scalar shape fallback that picked the FIRST + * shape-matching variant — i.e. the leading alias — so a plain `str` + * from a call was tagged as `linerr` (idx 0) instead of `str` (idx 1). + * A subsequent match then fired the wrong arm. cstage always type-keyed, + * so pre-#66 this was a cstage-vs-wwstage divergence; each row exits 10 + * on pre-#66 wwstage and 0 on cstage / post-#66 wwstage. The 990-997 + * byte-id gates do not cover this because the corpus has no + * same-shape co-variant whose name key and type key disagree. + * + * Pure runtime test: build through both drivers (cstage `ww`, wwstage + * `ww_ww`) and assert each arm fires for its matching input. + */ +#include +#include +#include +#include +#include + +static int +runwait(const char *cmd) +{ + int rc = system(cmd); + if (rc == -1) return -1; + if (WIFEXITED(rc)) return WEXITSTATUS(rc); + return -1; +} + +struct row { + const char *label; + const char *src; /* single-module program — written as p.ww */ + int want; +}; + +static const struct row rows[] = { + /* str axis. `(linerr | str)` with linerr = !str, alias first. + * classify(0) widens a str returned by a call (no surface name) + * — pre-#66 the shape fallback tags it as the leading str-shape + * variant linerr (idx 0) and the match returns 1 not 2. + * classify(1) widens a linerr (round-trip soundness). */ + { "linerr_str_alias_first_callret", + "package main;\n" + "type linerr = !str;\n" + "fn getstr() str = { return \"hi\"; };\n" + "fn produce(k: i32) (linerr | str) = {\n" + " if (k == 0) { return getstr(); };\n" + " let e: linerr = \"boom\";\n" + " return e;\n" + "};\n" + "export fn classify(k: i32) i32 = {\n" + " match (produce(k)) {\n" + " case let er: linerr => return 1;\n" + " case let v: str => return 2;\n" + " };\n" + "};\n" + "export fn main() i32 = {\n" + " if (classify(0) != 2) { return 10; };\n" + " if (classify(1) != 1) { return 11; };\n" + " return 0;\n" + "};\n", + 0 }, + /* scalar axis. `(ec | i32)` with ec = !i32, alias first. + * classify(0) widens an i32 returned by a call — pre-#66 the + * scalar shape fallback tags it as the leading scalar-shape + * variant ec (idx 0). Confirms the flip isn't str-specific. */ + { "errint_int_alias_first_callret", + "package main;\n" + "type ec = !i32;\n" + "fn getint() i32 = { return 7; };\n" + "fn produce(k: i32) (ec | i32) = {\n" + " if (k == 0) { return getint(); };\n" + " let e: ec = 9;\n" + " return e;\n" + "};\n" + "export fn classify(k: i32) i32 = {\n" + " match (produce(k)) {\n" + " case let er: ec => return 1;\n" + " case let v: i32 => return 2;\n" + " };\n" + "};\n" + "export fn main() i32 = {\n" + " if (classify(0) != 2) { return 10; };\n" + " if (classify(1) != 1) { return 11; };\n" + " return 0;\n" + "};\n", + 0 }, +}; + +static int +write_file(const char *dir, const char *name, const char *body) +{ + char path[512]; + snprintf(path, sizeof path, "%s/%s", dir, name); + FILE *f = fopen(path, "wb"); + if (!f) return -1; + fputs(body, f); + fclose(f); + return 0; +} + +static int +run_driver(const char *driver, const struct row *r, int i) +{ + char tmpdir[96], cmd[2048]; + snprintf(tmpdir, sizeof tmpdir, "/tmp/vtk_%d_%d", getpid(), i); + mkdir(tmpdir, 0755); + + if (write_file(tmpdir, "p.ww", r->src) != 0) return -1; + + snprintf(cmd, sizeof cmd, + "cd %s && %s build p.ww 2>/dev/null", tmpdir, driver); + if (runwait(cmd) != 0) { + fprintf(stderr, "row[%s]: build via %s failed\n", + r->label, driver); + snprintf(cmd, sizeof cmd, "rm -rf %s", tmpdir); + runwait(cmd); + return -1; + } + + char outbin[160]; + snprintf(outbin, sizeof outbin, "%s/p", tmpdir); + int got = runwait(outbin); + + snprintf(cmd, sizeof cmd, "rm -rf %s", tmpdir); + runwait(cmd); + return got; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[512]; + if (bin[0] != '/') { + char cwd[256]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + + char cdrv[640]; + snprintf(cdrv, sizeof cdrv, "%s/ww", bin); + char wdrv[640]; + snprintf(wdrv, sizeof wdrv, "%s/ww_ww", bin); + + struct { const char *name; const char *path; int gated_on_existence; } + drivers[] = { + { "cstage", cdrv, 0 }, + { "wwstage", wdrv, 1 }, + { NULL, NULL, 0 }, + }; + + int n = (int)(sizeof rows / sizeof rows[0]); + int total = 0, fail = 0; + for (int d = 0; drivers[d].name; d++) { + if (drivers[d].gated_on_existence + && access(drivers[d].path, X_OK) != 0) { + fprintf(stderr, + "variant_typekey_run: skip %s (no %s)\n", + drivers[d].name, drivers[d].path); + continue; + } + for (int i = 0; i < n; i++) { + int got = run_driver(drivers[d].path, &rows[i], i); + total++; + if (got != rows[i].want) { + fprintf(stderr, + "variant_typekey_run[%s][%s]: exit=%d want=%d\n", + drivers[d].name, rows[i].label, + got, rows[i].want); + fail++; + } + } + } + + if (fail) { + fprintf(stderr, + "variant_typekey_run: %d/%d fixtures failed\n", + fail, total); + return 1; + } + printf("variant_typekey_run: %d/%d ok\n", total, total); + return 0; +}