From 758d3ec8c39b3437e6c417122ccda5c78bfcfd8c Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Tue, 9 Jun 2026 21:27:40 +0900 Subject: [PATCH] wcc/check: #14 nominal typeeqast (aliassym) closes A6 cross-module variant identity (wwstage) typeeqast's N_TNAME arm compared variant types by SURFACE string (streq(aa.str, bb.str)), so a cross-module type referenced bare (oserror) vs qualified (os.oserror) mis-compared unequal -> A6: concrete->tagged silently mis-identified the variant. Resolve each name to its canonical type sym before comparing: keep the streq fast-path, else aliassym(c,aa)==aliassym(c,bb) (ww's existing resolver maps bare [#53] and qualified [#51] to the same SK_TYPE sym). AST analog of cstage type.c:278 TY_NAMED a==b / harec types.c:579 ident_equal -- NO interning. `c` threaded into 21 typeeqast sites. B-full Layer 1: closes A6/#14; Layer 2 (A7 tagged->tagged subset reject) stays deferred to the #199b flatten arc. Byte-id-neutral; graduates shlex #59.15 (989 M_WWREJECT->M_ID). test/wcc/839 pins both-stage symmetry (discriminating teeth = 989 #59.15, per 839 doc). --- Makefile | 7 + selfhost/cmd/w6c/main.combined.ww | 60 +++--- selfhost/cmd/wcc/check.ww | 60 +++--- selfhost/cmd/wwdump/main.combined.ww | 60 +++--- test/wcc/839_xmod_nominal_typeeqast.c | 279 ++++++++++++++++++++++++++ test/wcc/989_lib_byteid.c | 5 +- 6 files changed, 395 insertions(+), 76 deletions(-) create mode 100644 test/wcc/839_xmod_nominal_typeeqast.c diff --git a/Makefile b/Makefile index 4d854a1e..65191627 100644 --- a/Makefile +++ b/Makefile @@ -263,6 +263,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \ $(BIN)/test_tagged_assignable_cluster \ $(BIN)/test_idcast_tagged_return \ $(BIN)/test_global_tagged_castinit \ + $(BIN)/test_xmod_nominal_typeeqast \ $(BIN)/test_inferred_array_global \ $(BIN)/test_slice_str_global_arg \ $(BIN)/test_slice_str_global_zero \ @@ -758,6 +759,12 @@ $(BIN)/test_global_tagged_castinit: test/wcc/838_global_tagged_castinit.c $(BIN) $(LIB)/libwwrt.a | $(BIN) $(CC) $(CFLAGS) -o $@ $< +$(BIN)/test_xmod_nominal_typeeqast: test/wcc/839_xmod_nominal_typeeqast.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_inferred_array_global: test/wcc/833_inferred_array_global.c $(BIN)/ww \ $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \ $(BIN)/ww_ww $(BIN)/w6c_ww $(BIN)/w6a_ww $(BIN)/w6l_ww \ diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index 3ef2c9c8..27325f33 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -11121,23 +11121,33 @@ fn resolvealias(c: *checker, n: *node) *node = { // typeeqast — structural equality on AST type expressions, mod // the `!` wrapper. Mirrors variant_match in cgen + check.c: NAMED -// types compare by string (the closest stand-in for pointer -// identity at the AST level); other nodes recurse by kind. -fn typeeqast(a: *node, b: *node) bool = { +// types compare by string fast-path, else by resolved-decl identity +// (the AST analog of cstage type.c:278 `TY_NAMED: a == b` and harec +// types.c:579 `STORAGE_ALIAS: ident_equal`). #14 B-full Layer 1: +// bare `oserror` vs qualified `os.oserror` resolve to the SAME +// SK_TYPE sym (aliassym maps both via #51/#53), so a cross-module +// nominal forward compares equal where the surface streq said false. +fn typeeqast(c: *checker, a: *node, b: *node) bool = { let aa: *node = unwrapbang(a); let bb: *node = unwrapbang(b); if (aa == nil) { return bb == nil; }; if (bb == nil) { return false; }; if (aa.kind != bb.kind) { return false; }; let k: nkind = aa.kind; - if (k == nkind.N_TNAME) { return streq(aa.str, bb.str); }; - if (k == nkind.N_TPTR) { return typeeqast(aa.lhs, bb.lhs); }; - if (k == nkind.N_TSLICE){ return typeeqast(aa.lhs, bb.lhs); }; - if (k == nkind.N_TCHAN) { return typeeqast(aa.lhs, bb.lhs); }; + if (k == nkind.N_TNAME) { + if (streq(aa.str, bb.str)) { return true; }; + let sa: *sym = aliassym(c, aa); + let sb: *sym = aliassym(c, bb); + if (sa != nil && sa == sb) { return true; }; + return false; + }; + if (k == nkind.N_TPTR) { return typeeqast(c, aa.lhs, bb.lhs); }; + if (k == nkind.N_TSLICE){ return typeeqast(c, aa.lhs, bb.lhs); }; + if (k == nkind.N_TCHAN) { return typeeqast(c, aa.lhs, bb.lhs); }; if (k == nkind.N_TFN) { // Divergence: cstage type.c:239 compares resolved Type; we // compare AST. See #178. - if (!typeeqast(aa.lhs, bb.lhs)) { return false; }; + if (!typeeqast(c, aa.lhs, bb.lhs)) { return false; }; let pa: *node = aa.list; let pb: *node = bb.list; for (pa != nil) { @@ -11149,7 +11159,7 @@ fn typeeqast(a: *node, b: *node) bool = { let va: bool = pa.op == tkind.TK_ELLIPSIS; let vb: bool = pb.op == tkind.TK_ELLIPSIS; if (va != vb) { return false; }; - if (!typeeqast(pa.lhs, pb.lhs)) { return false; }; + if (!typeeqast(c, pa.lhs, pb.lhs)) { return false; }; }; pa = pa.next; pb = pb.next; @@ -11168,7 +11178,7 @@ fn typeeqast(a: *node, b: *node) bool = { let pb: *node = bb.list; for (pa != nil) { if (pb == nil) { return false; }; - if (!typeeqast(pa.lhs, pb.lhs)) { return false; }; + if (!typeeqast(c, pa.lhs, pb.lhs)) { return false; }; pa = pa.next; pb = pb.next; }; @@ -11199,7 +11209,7 @@ fn typeeqast(a: *node, b: *node) bool = { // lands; b1c's (void|size) has none, so byte-id is untouched. if (pa.op == tkind.TK_ELLIPSIS) { return false; }; if (pb.op == tkind.TK_ELLIPSIS) { return false; }; - if (!typeeqast(pa, pb)) { return false; }; + if (!typeeqast(c, pa, pb)) { return false; }; pa = pa.next; pb = pb.next; }; @@ -12492,7 +12502,7 @@ fn unifyarith(c: *checker, ltn: *node, rtn: *node) *node = { if (ru) { if (isassignable(c, ltn, rtn, &conf)) { return ltn; }; }; - if (typeeqast(ltn, rtn)) { return ltn; }; + if (typeeqast(c, ltn, rtn)) { return ltn; }; // Mismatched typed pair — return ltn so the binop stamps something; // 5-lite-b: the trailing nil-on-mismatch shape was eliminated when // the helper was split out of binoptype. @@ -12568,7 +12578,7 @@ fn binoptype(c: *checker, e: *node) *node = { // broad differing-types flip (typeeqast vs cstage type_eq // asymmetry risk) stays out. if (varianterr(c, ltn) || varianterr(c, rtn)) { - if (!typeeqast(ltn, rtn)) { + if (!typeeqast(c, ltn, rtn)) { deffolderr(c, e, "operands have differing types"); }; }; @@ -13994,7 +14004,7 @@ fn addrfnptrmatches(c: *checker, ptr: *node, synth: *node) bool = { let ref: *node = resolvealias(c, unwrapbang(pu.lhs)); if (ref == nil) { return false; }; if (ref.kind != nkind.N_TFN) { return false; }; - return typeeqast(synth, ref); + return typeeqast(c, synth, ref); }; // assignableaddrfn — project #206 Option C gate. Mirror of cstage @@ -14060,7 +14070,7 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = { let su: *node = resolvealias(c, unwrapbang(src)); if (du == nil) { *confident = false; return true; }; if (su == nil) { *confident = false; return true; }; - if (typeeqast(du, su)) { return true; }; + if (typeeqast(c, du, su)) { return true; }; // #258: implicit [N]T -> []T array-to-slice borrow. Hare admits an // array with a defined length wherever its element slice is expected // (ref/harec/src/types.c:1080-1097, the SLICE-dst arm). Element types @@ -14070,7 +14080,7 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = { // explicit full slice via desugararrayslice; cgen is untouched. if (du.kind == nkind.N_TSLICE) { if (su.kind == nkind.N_TARRAY) { - if (typeeqast(du.lhs, su.lhs)) { return true; }; + if (typeeqast(c, du.lhs, su.lhs)) { return true; }; return false; }; }; @@ -14201,7 +14211,7 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = { let vtagged: bool = false; if (vu != nil) { if (vu.kind == nkind.N_TTAGGED) { vtagged = true; }; }; if (vtagged && !vspread) { - if (typeeqast(v, src)) { return true; }; + if (typeeqast(c, v, src)) { return true; }; } else { let innerconf: bool = false; if (isassignable(c, v, src, &innerconf)) { return true; }; @@ -14228,7 +14238,7 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = { let vu: *node = resolvealias(c, unwrapbang(v)); if (vu != nil) { if (vu.kind == nkind.N_TTAGGED) { - if (typeeqast(v, src)) { return true; }; + if (typeeqast(c, v, src)) { return true; }; }; }; v = v.next; @@ -14375,12 +14385,12 @@ fn taggeddefmod(c: *checker, st: *node) str = { fn casecovers(c: *checker, cs: *node, want: *node, unionmod: str) bool = { if (cs.lhs != nil) { - if (typeeqast(cs.lhs, want)) { return true; }; + if (typeeqast(c, cs.lhs, want)) { return true; }; if (casevariantpairmatch(want, cs.lhs, unionmod)) { return true; }; }; let alt: *node = cs.list; for (alt != nil) { - if (typeeqast(alt, want)) { return true; }; + if (typeeqast(c, alt, want)) { return true; }; if (casevariantpairmatch(want, alt, unionmod)) { return true; }; alt = alt.next; }; @@ -14427,7 +14437,7 @@ fn casevariantin(c: *checker, tagged: *node, pat: *node, unionmod: str) bool = { }; }; }; - if (typeeqast(v, pat)) { return true; }; + if (typeeqast(c, v, pat)) { return true; }; if (casevariantpairmatch(v, pat, unionmod)) { return true; }; v = v.next; }; @@ -14602,7 +14612,7 @@ fn taggedarrayvariantctor(c: *checker, dst: *node, src: *node) bool = { let vtagged: bool = false; if (vu != nil) { if (vu.kind == nkind.N_TTAGGED) { vtagged = true; }; }; if (vtagged && !vspread) { - if (typeeqast(v, src)) { return false; }; + if (typeeqast(c, v, src)) { return false; }; } else { let innerconf: bool = false; if (isassignable(c, v, src, &innerconf)) { @@ -14822,7 +14832,7 @@ fn desugararrayslice(c: *checker, dsttn: *node, srctn: *node, val: *node) *node if (su == nil) { return val; }; if (du.kind != nkind.N_TSLICE) { return val; }; if (su.kind != nkind.N_TARRAY) { return val; }; - if (!typeeqast(du.lhs, su.lhs)) { return val; }; + if (!typeeqast(c, du.lhs, su.lhs)) { return val; }; let sl: *node = newnode(nkind.N_SLICE, val.file, val.line, val.col); sl.lhs = val; // sliced base; lo (.rhs) / hi (.cond) nil → 0 : len(arr) let slt: *node = newnode(nkind.N_TSLICE, "", 0, 0); @@ -14898,7 +14908,7 @@ fn desugarcallargs(c: *checker, n: *node) void = { let au: *node = resolvealias(c, unwrapbang(atype)); if (pu != nil) { if (au != nil) { if (pu.kind == nkind.N_TSLICE) { if (au.kind == nkind.N_TARRAY) { - if (!typeeqast(pu.lhs, au.lhs)) { + if (!typeeqast(c, pu.lhs, au.lhs)) { errnotassign(c, param.lhs, atype, "argument"); }; }; }; @@ -15433,7 +15443,7 @@ fn checktryprop(c: *checker, n: *node) void = { let found: bool = false; let rv: *node = r.list; for (rv != nil) { - if (typeeqast(rv, ev)) { + if (typeeqast(c, rv, ev)) { found = true; rv = nil; } else { rv = rv.next; }; diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index 50dc6dd3..b2d88895 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -840,23 +840,33 @@ fn resolvealias(c: *checker, n: *node) *node = { // typeeqast — structural equality on AST type expressions, mod // the `!` wrapper. Mirrors variant_match in cgen + check.c: NAMED -// types compare by string (the closest stand-in for pointer -// identity at the AST level); other nodes recurse by kind. -fn typeeqast(a: *node, b: *node) bool = { +// types compare by string fast-path, else by resolved-decl identity +// (the AST analog of cstage type.c:278 `TY_NAMED: a == b` and harec +// types.c:579 `STORAGE_ALIAS: ident_equal`). #14 B-full Layer 1: +// bare `oserror` vs qualified `os.oserror` resolve to the SAME +// SK_TYPE sym (aliassym maps both via #51/#53), so a cross-module +// nominal forward compares equal where the surface streq said false. +fn typeeqast(c: *checker, a: *node, b: *node) bool = { let aa: *node = unwrapbang(a); let bb: *node = unwrapbang(b); if (aa == nil) { return bb == nil; }; if (bb == nil) { return false; }; if (aa.kind != bb.kind) { return false; }; let k: nkind = aa.kind; - if (k == nkind.N_TNAME) { return streq(aa.str, bb.str); }; - if (k == nkind.N_TPTR) { return typeeqast(aa.lhs, bb.lhs); }; - if (k == nkind.N_TSLICE){ return typeeqast(aa.lhs, bb.lhs); }; - if (k == nkind.N_TCHAN) { return typeeqast(aa.lhs, bb.lhs); }; + if (k == nkind.N_TNAME) { + if (streq(aa.str, bb.str)) { return true; }; + let sa: *sym = aliassym(c, aa); + let sb: *sym = aliassym(c, bb); + if (sa != nil && sa == sb) { return true; }; + return false; + }; + if (k == nkind.N_TPTR) { return typeeqast(c, aa.lhs, bb.lhs); }; + if (k == nkind.N_TSLICE){ return typeeqast(c, aa.lhs, bb.lhs); }; + if (k == nkind.N_TCHAN) { return typeeqast(c, aa.lhs, bb.lhs); }; if (k == nkind.N_TFN) { // Divergence: cstage type.c:239 compares resolved Type; we // compare AST. See #178. - if (!typeeqast(aa.lhs, bb.lhs)) { return false; }; + if (!typeeqast(c, aa.lhs, bb.lhs)) { return false; }; let pa: *node = aa.list; let pb: *node = bb.list; for (pa != nil) { @@ -868,7 +878,7 @@ fn typeeqast(a: *node, b: *node) bool = { let va: bool = pa.op == tkind.TK_ELLIPSIS; let vb: bool = pb.op == tkind.TK_ELLIPSIS; if (va != vb) { return false; }; - if (!typeeqast(pa.lhs, pb.lhs)) { return false; }; + if (!typeeqast(c, pa.lhs, pb.lhs)) { return false; }; }; pa = pa.next; pb = pb.next; @@ -887,7 +897,7 @@ fn typeeqast(a: *node, b: *node) bool = { let pb: *node = bb.list; for (pa != nil) { if (pb == nil) { return false; }; - if (!typeeqast(pa.lhs, pb.lhs)) { return false; }; + if (!typeeqast(c, pa.lhs, pb.lhs)) { return false; }; pa = pa.next; pb = pb.next; }; @@ -918,7 +928,7 @@ fn typeeqast(a: *node, b: *node) bool = { // lands; b1c's (void|size) has none, so byte-id is untouched. if (pa.op == tkind.TK_ELLIPSIS) { return false; }; if (pb.op == tkind.TK_ELLIPSIS) { return false; }; - if (!typeeqast(pa, pb)) { return false; }; + if (!typeeqast(c, pa, pb)) { return false; }; pa = pa.next; pb = pb.next; }; @@ -2211,7 +2221,7 @@ fn unifyarith(c: *checker, ltn: *node, rtn: *node) *node = { if (ru) { if (isassignable(c, ltn, rtn, &conf)) { return ltn; }; }; - if (typeeqast(ltn, rtn)) { return ltn; }; + if (typeeqast(c, ltn, rtn)) { return ltn; }; // Mismatched typed pair — return ltn so the binop stamps something; // 5-lite-b: the trailing nil-on-mismatch shape was eliminated when // the helper was split out of binoptype. @@ -2287,7 +2297,7 @@ fn binoptype(c: *checker, e: *node) *node = { // broad differing-types flip (typeeqast vs cstage type_eq // asymmetry risk) stays out. if (varianterr(c, ltn) || varianterr(c, rtn)) { - if (!typeeqast(ltn, rtn)) { + if (!typeeqast(c, ltn, rtn)) { deffolderr(c, e, "operands have differing types"); }; }; @@ -3713,7 +3723,7 @@ fn addrfnptrmatches(c: *checker, ptr: *node, synth: *node) bool = { let ref: *node = resolvealias(c, unwrapbang(pu.lhs)); if (ref == nil) { return false; }; if (ref.kind != nkind.N_TFN) { return false; }; - return typeeqast(synth, ref); + return typeeqast(c, synth, ref); }; // assignableaddrfn — project #206 Option C gate. Mirror of cstage @@ -3779,7 +3789,7 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = { let su: *node = resolvealias(c, unwrapbang(src)); if (du == nil) { *confident = false; return true; }; if (su == nil) { *confident = false; return true; }; - if (typeeqast(du, su)) { return true; }; + if (typeeqast(c, du, su)) { return true; }; // #258: implicit [N]T -> []T array-to-slice borrow. Hare admits an // array with a defined length wherever its element slice is expected // (ref/harec/src/types.c:1080-1097, the SLICE-dst arm). Element types @@ -3789,7 +3799,7 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = { // explicit full slice via desugararrayslice; cgen is untouched. if (du.kind == nkind.N_TSLICE) { if (su.kind == nkind.N_TARRAY) { - if (typeeqast(du.lhs, su.lhs)) { return true; }; + if (typeeqast(c, du.lhs, su.lhs)) { return true; }; return false; }; }; @@ -3920,7 +3930,7 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = { let vtagged: bool = false; if (vu != nil) { if (vu.kind == nkind.N_TTAGGED) { vtagged = true; }; }; if (vtagged && !vspread) { - if (typeeqast(v, src)) { return true; }; + if (typeeqast(c, v, src)) { return true; }; } else { let innerconf: bool = false; if (isassignable(c, v, src, &innerconf)) { return true; }; @@ -3947,7 +3957,7 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = { let vu: *node = resolvealias(c, unwrapbang(v)); if (vu != nil) { if (vu.kind == nkind.N_TTAGGED) { - if (typeeqast(v, src)) { return true; }; + if (typeeqast(c, v, src)) { return true; }; }; }; v = v.next; @@ -4094,12 +4104,12 @@ fn taggeddefmod(c: *checker, st: *node) str = { fn casecovers(c: *checker, cs: *node, want: *node, unionmod: str) bool = { if (cs.lhs != nil) { - if (typeeqast(cs.lhs, want)) { return true; }; + if (typeeqast(c, cs.lhs, want)) { return true; }; if (casevariantpairmatch(want, cs.lhs, unionmod)) { return true; }; }; let alt: *node = cs.list; for (alt != nil) { - if (typeeqast(alt, want)) { return true; }; + if (typeeqast(c, alt, want)) { return true; }; if (casevariantpairmatch(want, alt, unionmod)) { return true; }; alt = alt.next; }; @@ -4146,7 +4156,7 @@ fn casevariantin(c: *checker, tagged: *node, pat: *node, unionmod: str) bool = { }; }; }; - if (typeeqast(v, pat)) { return true; }; + if (typeeqast(c, v, pat)) { return true; }; if (casevariantpairmatch(v, pat, unionmod)) { return true; }; v = v.next; }; @@ -4321,7 +4331,7 @@ fn taggedarrayvariantctor(c: *checker, dst: *node, src: *node) bool = { let vtagged: bool = false; if (vu != nil) { if (vu.kind == nkind.N_TTAGGED) { vtagged = true; }; }; if (vtagged && !vspread) { - if (typeeqast(v, src)) { return false; }; + if (typeeqast(c, v, src)) { return false; }; } else { let innerconf: bool = false; if (isassignable(c, v, src, &innerconf)) { @@ -4541,7 +4551,7 @@ fn desugararrayslice(c: *checker, dsttn: *node, srctn: *node, val: *node) *node if (su == nil) { return val; }; if (du.kind != nkind.N_TSLICE) { return val; }; if (su.kind != nkind.N_TARRAY) { return val; }; - if (!typeeqast(du.lhs, su.lhs)) { return val; }; + if (!typeeqast(c, du.lhs, su.lhs)) { return val; }; let sl: *node = newnode(nkind.N_SLICE, val.file, val.line, val.col); sl.lhs = val; // sliced base; lo (.rhs) / hi (.cond) nil → 0 : len(arr) let slt: *node = newnode(nkind.N_TSLICE, "", 0, 0); @@ -4617,7 +4627,7 @@ fn desugarcallargs(c: *checker, n: *node) void = { let au: *node = resolvealias(c, unwrapbang(atype)); if (pu != nil) { if (au != nil) { if (pu.kind == nkind.N_TSLICE) { if (au.kind == nkind.N_TARRAY) { - if (!typeeqast(pu.lhs, au.lhs)) { + if (!typeeqast(c, pu.lhs, au.lhs)) { errnotassign(c, param.lhs, atype, "argument"); }; }; }; @@ -5152,7 +5162,7 @@ fn checktryprop(c: *checker, n: *node) void = { let found: bool = false; let rv: *node = r.list; for (rv != nil) { - if (typeeqast(rv, ev)) { + if (typeeqast(c, rv, ev)) { found = true; rv = nil; } else { rv = rv.next; }; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 546f9c8c..5f12d2ab 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -11121,23 +11121,33 @@ fn resolvealias(c: *checker, n: *node) *node = { // typeeqast — structural equality on AST type expressions, mod // the `!` wrapper. Mirrors variant_match in cgen + check.c: NAMED -// types compare by string (the closest stand-in for pointer -// identity at the AST level); other nodes recurse by kind. -fn typeeqast(a: *node, b: *node) bool = { +// types compare by string fast-path, else by resolved-decl identity +// (the AST analog of cstage type.c:278 `TY_NAMED: a == b` and harec +// types.c:579 `STORAGE_ALIAS: ident_equal`). #14 B-full Layer 1: +// bare `oserror` vs qualified `os.oserror` resolve to the SAME +// SK_TYPE sym (aliassym maps both via #51/#53), so a cross-module +// nominal forward compares equal where the surface streq said false. +fn typeeqast(c: *checker, a: *node, b: *node) bool = { let aa: *node = unwrapbang(a); let bb: *node = unwrapbang(b); if (aa == nil) { return bb == nil; }; if (bb == nil) { return false; }; if (aa.kind != bb.kind) { return false; }; let k: nkind = aa.kind; - if (k == nkind.N_TNAME) { return streq(aa.str, bb.str); }; - if (k == nkind.N_TPTR) { return typeeqast(aa.lhs, bb.lhs); }; - if (k == nkind.N_TSLICE){ return typeeqast(aa.lhs, bb.lhs); }; - if (k == nkind.N_TCHAN) { return typeeqast(aa.lhs, bb.lhs); }; + if (k == nkind.N_TNAME) { + if (streq(aa.str, bb.str)) { return true; }; + let sa: *sym = aliassym(c, aa); + let sb: *sym = aliassym(c, bb); + if (sa != nil && sa == sb) { return true; }; + return false; + }; + if (k == nkind.N_TPTR) { return typeeqast(c, aa.lhs, bb.lhs); }; + if (k == nkind.N_TSLICE){ return typeeqast(c, aa.lhs, bb.lhs); }; + if (k == nkind.N_TCHAN) { return typeeqast(c, aa.lhs, bb.lhs); }; if (k == nkind.N_TFN) { // Divergence: cstage type.c:239 compares resolved Type; we // compare AST. See #178. - if (!typeeqast(aa.lhs, bb.lhs)) { return false; }; + if (!typeeqast(c, aa.lhs, bb.lhs)) { return false; }; let pa: *node = aa.list; let pb: *node = bb.list; for (pa != nil) { @@ -11149,7 +11159,7 @@ fn typeeqast(a: *node, b: *node) bool = { let va: bool = pa.op == tkind.TK_ELLIPSIS; let vb: bool = pb.op == tkind.TK_ELLIPSIS; if (va != vb) { return false; }; - if (!typeeqast(pa.lhs, pb.lhs)) { return false; }; + if (!typeeqast(c, pa.lhs, pb.lhs)) { return false; }; }; pa = pa.next; pb = pb.next; @@ -11168,7 +11178,7 @@ fn typeeqast(a: *node, b: *node) bool = { let pb: *node = bb.list; for (pa != nil) { if (pb == nil) { return false; }; - if (!typeeqast(pa.lhs, pb.lhs)) { return false; }; + if (!typeeqast(c, pa.lhs, pb.lhs)) { return false; }; pa = pa.next; pb = pb.next; }; @@ -11199,7 +11209,7 @@ fn typeeqast(a: *node, b: *node) bool = { // lands; b1c's (void|size) has none, so byte-id is untouched. if (pa.op == tkind.TK_ELLIPSIS) { return false; }; if (pb.op == tkind.TK_ELLIPSIS) { return false; }; - if (!typeeqast(pa, pb)) { return false; }; + if (!typeeqast(c, pa, pb)) { return false; }; pa = pa.next; pb = pb.next; }; @@ -12492,7 +12502,7 @@ fn unifyarith(c: *checker, ltn: *node, rtn: *node) *node = { if (ru) { if (isassignable(c, ltn, rtn, &conf)) { return ltn; }; }; - if (typeeqast(ltn, rtn)) { return ltn; }; + if (typeeqast(c, ltn, rtn)) { return ltn; }; // Mismatched typed pair — return ltn so the binop stamps something; // 5-lite-b: the trailing nil-on-mismatch shape was eliminated when // the helper was split out of binoptype. @@ -12568,7 +12578,7 @@ fn binoptype(c: *checker, e: *node) *node = { // broad differing-types flip (typeeqast vs cstage type_eq // asymmetry risk) stays out. if (varianterr(c, ltn) || varianterr(c, rtn)) { - if (!typeeqast(ltn, rtn)) { + if (!typeeqast(c, ltn, rtn)) { deffolderr(c, e, "operands have differing types"); }; }; @@ -13994,7 +14004,7 @@ fn addrfnptrmatches(c: *checker, ptr: *node, synth: *node) bool = { let ref: *node = resolvealias(c, unwrapbang(pu.lhs)); if (ref == nil) { return false; }; if (ref.kind != nkind.N_TFN) { return false; }; - return typeeqast(synth, ref); + return typeeqast(c, synth, ref); }; // assignableaddrfn — project #206 Option C gate. Mirror of cstage @@ -14060,7 +14070,7 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = { let su: *node = resolvealias(c, unwrapbang(src)); if (du == nil) { *confident = false; return true; }; if (su == nil) { *confident = false; return true; }; - if (typeeqast(du, su)) { return true; }; + if (typeeqast(c, du, su)) { return true; }; // #258: implicit [N]T -> []T array-to-slice borrow. Hare admits an // array with a defined length wherever its element slice is expected // (ref/harec/src/types.c:1080-1097, the SLICE-dst arm). Element types @@ -14070,7 +14080,7 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = { // explicit full slice via desugararrayslice; cgen is untouched. if (du.kind == nkind.N_TSLICE) { if (su.kind == nkind.N_TARRAY) { - if (typeeqast(du.lhs, su.lhs)) { return true; }; + if (typeeqast(c, du.lhs, su.lhs)) { return true; }; return false; }; }; @@ -14201,7 +14211,7 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = { let vtagged: bool = false; if (vu != nil) { if (vu.kind == nkind.N_TTAGGED) { vtagged = true; }; }; if (vtagged && !vspread) { - if (typeeqast(v, src)) { return true; }; + if (typeeqast(c, v, src)) { return true; }; } else { let innerconf: bool = false; if (isassignable(c, v, src, &innerconf)) { return true; }; @@ -14228,7 +14238,7 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = { let vu: *node = resolvealias(c, unwrapbang(v)); if (vu != nil) { if (vu.kind == nkind.N_TTAGGED) { - if (typeeqast(v, src)) { return true; }; + if (typeeqast(c, v, src)) { return true; }; }; }; v = v.next; @@ -14375,12 +14385,12 @@ fn taggeddefmod(c: *checker, st: *node) str = { fn casecovers(c: *checker, cs: *node, want: *node, unionmod: str) bool = { if (cs.lhs != nil) { - if (typeeqast(cs.lhs, want)) { return true; }; + if (typeeqast(c, cs.lhs, want)) { return true; }; if (casevariantpairmatch(want, cs.lhs, unionmod)) { return true; }; }; let alt: *node = cs.list; for (alt != nil) { - if (typeeqast(alt, want)) { return true; }; + if (typeeqast(c, alt, want)) { return true; }; if (casevariantpairmatch(want, alt, unionmod)) { return true; }; alt = alt.next; }; @@ -14427,7 +14437,7 @@ fn casevariantin(c: *checker, tagged: *node, pat: *node, unionmod: str) bool = { }; }; }; - if (typeeqast(v, pat)) { return true; }; + if (typeeqast(c, v, pat)) { return true; }; if (casevariantpairmatch(v, pat, unionmod)) { return true; }; v = v.next; }; @@ -14602,7 +14612,7 @@ fn taggedarrayvariantctor(c: *checker, dst: *node, src: *node) bool = { let vtagged: bool = false; if (vu != nil) { if (vu.kind == nkind.N_TTAGGED) { vtagged = true; }; }; if (vtagged && !vspread) { - if (typeeqast(v, src)) { return false; }; + if (typeeqast(c, v, src)) { return false; }; } else { let innerconf: bool = false; if (isassignable(c, v, src, &innerconf)) { @@ -14822,7 +14832,7 @@ fn desugararrayslice(c: *checker, dsttn: *node, srctn: *node, val: *node) *node if (su == nil) { return val; }; if (du.kind != nkind.N_TSLICE) { return val; }; if (su.kind != nkind.N_TARRAY) { return val; }; - if (!typeeqast(du.lhs, su.lhs)) { return val; }; + if (!typeeqast(c, du.lhs, su.lhs)) { return val; }; let sl: *node = newnode(nkind.N_SLICE, val.file, val.line, val.col); sl.lhs = val; // sliced base; lo (.rhs) / hi (.cond) nil → 0 : len(arr) let slt: *node = newnode(nkind.N_TSLICE, "", 0, 0); @@ -14898,7 +14908,7 @@ fn desugarcallargs(c: *checker, n: *node) void = { let au: *node = resolvealias(c, unwrapbang(atype)); if (pu != nil) { if (au != nil) { if (pu.kind == nkind.N_TSLICE) { if (au.kind == nkind.N_TARRAY) { - if (!typeeqast(pu.lhs, au.lhs)) { + if (!typeeqast(c, pu.lhs, au.lhs)) { errnotassign(c, param.lhs, atype, "argument"); }; }; }; @@ -15433,7 +15443,7 @@ fn checktryprop(c: *checker, n: *node) void = { let found: bool = false; let rv: *node = r.list; for (rv != nil) { - if (typeeqast(rv, ev)) { + if (typeeqast(c, rv, ev)) { found = true; rv = nil; } else { rv = rv.next; }; diff --git a/test/wcc/839_xmod_nominal_typeeqast.c b/test/wcc/839_xmod_nominal_typeeqast.c new file mode 100644 index 00000000..205cca95 --- /dev/null +++ b/test/wcc/839_xmod_nominal_typeeqast.c @@ -0,0 +1,279 @@ +/* + * 839_xmod_nominal_typeeqast — B-full Layer 1 (#14): typeeqast's N_TNAME + * arm now compares by RESOLVED-decl identity (aliassym pointer-equality), + * not surface spelling. A cross-module type referenced bare inside its + * defining module (`myerr`) and qualified by a consumer (`e.myerr`) used + * to mis-compare unequal (streq), so a nominal forward / variant match was + * silently mis-identified. cstage already equates them (type.c:278 + * `TY_NAMED: a == b` on resolved Type); harec equates them + * (types.c:579 `STORAGE_ALIAS: ident_equal`). ww now matches at the AST + * level via the existing #51/#53 resolver — NO type-interning layer. + * + * The headline discriminator for this fix is the lib/shlex graduation + * (989 #59.15: M_WWREJECT -> M_ID). That flip needs shlex's full + * `(size | io.error)` match pipeline and is NOT reproducible standalone, + * so the gate that actually distinguishes pre-fix surface-streq from + * post-fix nominal-aliassym is 989_lib_byteid, not this test. + * + * scenario | shape | gate + * ------------------+---------------------------------------------+------ + * nominal_match | match e.res whose variants are spelled BARE | 42 + * | inside e, with QUALIFIED case patterns | + * | (`case e.myerr`) — nominal cover | + * a6_concrete | concrete `e.myerr` returned into `e.res` | 0 + * | (A6 concrete->tagged, bare-vs-qualified) | + * incompat_concrete | `str` into `(i64 | e.myerr)` — NO str | FAIL + * | variant: confident reject (no over-accept)| + * distinct_alias | distinct alias `other = !i64` into `e.res` | FAIL + * | — rejected at CGEN (#95 structural | + * | ambiguity: two i64-underlying variants), | + * | symmetric both stages | + * + * SCOPE / no-teeth disclosure: all four rows decide IDENTICALLY against + * the pre-fix surface-streq HEAD (nominal_match->42, a6_concrete->0, the + * two NEG rows reject) on BOTH stages — the nominal-identity change does + * not flip any of them, because each is reached through a sibling + * mechanism that already handled bare-vs-qualified: nominal_match via + * casevariantpairmatch (#205 match-cover resolve), a6_concrete via the + * concrete->tagged arm's underlying-i64 resolvealias, and distinct_alias + * via the cgen #95 ambiguity reject (the CHECKER accepts it — `other`'s + * underlying i64 matches a variant; the reject is structural, not + * nominal). So this test does NOT guard a revert of typeeqast to streq; + * that is 989 #59.15's job. What it DOES pin is that the four + * cross-module nominal accept/reject decisions stay correct AND symmetric + * across cstage/wwstage (rule-10) — a forward guard against either stage + * drifting on cross-module nominal forwards. + * + * Both stages must agree (rule-10): the POS rows build+run to the same + * exit on cstage and wwstage; the NEG rows build-FAIL on both. Byte-id of + * the emitted asm is NOT asserted here — receiving a cross-module tagged + * return and re-passing it crosses a pre-existing, unrelated cgen frame- + * layout divergence; B-full Layer 1's byte-id-neutrality is gated by + * 989_lib_byteid + 990-997 (the bootstrap forwards). + */ +#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 file { const char *name; const char *src; }; + +struct scenario { + const char *label; + const struct file *files; /* name==NULL terminates */ + int expect_build; /* 1 = build+run to want_exit; 0 = must FAIL */ + int want_exit; +}; + +/* ---- nominal_match: bare union variants, qualified case patterns ---- */ +static const struct file nominal_match_files[] = { + { "e.ww", + "package e;\n" + "\n" + "export type myerr = !i64;\n" + "export type res = (i64 | myerr);\n" + "\n" + "export fn ok(v: i64) res = { return v; };\n" + "export fn bad() res = { return (-7: myerr); };\n" }, + { "main.ww", + "package main;\n" + "\n" + "import e;\n" + "\n" + "fn unwrap(r: e.res) i64 = {\n" + " match (r) {\n" + " case let n: i64 => return n;\n" + " case let x: e.myerr => return -1;\n" + " };\n" + "};\n" + "\n" + "export fn main() i32 = {\n" + " let a = unwrap(e.ok(40));\n" + " let b = unwrap(e.bad());\n" + " if (a != 40) { return 11; };\n" + " if (b != -1) { return 12; };\n" + " return 42;\n" + "};\n" }, + { NULL, NULL } +}; + +/* ---- a6_concrete: concrete e.myerr into e.res return (A6) ---------- */ +static const struct file a6_concrete_files[] = { + { "e.ww", + "package e;\n" + "\n" + "export type myerr = !i64;\n" + "export type res = (i64 | myerr);\n" }, + { "main.ww", + "package main;\n" + "\n" + "import e;\n" + "\n" + "fn wrap(x: e.myerr) e.res = { return x; };\n" + "\n" + "export fn main() i32 = { return 0; };\n" }, + { NULL, NULL } +}; + +/* ---- incompat_concrete: str into (i64|e.myerr) — must reject ------- */ +static const struct file incompat_concrete_files[] = { + { "e.ww", + "package e;\n" + "\n" + "export type myerr = !i64;\n" }, + { "main.ww", + "package main;\n" + "\n" + "import e;\n" + "\n" + "type u = (i64 | e.myerr);\n" + "\n" + "fn bad(s: str) u = { return s; };\n" + "\n" + "export fn main() i32 = { return 0; };\n" }, + { NULL, NULL } +}; + +/* ---- distinct_alias: distinct alias over same underlying — reject -- */ +static const struct file distinct_alias_files[] = { + { "e.ww", + "package e;\n" + "\n" + "export type myerr = !i64;\n" + "export type res = (i64 | myerr);\n" }, + { "main.ww", + "package main;\n" + "\n" + "import e;\n" + "\n" + "type other = !i64;\n" + "\n" + "fn f(x: other) e.res = { return x; };\n" + "\n" + "export fn main() i32 = { return 0; };\n" }, + { NULL, NULL } +}; + +static const struct scenario scenarios[] = { + { "nominal_match", nominal_match_files, 1, 42 }, + { "a6_concrete", a6_concrete_files, 1, 0 }, + { "incompat_concrete", incompat_concrete_files, 0, 0 }, + { "distinct_alias", distinct_alias_files, 0, 0 }, +}; + +struct driver { const char *name; char path[2100]; int gated; }; + +static int +run_one(const struct driver *drv, const struct scenario *sc) +{ + char dir[] = "/tmp/ww839_XXXXXX"; + if (mkdtemp(dir) == NULL) { + fprintf(stderr, "839[%s][%s]: mkdtemp failed\n", + drv->name, sc->label); + return -1; + } + + char path[1024], cmd[4096]; + int rc = 0; + + for (int i = 0; sc->files[i].name; i++) { + snprintf(path, sizeof path, "%s/%s", dir, sc->files[i].name); + FILE *f = fopen(path, "wb"); + if (!f) { + fprintf(stderr, "839[%s][%s]: write %s\n", + drv->name, sc->label, sc->files[i].name); + rc = -1; goto done; + } + fputs(sc->files[i].src, f); + fclose(f); + } + + snprintf(cmd, sizeof cmd, "cd %s && %s build -I %s %s/main.ww " + ">/dev/null 2>&1", dir, drv->path, dir, dir); + int brc = runwait(cmd); + + if (!sc->expect_build) { + /* NEG: the build must FAIL on this stage. */ + if (brc == 0) { + fprintf(stderr, "839[%s][%s]: built ok, expected a " + "confident reject\n", drv->name, sc->label); + rc = -1; + } + goto done; + } + + if (brc != 0) { + fprintf(stderr, "839[%s][%s]: build failed, expected ok\n", + drv->name, sc->label); + rc = -1; goto done; + } + snprintf(path, sizeof path, "%s/main", dir); + int got = runwait(path); + if (got != sc->want_exit) { + fprintf(stderr, "839[%s][%s]: exit %d, want %d\n", + drv->name, sc->label, got, sc->want_exit); + rc = -1; + } + +done: + snprintf(cmd, sizeof cmd, "rm -rf %s", dir); + (void)runwait(cmd); + return rc; +} + +int +main(void) +{ + const char *bin = getenv("BIN"); + if (!bin) bin = "out/bin"; + char absbin[2048]; + if (bin[0] != '/') { + char cwd[1024]; + if (getcwd(cwd, sizeof cwd) == NULL) return 1; + snprintf(absbin, sizeof absbin, "%s/%s", cwd, bin); + bin = absbin; + } + + struct driver drivers[2]; + snprintf(drivers[0].path, sizeof drivers[0].path, "%s/ww", bin); + drivers[0].name = "cstage"; + drivers[0].gated = 0; + snprintf(drivers[1].path, sizeof drivers[1].path, "%s/ww_ww", bin); + drivers[1].name = "wwstage"; + drivers[1].gated = 1; + + int n = (int)(sizeof scenarios / sizeof scenarios[0]); + int total = 0, fail = 0; + + for (int d = 0; d < 2; d++) { + if (drivers[d].gated && access(drivers[d].path, X_OK) != 0) { + fprintf(stderr, "839: skip %s (no %s)\n", + drivers[d].name, drivers[d].path); + continue; + } + for (int i = 0; i < n; i++) { + total++; + if (run_one(&drivers[d], &scenarios[i]) != 0) + fail++; + } + } + + if (fail) { + fprintf(stderr, "839 xmod_nominal_typeeqast: %d/%d failed\n", + fail, total); + return 1; + } + printf("xmod_nominal_typeeqast: %d/%d ok (cross-module nominal " + "accept + reject, both stages)\n", total, total); + return 0; +} diff --git a/test/wcc/989_lib_byteid.c b/test/wcc/989_lib_byteid.c index 23f7759a..9508c46f 100644 --- a/test/wcc/989_lib_byteid.c +++ b/test/wcc/989_lib_byteid.c @@ -157,8 +157,11 @@ static const struct ent ents[] = { .mode = M_WWREJECT, .cite = "#59.13" }, { .fixture = "lib/fnmatch/fnmatchtest.ww", .mode = M_WWREJECT, .cite = "#59.14" }, + /* #59.15 shlex graduated to M_ID by B-full Layer 1 (#14 nominal + * typeeqast): the cross-module bare-vs-qualified variant forward + * w6c_ww over-rejected now compiles byte-identically. */ { .fixture = "lib/shlex/shlextest.ww", - .mode = M_WWREJECT, .cite = "#59.15" }, + .mode = M_ID, .cite = "#59.15 graduated by #14 nominal typeeqast" }, { .fixture = NULL }, };