From 9fed4ca492aaf51d4ab44b65fb73a37ae7d65430 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Wed, 20 May 2026 04:34:57 +0900 Subject: [PATCH] selfhost/cmd/wcc/check: nominal-first compare in tagged-variant inclusion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit isassignable's tagged-variant inclusion was resolvealias-unwrapping both src and each variant before typeeqast. Two NAMED structs (e.g. `(void | err)` with src=`err`) both flattened to N_TSTRUCT and typeeqast's conservative struct branch returned false — false positive on the assignability. Cstage variant_match (cmd/wcc/check.c:90-100) compares TY_NAMED pointer-identically, so the nominal name short-circuits before any body inspection. Mirror: try typeeqast on unwrapbang'd src vs unwrapbang'd variant first (catches the N_TNAME nominal match), fall through to resolvealias + structural compare for anonymous- union variants only. Reviewer's negative probe (different types modA.err vs modB.err with same leaf name) still correctly rejects — the parser joins pkg.alias into one TNAME string, so modA.err ≠ modB.err at the nominal level. Bare-vs-qualified residual (cstage admits `(void | M.err)` ← bare `err` inside module M; wwstage still rejects) tracked as #57. Not hit by any current fixture; unblocks #50 (after #56) and #42. 131/131 + 4 lines of pre-existing pessimism cleared in selfhost/cmd/wcc/check.ww's own resolution. --- selfhost/cmd/w6c/main.combined.ww | 19 ++++++++++++++++++- selfhost/cmd/wcc/check.ww | 19 ++++++++++++++++++- selfhost/cmd/wwdump/main.combined.ww | 19 ++++++++++++++++++- 3 files changed, 54 insertions(+), 3 deletions(-) diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index b9d1ba84..c1039f5e 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -7867,9 +7867,26 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = { }; // Tagged-union variant inclusion: src is one of dst's variants. if (du.kind == nkind.N_TTAGGED && su.kind != nkind.N_TTAGGED) { + // #55: nominal-first compare. Cstage variant_match + // (cmd/wcc/check.c:90-100) takes NAMED types as + // pointer-identical, so two `err`s match before bodies + // are resolved. Wwstage's typeeqast already does + // string-nominal on N_TNAME, but pre-fix this branch + // resolvealias-d both v and src to their bodies (e.g. + // N_TSTRUCT), and typeeqast's conservative struct arm + // returned false — `return e;` inside `fn f() (void | err)` + // got flagged. Compare surface forms first; fall through + // to resolved compare only when the surface mismatches + // (covers structurally-anonymous variant cases that + // resolvealias actually disambiguates). Residual: bare-vs- + // qualified TNAME (`(void | modM.err)` variant vs bare `err` + // inside modM) still misses both arms — filed as #57. + let srcraw: *node = unwrapbang(src); let v: *node = du.list; for (v != nil) { - let vu: *node = resolvealias(c, unwrapbang(v)); + let vraw: *node = unwrapbang(v); + if (typeeqast(vraw, srcraw)) { return true; }; + let vu: *node = resolvealias(c, vraw); if (vu != nil) { if (typeeqast(vu, su)) { return true; }; }; diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index 4005fe75..3ded9f30 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -873,9 +873,26 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = { }; // Tagged-union variant inclusion: src is one of dst's variants. if (du.kind == nkind.N_TTAGGED && su.kind != nkind.N_TTAGGED) { + // #55: nominal-first compare. Cstage variant_match + // (cmd/wcc/check.c:90-100) takes NAMED types as + // pointer-identical, so two `err`s match before bodies + // are resolved. Wwstage's typeeqast already does + // string-nominal on N_TNAME, but pre-fix this branch + // resolvealias-d both v and src to their bodies (e.g. + // N_TSTRUCT), and typeeqast's conservative struct arm + // returned false — `return e;` inside `fn f() (void | err)` + // got flagged. Compare surface forms first; fall through + // to resolved compare only when the surface mismatches + // (covers structurally-anonymous variant cases that + // resolvealias actually disambiguates). Residual: bare-vs- + // qualified TNAME (`(void | modM.err)` variant vs bare `err` + // inside modM) still misses both arms — filed as #57. + let srcraw: *node = unwrapbang(src); let v: *node = du.list; for (v != nil) { - let vu: *node = resolvealias(c, unwrapbang(v)); + let vraw: *node = unwrapbang(v); + if (typeeqast(vraw, srcraw)) { return true; }; + let vu: *node = resolvealias(c, vraw); if (vu != nil) { if (typeeqast(vu, su)) { return true; }; }; diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 28d502c3..5b0ae1b4 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -7867,9 +7867,26 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = { }; // Tagged-union variant inclusion: src is one of dst's variants. if (du.kind == nkind.N_TTAGGED && su.kind != nkind.N_TTAGGED) { + // #55: nominal-first compare. Cstage variant_match + // (cmd/wcc/check.c:90-100) takes NAMED types as + // pointer-identical, so two `err`s match before bodies + // are resolved. Wwstage's typeeqast already does + // string-nominal on N_TNAME, but pre-fix this branch + // resolvealias-d both v and src to their bodies (e.g. + // N_TSTRUCT), and typeeqast's conservative struct arm + // returned false — `return e;` inside `fn f() (void | err)` + // got flagged. Compare surface forms first; fall through + // to resolved compare only when the surface mismatches + // (covers structurally-anonymous variant cases that + // resolvealias actually disambiguates). Residual: bare-vs- + // qualified TNAME (`(void | modM.err)` variant vs bare `err` + // inside modM) still misses both arms — filed as #57. + let srcraw: *node = unwrapbang(src); let v: *node = du.list; for (v != nil) { - let vu: *node = resolvealias(c, unwrapbang(v)); + let vraw: *node = unwrapbang(v); + if (typeeqast(vraw, srcraw)) { return true; }; + let vu: *node = resolvealias(c, vraw); if (vu != nil) { if (typeeqast(vu, su)) { return true; }; };