wwstage: match cross-module union variants by (module, leaf) pair (#13)
wwstage's checker rejected matching an imported union's variants cross-module: casevariantin/casecovers' typeeqast did a raw streq, so a union's bare variant "unsupported" failed to match the dotted case pattern "errors.unsupported" (cstage compares resolved-Type identity, qualifier-agnostic). Add a (module, leaf)-pair fallback after typeeqast: reduce both the case pattern and each variant to (module, leaf) and match on pair equality -- a dotted name keeps its own qualifier, a bare name takes the union's defining module (taggeddefmod, via the aliassym hop chain). This closes BOTH directions: the false-reject of valid cross-module match AND a false-accept of a foreign same-leaf qualifier (case othermod.foo vs errors.error now rejected, matching cstage). Handles the nested errors.error-in-io.error case (the dotted variant keeps mod=errors, not the union's mod=io). typeeqast stays the first check so currently-valid code is byte-id-unchanged; the pair-match fires only on the previously-rejected qualified-vs-bare mix. Wired into casevariantin, casecovers, and the is/as caller. Adds test/wcc/787 (cross-module positive, exhaustiveness, foreign-qualifier reject-guard, dotted-variant body). Unblocks #5's cross-module io.error/errors.error decomposition. rule-10 fix-up; #10-family (wwstage cross-module resolution).
This commit is contained in:
12
Makefile
12
Makefile
@@ -336,6 +336,7 @@ TESTS = $(BIN)/test_smoke $(BIN)/test_lex $(BIN)/test_parse $(BIN)/test_check \
|
||||
$(BIN)/test_fieldfn_leaf_collide_run \
|
||||
$(BIN)/test_amp_fn_assign_run \
|
||||
$(BIN)/test_xmod_alias_struct_collide_run \
|
||||
$(BIN)/test_xmod_variant_match \
|
||||
$(BIN)/test_structvariant_largeunion_return \
|
||||
$(BIN)/test_narrow_alias_deref_store \
|
||||
$(BIN)/test_bufio_vstream_run \
|
||||
@@ -743,6 +744,17 @@ $(BIN)/test_xmod_alias_struct_collide_run: test/wcc/784_xmod_alias_struct_collid
|
||||
$(LIB)/libwwrt.a | $(BIN)
|
||||
$(CC) $(CFLAGS) -o $@ $<
|
||||
|
||||
# #13: cross-module decomposition of an imported union's variants
|
||||
# (`case pkg.a`). Pre-fix the wwstage checker rejected it; cstage built
|
||||
# it. cstage driver build + run pins routing; raw w6c_ww on the
|
||||
# combined.ww (must now accept) + cs==ww byte-id is the discriminator.
|
||||
# Builds its own 2-module fixtures in a private mktemp dir.
|
||||
$(BIN)/test_xmod_variant_match: test/wcc/787_xmod_variant_match.c \
|
||||
$(BIN)/ww $(BIN)/w6c $(BIN)/w6a $(BIN)/w6l \
|
||||
$(BIN)/w6c_ww \
|
||||
$(LIB)/libwwrt.a | $(BIN)
|
||||
$(CC) $(CFLAGS) -o $@ $<
|
||||
|
||||
# #9: returning a STRUCT variant of a LARGE (>4-eightbyte) tagged union.
|
||||
# Both-stage byte-id + runtime (reads tag AND the widened &fn field, so a
|
||||
# dropped-store regression can't hide behind self-consistent byte-id).
|
||||
|
||||
@@ -13389,13 +13389,94 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = {
|
||||
// tagged-union type is handled by some case (or a default arm
|
||||
// exists). Multi-pattern `case A | B =>` covers all alts.
|
||||
|
||||
fn casecovers(c: *checker, cs: *node, want: *node) bool = {
|
||||
// qualleaf — rightmost dotted segment of a (possibly module-qualified)
|
||||
// type name; the whole name when unqualified.
|
||||
fn qualleaf(nm: str) str = {
|
||||
let dotidx: i32 = -1;
|
||||
let i: i32 = 0;
|
||||
for (i < nm.len) {
|
||||
if (nm[i] == 46u8) { dotidx = i; };
|
||||
i += 1;
|
||||
};
|
||||
if (dotidx < 0) { return nm; };
|
||||
let leaf: str;
|
||||
leaf.ptr = nm.ptr + ((dotidx + 1): u64);
|
||||
leaf.len = nm.len - dotidx - 1;
|
||||
return leaf;
|
||||
};
|
||||
|
||||
// qualmod — module qualifier of a type name (segment before the
|
||||
// rightmost '.'), or `defmod` when unqualified.
|
||||
fn qualmod(nm: str, defmod: str) str = {
|
||||
let dotidx: i32 = -1;
|
||||
let i: i32 = 0;
|
||||
for (i < nm.len) {
|
||||
if (nm[i] == 46u8) { dotidx = i; };
|
||||
i += 1;
|
||||
};
|
||||
if (dotidx < 0) { return defmod; };
|
||||
let head: str;
|
||||
head.ptr = nm.ptr;
|
||||
head.len = dotidx;
|
||||
return head;
|
||||
};
|
||||
|
||||
// casevariantpairmatch — a case pattern names variant `v` of the tagged
|
||||
// union iff their (module, leaf) PAIRS match. Each name reduces to
|
||||
// (qualmod, qualleaf): a DOTTED name keeps its own qualifier; a BARE
|
||||
// name is attributed `unionmod`, the union's defining module. So a
|
||||
// cross-module `case errors.unsupported` matches the bare `unsupported`
|
||||
// in errors.error's body, while a foreign `othermod.unsupported` is
|
||||
// REJECTED (qualifier differs) and Shape A's dotted `errors.error`
|
||||
// variant of io.error keeps matching `case errors.error` (defmod is
|
||||
// NOT forced onto a dotted variant — drew #13). Approximates harec's
|
||||
// resolved-Type variant identity (cmd/wcc/check.c:1651) at the AST
|
||||
// level via the existing nmod/aliassym machinery (cf. #51/#53); the
|
||||
// precise Type-identity form is #10 (tinfo SSoT). typeeqast handles the
|
||||
// exact-string cases first, so this fires only on the qualified-vs-bare
|
||||
// mix.
|
||||
fn casevariantpairmatch(v: *node, pat: *node, unionmod: str) bool = {
|
||||
let vv: *node = unwrapbang(v);
|
||||
let pp: *node = unwrapbang(pat);
|
||||
if (vv == nil) { return false; };
|
||||
if (pp == nil) { return false; };
|
||||
if (vv.kind != nkind.N_TNAME) { return false; };
|
||||
if (pp.kind != nkind.N_TNAME) { return false; };
|
||||
if (!streq(qualleaf(vv.str), qualleaf(pp.str))) { return false; };
|
||||
return streq(qualmod(vv.str, unionmod), qualmod(pp.str, unionmod));
|
||||
};
|
||||
|
||||
// taggeddefmod — defining module of the typedecl whose body IS the
|
||||
// tagged union (follows alias hops via aliassym, the #51/#53 machinery).
|
||||
// Bare variants in that body are defined here: io.error =
|
||||
// !(errors.error | underread | nomem) (module io) -> "io"; errors.error
|
||||
// -> "errors". Falls back to c.curmod when the chain can't resolve.
|
||||
fn taggeddefmod(c: *checker, st: *node) str = {
|
||||
let defmod: str = c.curmod;
|
||||
let cur: *node = unwrapbang(st);
|
||||
for (cur != nil) {
|
||||
if (cur.kind != nkind.N_TNAME) { return defmod; };
|
||||
let s: *sym = aliassym(c, cur);
|
||||
if (s == nil) { return defmod; };
|
||||
if (s.decl == nil) { return defmod; };
|
||||
if (s.decl.nmod.len > 0) { defmod = s.decl.nmod; };
|
||||
let body: *node = unwrapbang(s.decl.lhs);
|
||||
if (body == nil) { return defmod; };
|
||||
if (body.kind == nkind.N_TTAGGED) { return defmod; };
|
||||
cur = body;
|
||||
};
|
||||
return defmod;
|
||||
};
|
||||
|
||||
fn casecovers(c: *checker, cs: *node, want: *node, unionmod: str) bool = {
|
||||
if (cs.lhs != nil) {
|
||||
if (typeeqast(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 (casevariantpairmatch(want, alt, unionmod)) { return true; };
|
||||
alt = alt.next;
|
||||
};
|
||||
return false;
|
||||
@@ -13417,10 +13498,11 @@ fn errmatchvariant(c: *checker, n: *node, vname: *node) void = {
|
||||
// casevariantin — true iff `pat` (a `case T` pattern, including
|
||||
// each alt of a multi-pattern) names a variant of the tagged
|
||||
// union `tagged`.
|
||||
fn casevariantin(tagged: *node, pat: *node) bool = {
|
||||
fn casevariantin(tagged: *node, pat: *node, unionmod: str) bool = {
|
||||
let v: *node = tagged.list;
|
||||
for (v != nil) {
|
||||
if (typeeqast(v, pat)) { return true; };
|
||||
if (casevariantpairmatch(v, pat, unionmod)) { return true; };
|
||||
v = v.next;
|
||||
};
|
||||
return false;
|
||||
@@ -13446,18 +13528,23 @@ fn checkmatchexhaust(c: *checker, n: *node) void = {
|
||||
let u: *node = resolvealias(c, unwrapbang(st));
|
||||
if (u == nil) { return; };
|
||||
if (u.kind != nkind.N_TTAGGED) { return; };
|
||||
// #13: the union's defining module, so a bare body variant can be
|
||||
// matched against a module-qualified cross-module case pattern (and
|
||||
// a foreign-qualifier pattern correctly rejected). See
|
||||
// casevariantpairmatch.
|
||||
let unionmod: str = taggeddefmod(c, st);
|
||||
// Validity: every `case T` pattern (and multi-pattern alts)
|
||||
// must name a variant of u. Catches typos and dead arms that
|
||||
// the dispatch would never reach.
|
||||
let cs0: *node = n.list;
|
||||
for (cs0 != nil) {
|
||||
if (cs0.lhs != nil) {
|
||||
if (!casevariantin(u, cs0.lhs)) {
|
||||
if (!casevariantin(u, cs0.lhs, unionmod)) {
|
||||
errbadcase(c, cs0.lhs);
|
||||
};
|
||||
let alt: *node = cs0.list;
|
||||
for (alt != nil) {
|
||||
if (!casevariantin(u, alt)) {
|
||||
if (!casevariantin(u, alt, unionmod)) {
|
||||
errbadcase(c, alt);
|
||||
};
|
||||
alt = alt.next;
|
||||
@@ -13477,7 +13564,7 @@ fn checkmatchexhaust(c: *checker, n: *node) void = {
|
||||
let covered: bool = false;
|
||||
let cs2: *node = n.list;
|
||||
for (cs2 != nil) {
|
||||
if (casecovers(c, cs2, v)) {
|
||||
if (casecovers(c, cs2, v, unionmod)) {
|
||||
covered = true;
|
||||
cs2 = nil;
|
||||
} else {
|
||||
@@ -13729,7 +13816,7 @@ fn checkisas(c: *checker, n: *node) void = {
|
||||
if (utinfo != nil) { if (wanttinfo != nil) {
|
||||
if (flatvariantidxt(utinfo, wanttinfo) >= 0) { return; };
|
||||
}; };
|
||||
if (casevariantin(u, want)) { return; };
|
||||
if (casevariantin(u, want, taggeddefmod(c, st))) { return; };
|
||||
os.write(2, "is/as: not a variant of operand".ptr, 31u64);
|
||||
if (want.kind == nkind.N_TNAME) {
|
||||
os.write(2, " (".ptr, 2u64);
|
||||
|
||||
@@ -3288,13 +3288,94 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = {
|
||||
// tagged-union type is handled by some case (or a default arm
|
||||
// exists). Multi-pattern `case A | B =>` covers all alts.
|
||||
|
||||
fn casecovers(c: *checker, cs: *node, want: *node) bool = {
|
||||
// qualleaf — rightmost dotted segment of a (possibly module-qualified)
|
||||
// type name; the whole name when unqualified.
|
||||
fn qualleaf(nm: str) str = {
|
||||
let dotidx: i32 = -1;
|
||||
let i: i32 = 0;
|
||||
for (i < nm.len) {
|
||||
if (nm[i] == 46u8) { dotidx = i; };
|
||||
i += 1;
|
||||
};
|
||||
if (dotidx < 0) { return nm; };
|
||||
let leaf: str;
|
||||
leaf.ptr = nm.ptr + ((dotidx + 1): u64);
|
||||
leaf.len = nm.len - dotidx - 1;
|
||||
return leaf;
|
||||
};
|
||||
|
||||
// qualmod — module qualifier of a type name (segment before the
|
||||
// rightmost '.'), or `defmod` when unqualified.
|
||||
fn qualmod(nm: str, defmod: str) str = {
|
||||
let dotidx: i32 = -1;
|
||||
let i: i32 = 0;
|
||||
for (i < nm.len) {
|
||||
if (nm[i] == 46u8) { dotidx = i; };
|
||||
i += 1;
|
||||
};
|
||||
if (dotidx < 0) { return defmod; };
|
||||
let head: str;
|
||||
head.ptr = nm.ptr;
|
||||
head.len = dotidx;
|
||||
return head;
|
||||
};
|
||||
|
||||
// casevariantpairmatch — a case pattern names variant `v` of the tagged
|
||||
// union iff their (module, leaf) PAIRS match. Each name reduces to
|
||||
// (qualmod, qualleaf): a DOTTED name keeps its own qualifier; a BARE
|
||||
// name is attributed `unionmod`, the union's defining module. So a
|
||||
// cross-module `case errors.unsupported` matches the bare `unsupported`
|
||||
// in errors.error's body, while a foreign `othermod.unsupported` is
|
||||
// REJECTED (qualifier differs) and Shape A's dotted `errors.error`
|
||||
// variant of io.error keeps matching `case errors.error` (defmod is
|
||||
// NOT forced onto a dotted variant — drew #13). Approximates harec's
|
||||
// resolved-Type variant identity (cmd/wcc/check.c:1651) at the AST
|
||||
// level via the existing nmod/aliassym machinery (cf. #51/#53); the
|
||||
// precise Type-identity form is #10 (tinfo SSoT). typeeqast handles the
|
||||
// exact-string cases first, so this fires only on the qualified-vs-bare
|
||||
// mix.
|
||||
fn casevariantpairmatch(v: *node, pat: *node, unionmod: str) bool = {
|
||||
let vv: *node = unwrapbang(v);
|
||||
let pp: *node = unwrapbang(pat);
|
||||
if (vv == nil) { return false; };
|
||||
if (pp == nil) { return false; };
|
||||
if (vv.kind != nkind.N_TNAME) { return false; };
|
||||
if (pp.kind != nkind.N_TNAME) { return false; };
|
||||
if (!streq(qualleaf(vv.str), qualleaf(pp.str))) { return false; };
|
||||
return streq(qualmod(vv.str, unionmod), qualmod(pp.str, unionmod));
|
||||
};
|
||||
|
||||
// taggeddefmod — defining module of the typedecl whose body IS the
|
||||
// tagged union (follows alias hops via aliassym, the #51/#53 machinery).
|
||||
// Bare variants in that body are defined here: io.error =
|
||||
// !(errors.error | underread | nomem) (module io) -> "io"; errors.error
|
||||
// -> "errors". Falls back to c.curmod when the chain can't resolve.
|
||||
fn taggeddefmod(c: *checker, st: *node) str = {
|
||||
let defmod: str = c.curmod;
|
||||
let cur: *node = unwrapbang(st);
|
||||
for (cur != nil) {
|
||||
if (cur.kind != nkind.N_TNAME) { return defmod; };
|
||||
let s: *sym = aliassym(c, cur);
|
||||
if (s == nil) { return defmod; };
|
||||
if (s.decl == nil) { return defmod; };
|
||||
if (s.decl.nmod.len > 0) { defmod = s.decl.nmod; };
|
||||
let body: *node = unwrapbang(s.decl.lhs);
|
||||
if (body == nil) { return defmod; };
|
||||
if (body.kind == nkind.N_TTAGGED) { return defmod; };
|
||||
cur = body;
|
||||
};
|
||||
return defmod;
|
||||
};
|
||||
|
||||
fn casecovers(c: *checker, cs: *node, want: *node, unionmod: str) bool = {
|
||||
if (cs.lhs != nil) {
|
||||
if (typeeqast(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 (casevariantpairmatch(want, alt, unionmod)) { return true; };
|
||||
alt = alt.next;
|
||||
};
|
||||
return false;
|
||||
@@ -3316,10 +3397,11 @@ fn errmatchvariant(c: *checker, n: *node, vname: *node) void = {
|
||||
// casevariantin — true iff `pat` (a `case T` pattern, including
|
||||
// each alt of a multi-pattern) names a variant of the tagged
|
||||
// union `tagged`.
|
||||
fn casevariantin(tagged: *node, pat: *node) bool = {
|
||||
fn casevariantin(tagged: *node, pat: *node, unionmod: str) bool = {
|
||||
let v: *node = tagged.list;
|
||||
for (v != nil) {
|
||||
if (typeeqast(v, pat)) { return true; };
|
||||
if (casevariantpairmatch(v, pat, unionmod)) { return true; };
|
||||
v = v.next;
|
||||
};
|
||||
return false;
|
||||
@@ -3345,18 +3427,23 @@ fn checkmatchexhaust(c: *checker, n: *node) void = {
|
||||
let u: *node = resolvealias(c, unwrapbang(st));
|
||||
if (u == nil) { return; };
|
||||
if (u.kind != nkind.N_TTAGGED) { return; };
|
||||
// #13: the union's defining module, so a bare body variant can be
|
||||
// matched against a module-qualified cross-module case pattern (and
|
||||
// a foreign-qualifier pattern correctly rejected). See
|
||||
// casevariantpairmatch.
|
||||
let unionmod: str = taggeddefmod(c, st);
|
||||
// Validity: every `case T` pattern (and multi-pattern alts)
|
||||
// must name a variant of u. Catches typos and dead arms that
|
||||
// the dispatch would never reach.
|
||||
let cs0: *node = n.list;
|
||||
for (cs0 != nil) {
|
||||
if (cs0.lhs != nil) {
|
||||
if (!casevariantin(u, cs0.lhs)) {
|
||||
if (!casevariantin(u, cs0.lhs, unionmod)) {
|
||||
errbadcase(c, cs0.lhs);
|
||||
};
|
||||
let alt: *node = cs0.list;
|
||||
for (alt != nil) {
|
||||
if (!casevariantin(u, alt)) {
|
||||
if (!casevariantin(u, alt, unionmod)) {
|
||||
errbadcase(c, alt);
|
||||
};
|
||||
alt = alt.next;
|
||||
@@ -3376,7 +3463,7 @@ fn checkmatchexhaust(c: *checker, n: *node) void = {
|
||||
let covered: bool = false;
|
||||
let cs2: *node = n.list;
|
||||
for (cs2 != nil) {
|
||||
if (casecovers(c, cs2, v)) {
|
||||
if (casecovers(c, cs2, v, unionmod)) {
|
||||
covered = true;
|
||||
cs2 = nil;
|
||||
} else {
|
||||
@@ -3628,7 +3715,7 @@ fn checkisas(c: *checker, n: *node) void = {
|
||||
if (utinfo != nil) { if (wanttinfo != nil) {
|
||||
if (flatvariantidxt(utinfo, wanttinfo) >= 0) { return; };
|
||||
}; };
|
||||
if (casevariantin(u, want)) { return; };
|
||||
if (casevariantin(u, want, taggeddefmod(c, st))) { return; };
|
||||
os.write(2, "is/as: not a variant of operand".ptr, 31u64);
|
||||
if (want.kind == nkind.N_TNAME) {
|
||||
os.write(2, " (".ptr, 2u64);
|
||||
|
||||
@@ -13389,13 +13389,94 @@ fn isassignable(c: *checker, dst: *node, src: *node, confident: *bool) bool = {
|
||||
// tagged-union type is handled by some case (or a default arm
|
||||
// exists). Multi-pattern `case A | B =>` covers all alts.
|
||||
|
||||
fn casecovers(c: *checker, cs: *node, want: *node) bool = {
|
||||
// qualleaf — rightmost dotted segment of a (possibly module-qualified)
|
||||
// type name; the whole name when unqualified.
|
||||
fn qualleaf(nm: str) str = {
|
||||
let dotidx: i32 = -1;
|
||||
let i: i32 = 0;
|
||||
for (i < nm.len) {
|
||||
if (nm[i] == 46u8) { dotidx = i; };
|
||||
i += 1;
|
||||
};
|
||||
if (dotidx < 0) { return nm; };
|
||||
let leaf: str;
|
||||
leaf.ptr = nm.ptr + ((dotidx + 1): u64);
|
||||
leaf.len = nm.len - dotidx - 1;
|
||||
return leaf;
|
||||
};
|
||||
|
||||
// qualmod — module qualifier of a type name (segment before the
|
||||
// rightmost '.'), or `defmod` when unqualified.
|
||||
fn qualmod(nm: str, defmod: str) str = {
|
||||
let dotidx: i32 = -1;
|
||||
let i: i32 = 0;
|
||||
for (i < nm.len) {
|
||||
if (nm[i] == 46u8) { dotidx = i; };
|
||||
i += 1;
|
||||
};
|
||||
if (dotidx < 0) { return defmod; };
|
||||
let head: str;
|
||||
head.ptr = nm.ptr;
|
||||
head.len = dotidx;
|
||||
return head;
|
||||
};
|
||||
|
||||
// casevariantpairmatch — a case pattern names variant `v` of the tagged
|
||||
// union iff their (module, leaf) PAIRS match. Each name reduces to
|
||||
// (qualmod, qualleaf): a DOTTED name keeps its own qualifier; a BARE
|
||||
// name is attributed `unionmod`, the union's defining module. So a
|
||||
// cross-module `case errors.unsupported` matches the bare `unsupported`
|
||||
// in errors.error's body, while a foreign `othermod.unsupported` is
|
||||
// REJECTED (qualifier differs) and Shape A's dotted `errors.error`
|
||||
// variant of io.error keeps matching `case errors.error` (defmod is
|
||||
// NOT forced onto a dotted variant — drew #13). Approximates harec's
|
||||
// resolved-Type variant identity (cmd/wcc/check.c:1651) at the AST
|
||||
// level via the existing nmod/aliassym machinery (cf. #51/#53); the
|
||||
// precise Type-identity form is #10 (tinfo SSoT). typeeqast handles the
|
||||
// exact-string cases first, so this fires only on the qualified-vs-bare
|
||||
// mix.
|
||||
fn casevariantpairmatch(v: *node, pat: *node, unionmod: str) bool = {
|
||||
let vv: *node = unwrapbang(v);
|
||||
let pp: *node = unwrapbang(pat);
|
||||
if (vv == nil) { return false; };
|
||||
if (pp == nil) { return false; };
|
||||
if (vv.kind != nkind.N_TNAME) { return false; };
|
||||
if (pp.kind != nkind.N_TNAME) { return false; };
|
||||
if (!streq(qualleaf(vv.str), qualleaf(pp.str))) { return false; };
|
||||
return streq(qualmod(vv.str, unionmod), qualmod(pp.str, unionmod));
|
||||
};
|
||||
|
||||
// taggeddefmod — defining module of the typedecl whose body IS the
|
||||
// tagged union (follows alias hops via aliassym, the #51/#53 machinery).
|
||||
// Bare variants in that body are defined here: io.error =
|
||||
// !(errors.error | underread | nomem) (module io) -> "io"; errors.error
|
||||
// -> "errors". Falls back to c.curmod when the chain can't resolve.
|
||||
fn taggeddefmod(c: *checker, st: *node) str = {
|
||||
let defmod: str = c.curmod;
|
||||
let cur: *node = unwrapbang(st);
|
||||
for (cur != nil) {
|
||||
if (cur.kind != nkind.N_TNAME) { return defmod; };
|
||||
let s: *sym = aliassym(c, cur);
|
||||
if (s == nil) { return defmod; };
|
||||
if (s.decl == nil) { return defmod; };
|
||||
if (s.decl.nmod.len > 0) { defmod = s.decl.nmod; };
|
||||
let body: *node = unwrapbang(s.decl.lhs);
|
||||
if (body == nil) { return defmod; };
|
||||
if (body.kind == nkind.N_TTAGGED) { return defmod; };
|
||||
cur = body;
|
||||
};
|
||||
return defmod;
|
||||
};
|
||||
|
||||
fn casecovers(c: *checker, cs: *node, want: *node, unionmod: str) bool = {
|
||||
if (cs.lhs != nil) {
|
||||
if (typeeqast(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 (casevariantpairmatch(want, alt, unionmod)) { return true; };
|
||||
alt = alt.next;
|
||||
};
|
||||
return false;
|
||||
@@ -13417,10 +13498,11 @@ fn errmatchvariant(c: *checker, n: *node, vname: *node) void = {
|
||||
// casevariantin — true iff `pat` (a `case T` pattern, including
|
||||
// each alt of a multi-pattern) names a variant of the tagged
|
||||
// union `tagged`.
|
||||
fn casevariantin(tagged: *node, pat: *node) bool = {
|
||||
fn casevariantin(tagged: *node, pat: *node, unionmod: str) bool = {
|
||||
let v: *node = tagged.list;
|
||||
for (v != nil) {
|
||||
if (typeeqast(v, pat)) { return true; };
|
||||
if (casevariantpairmatch(v, pat, unionmod)) { return true; };
|
||||
v = v.next;
|
||||
};
|
||||
return false;
|
||||
@@ -13446,18 +13528,23 @@ fn checkmatchexhaust(c: *checker, n: *node) void = {
|
||||
let u: *node = resolvealias(c, unwrapbang(st));
|
||||
if (u == nil) { return; };
|
||||
if (u.kind != nkind.N_TTAGGED) { return; };
|
||||
// #13: the union's defining module, so a bare body variant can be
|
||||
// matched against a module-qualified cross-module case pattern (and
|
||||
// a foreign-qualifier pattern correctly rejected). See
|
||||
// casevariantpairmatch.
|
||||
let unionmod: str = taggeddefmod(c, st);
|
||||
// Validity: every `case T` pattern (and multi-pattern alts)
|
||||
// must name a variant of u. Catches typos and dead arms that
|
||||
// the dispatch would never reach.
|
||||
let cs0: *node = n.list;
|
||||
for (cs0 != nil) {
|
||||
if (cs0.lhs != nil) {
|
||||
if (!casevariantin(u, cs0.lhs)) {
|
||||
if (!casevariantin(u, cs0.lhs, unionmod)) {
|
||||
errbadcase(c, cs0.lhs);
|
||||
};
|
||||
let alt: *node = cs0.list;
|
||||
for (alt != nil) {
|
||||
if (!casevariantin(u, alt)) {
|
||||
if (!casevariantin(u, alt, unionmod)) {
|
||||
errbadcase(c, alt);
|
||||
};
|
||||
alt = alt.next;
|
||||
@@ -13477,7 +13564,7 @@ fn checkmatchexhaust(c: *checker, n: *node) void = {
|
||||
let covered: bool = false;
|
||||
let cs2: *node = n.list;
|
||||
for (cs2 != nil) {
|
||||
if (casecovers(c, cs2, v)) {
|
||||
if (casecovers(c, cs2, v, unionmod)) {
|
||||
covered = true;
|
||||
cs2 = nil;
|
||||
} else {
|
||||
@@ -13729,7 +13816,7 @@ fn checkisas(c: *checker, n: *node) void = {
|
||||
if (utinfo != nil) { if (wanttinfo != nil) {
|
||||
if (flatvariantidxt(utinfo, wanttinfo) >= 0) { return; };
|
||||
}; };
|
||||
if (casevariantin(u, want)) { return; };
|
||||
if (casevariantin(u, want, taggeddefmod(c, st))) { return; };
|
||||
os.write(2, "is/as: not a variant of operand".ptr, 31u64);
|
||||
if (want.kind == nkind.N_TNAME) {
|
||||
os.write(2, " (".ptr, 2u64);
|
||||
|
||||
368
test/wcc/787_xmod_variant_match.c
Normal file
368
test/wcc/787_xmod_variant_match.c
Normal file
@@ -0,0 +1,368 @@
|
||||
/*
|
||||
* 787_xmod_variant_match — project #13 close. Pins that a `package main`
|
||||
* can decompose an IMPORTED union's individual VARIANTS cross-module
|
||||
* (`match (e: pkg.u) { case pkg.a => ...; case let o: pkg.b => ... }`),
|
||||
* on BOTH stages, byte-identically (rule-10).
|
||||
*
|
||||
* THE BUG (wwstage-CHECKER-only, cs!=ww): wwstage's match-arm validity
|
||||
* (selfhost/cmd/wcc/check.ww casevariantin) and exhaustiveness
|
||||
* (casecovers) compared via typeeqast, whose N_TNAME arm is a raw
|
||||
* streq. A union's variant is written UNQUALIFIED in its defining
|
||||
* module (`a` in pkg.u's body); the cross-module case pattern is the
|
||||
* dotted `pkg.a` (one N_TNAME). streq("a","pkg.a") -> false -> wwstage
|
||||
* rejected "case: not a variant of scrutinee". cstage compares
|
||||
* resolved-Type identity (variant_match, cmd/wcc/check.c:1651), so the
|
||||
* qualifier is irrelevant and it built+routed correctly. Hare allows
|
||||
* cross-module variant decomposition; align UP to cstage (a too-strict
|
||||
* checker, NOT a down-align that would forbid the feature).
|
||||
*
|
||||
* THE FIX (#13): casevariantpairmatch reduces BOTH pattern and variant
|
||||
* to a (module, leaf) pair — a dotted name keeps its own qualifier, a
|
||||
* bare name is attributed the union's defining module (taggeddefmod via
|
||||
* aliassym .decl.nmod) — and matches the pairs. This accepts a
|
||||
* cross-module `case errors.unsupported` (vs bare `unsupported`) while
|
||||
* REJECTING a foreign `othermod.unsupported`, and keeps a dotted body
|
||||
* variant matching its own qualifier (Shape A). Wired into BOTH
|
||||
* casevariantin (validity) and casecovers (exhaustiveness). #10-family
|
||||
* (AST-name vs cstage tinfo); precise Type identity is the #10 endgame.
|
||||
*
|
||||
* The PRE-FIX failure mode is a wwstage CHECKER REJECT, so the
|
||||
* discriminator is the `w6c_ww` build of the combined.ww succeeding at
|
||||
* all — pre-fix it errored out; post-fix it succeeds AND is byte-id
|
||||
* with cstage. Self-contained 2-module fixtures (no lib coupling).
|
||||
*
|
||||
* scenario | shape | exit | byte-id
|
||||
* ------------------+----------------------------------------+------+--------
|
||||
* no_default | match pkg.u=!(a|b), bare arms, NO | 21 | cs==ww
|
||||
* | default — exercises casecovers | |
|
||||
* | exhaustiveness cross-module | |
|
||||
* bind_and_default | match pkg.u2=!(a|b|c): bare arm + a | 42 | cs==ww
|
||||
* | `case let o: pkg.b` BOUND arm + a | |
|
||||
* | `case =>` default; if-checks each arm | |
|
||||
* foreign_qualifier | `case o.a` vs pkg.u (o.a is a DIFFERENT | both | (reject
|
||||
* | module's same-leaf type) — BOTH |reject| net)
|
||||
* | stages must REJECT; guards the fix | |
|
||||
* | against a false-ACCEPT (bare leaf- | |
|
||||
* | strip would wrongly accept) | |
|
||||
* shape_a_dotted_var| union BODY holds dotted variant | 12 | cs==ww
|
||||
* | pkg.e=!(errs.bad|local); case errs.bad| |
|
||||
* | keeps OWN qualifier (drew Shape A) — | |
|
||||
* | sole positive lock on qualmod's | |
|
||||
* | dotted-variant branch | |
|
||||
*
|
||||
* GATE POLARITY: must stay GREEN. Red means wwstage rejected a valid
|
||||
* cross-module variant match again (w6c_ww build fails) or the routing
|
||||
* diverged (byte-id / runtime).
|
||||
*/
|
||||
#include <stdio.h>
|
||||
#include <stdlib.h>
|
||||
#include <unistd.h>
|
||||
#include <sys/stat.h>
|
||||
#include <sys/wait.h>
|
||||
|
||||
static int
|
||||
runwait(const char *cmd)
|
||||
{
|
||||
int rc = system(cmd);
|
||||
if (rc == -1) return -1;
|
||||
if (WIFEXITED(rc)) return WEXITSTATUS(rc);
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int
|
||||
slurp_eq(const char *a, const char *b)
|
||||
{
|
||||
FILE *fa = fopen(a, "rb");
|
||||
FILE *fb = fopen(b, "rb");
|
||||
if (!fa || !fb) { if (fa) fclose(fa); if (fb) fclose(fb); return -1; }
|
||||
int rc = 0;
|
||||
for (;;) {
|
||||
int ca = fgetc(fa);
|
||||
int cb = fgetc(fb);
|
||||
if (ca != cb) { rc = -1; break; }
|
||||
if (ca == EOF) break;
|
||||
}
|
||||
fclose(fa); fclose(fb);
|
||||
return rc;
|
||||
}
|
||||
|
||||
struct file { const char *name; const char *src; };
|
||||
|
||||
struct scenario {
|
||||
const char *label;
|
||||
const struct file *files; /* name==NULL terminates */
|
||||
int want_exit;
|
||||
int expect_reject; /* 1 = BOTH stages must REJECT (checker) */
|
||||
};
|
||||
|
||||
/* ---- no_default: bare cross-module arms, no default (exhaustiveness) */
|
||||
static const struct file no_default_files[] = {
|
||||
{ "pkg.ww",
|
||||
"package pkg;\n"
|
||||
"export type a = !void;\n"
|
||||
"export type b = !void;\n"
|
||||
"export type u = !(a | b);\n"
|
||||
"export fn mka() u = { let x: a; return x; };\n"
|
||||
"export fn mkb() u = { let y: b; return y; };\n" },
|
||||
{ "main.ww",
|
||||
"package main;\n"
|
||||
"import pkg;\n"
|
||||
"fn classify(e: pkg.u) i32 = {\n"
|
||||
" match (e) {\n"
|
||||
" case pkg.a => return 1;\n"
|
||||
" case pkg.b => return 2;\n"
|
||||
" };\n"
|
||||
"};\n"
|
||||
"export fn main() i32 = {\n"
|
||||
" return classify(pkg.mkb()) * 10 + classify(pkg.mka());\n"
|
||||
"};\n" },
|
||||
{ NULL, NULL }
|
||||
};
|
||||
|
||||
/* ---- bind_and_default: bound arm + default cross-module ----------- */
|
||||
static const struct file bind_default_files[] = {
|
||||
{ "pkg.ww",
|
||||
"package pkg;\n"
|
||||
"export type a = !void;\n"
|
||||
"export type b = !void;\n"
|
||||
"export type c = !void;\n"
|
||||
"export type u2 = !(a | b | c);\n"
|
||||
"export fn mka() u2 = { let x: a; return x; };\n"
|
||||
"export fn mkb() u2 = { let y: b; return y; };\n"
|
||||
"export fn mkc() u2 = { let z: c; return z; };\n" },
|
||||
{ "main.ww",
|
||||
"package main;\n"
|
||||
"import pkg;\n"
|
||||
"fn classify(e: pkg.u2) i32 = {\n"
|
||||
" match (e) {\n"
|
||||
" case pkg.a => return 1;\n"
|
||||
" case let o: pkg.b => return 2;\n"
|
||||
" case => return 9;\n"
|
||||
" };\n"
|
||||
"};\n"
|
||||
"export fn main() i32 = {\n"
|
||||
" if (classify(pkg.mka()) != 1) { return 11; };\n"
|
||||
" if (classify(pkg.mkb()) != 2) { return 12; };\n"
|
||||
" if (classify(pkg.mkc()) != 9) { return 13; };\n"
|
||||
" return 42;\n"
|
||||
"};\n" },
|
||||
{ NULL, NULL }
|
||||
};
|
||||
|
||||
/* ---- foreign_qualifier: a same-leaf variant from a DIFFERENT module
|
||||
* is NOT a variant of the scrutinee — BOTH stages must REJECT. Guards
|
||||
* the #13 fix against trading the false-reject for a false-ACCEPT: a
|
||||
* bare leaf-strip would wrongly accept `o.a` (leaf "a" matches pkg's
|
||||
* variant `a`); the (module,leaf)-pair match rejects it (o != pkg). */
|
||||
static const struct file foreign_qualifier_files[] = {
|
||||
{ "pkg.ww",
|
||||
"package pkg;\n"
|
||||
"export type a = !void;\n"
|
||||
"export type b = !void;\n"
|
||||
"export type u = !(a | b);\n"
|
||||
"export fn mka() u = { let x: a; return x; };\n" },
|
||||
{ "o.ww",
|
||||
"package o;\n"
|
||||
"export type a = !void;\n" },
|
||||
{ "main.ww",
|
||||
"package main;\n"
|
||||
"import pkg;\n"
|
||||
"import o;\n"
|
||||
"fn classify(e: pkg.u) i32 = {\n"
|
||||
" match (e) {\n"
|
||||
" case o.a => return 1;\n"
|
||||
" case => return 0;\n"
|
||||
" };\n"
|
||||
"};\n"
|
||||
"export fn main() i32 = { return classify(pkg.mka()); };\n" },
|
||||
{ NULL, NULL }
|
||||
};
|
||||
|
||||
/* ---- shape_a_dotted_variant: the union's BODY itself holds a dotted
|
||||
* cross-module variant (`pkg.e = !(errs.bad | local)`), matched with a
|
||||
* dotted `case errs.bad` AND a bare-variant `case pkg.local`. This is
|
||||
* drew's Shape A (cf. io.error nesting errors.error): the dotted body
|
||||
* variant `errs.bad` must keep ITS OWN qualifier (errs), NOT be forced
|
||||
* onto the union's defining module (pkg) — else `case errs.bad` would
|
||||
* false-REJECT. The other three scenarios carry only BARE body variants,
|
||||
* so this is the sole positive lock on qualmod's dotted-variant branch;
|
||||
* a regression that attributes unionmod to every variant breaks here
|
||||
* alone. exit = classify(mkbad())*10 + classify(mklocal()) = 1*10+2. */
|
||||
static const struct file shape_a_files[] = {
|
||||
{ "errs.ww",
|
||||
"package errs;\n"
|
||||
"export type bad = !void;\n" },
|
||||
{ "pkg.ww",
|
||||
"package pkg;\n"
|
||||
"import errs;\n"
|
||||
"export type local = !void;\n"
|
||||
"export type e = !(errs.bad | local);\n"
|
||||
"export fn mkbad() e = { let x: errs.bad; return x; };\n"
|
||||
"export fn mklocal() e = { let y: local; return y; };\n" },
|
||||
{ "main.ww",
|
||||
"package main;\n"
|
||||
"import pkg;\n"
|
||||
"import errs;\n"
|
||||
"fn classify(x: pkg.e) i32 = {\n"
|
||||
" match (x) {\n"
|
||||
" case errs.bad => return 1;\n"
|
||||
" case pkg.local => return 2;\n"
|
||||
" };\n"
|
||||
"};\n"
|
||||
"export fn main() i32 = {\n"
|
||||
" return classify(pkg.mkbad()) * 10 + classify(pkg.mklocal());\n"
|
||||
"};\n" },
|
||||
{ NULL, NULL }
|
||||
};
|
||||
|
||||
static const struct scenario scenarios[] = {
|
||||
{ "no_default", no_default_files, 21, 0 },
|
||||
{ "bind_and_default", bind_default_files, 42, 0 },
|
||||
{ "foreign_qualifier", foreign_qualifier_files, 0, 1 },
|
||||
{ "shape_a_dotted_var", shape_a_files, 12, 0 },
|
||||
};
|
||||
|
||||
static int
|
||||
run_scenario(const char *bin, const char *w6c, const char *w6c_ww,
|
||||
const struct scenario *sc)
|
||||
{
|
||||
char dir[] = "/tmp/ww787_XXXXXX";
|
||||
if (mkdtemp(dir) == NULL) {
|
||||
fprintf(stderr, "787[%s]: mkdtemp failed\n", 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, "787[%s]: write %s\n", sc->label,
|
||||
sc->files[i].name); rc = -1; goto done; }
|
||||
fputs(sc->files[i].src, f);
|
||||
fclose(f);
|
||||
}
|
||||
|
||||
char comb[1024];
|
||||
snprintf(comb, sizeof comb, "%s/main.combined.ww", dir);
|
||||
|
||||
if (sc->expect_reject) {
|
||||
/* The driver build MUST fail (cstage's checker rejects the
|
||||
* foreign-qualifier variant), but expand() still writes the
|
||||
* combined unit before the checker runs — so re-check it with
|
||||
* BOTH backends and require BOTH to reject. w6c_ww accepting
|
||||
* here is the #13 false-ACCEPT regression. */
|
||||
char xs[1024];
|
||||
snprintf(xs, sizeof xs, "%s/x.s", dir);
|
||||
snprintf(cmd, sizeof cmd,
|
||||
"cd %s && %s/ww build -I %s %s/main.ww >/dev/null 2>&1",
|
||||
dir, bin, dir, dir);
|
||||
if (runwait(cmd) == 0) {
|
||||
fprintf(stderr, "787[%s]: cstage build SUCCEEDED, "
|
||||
"expected reject\n", sc->label);
|
||||
rc = -1; goto done;
|
||||
}
|
||||
if (access(comb, 0) != 0) {
|
||||
fprintf(stderr, "787[%s]: no combined.ww to re-check\n",
|
||||
sc->label);
|
||||
rc = -1; goto done;
|
||||
}
|
||||
snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", w6c, xs, comb);
|
||||
if (runwait(cmd) == 0) {
|
||||
fprintf(stderr, "787[%s]: w6c ACCEPTED foreign variant, "
|
||||
"expected reject\n", sc->label);
|
||||
rc = -1; goto done;
|
||||
}
|
||||
snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", w6c_ww, xs, comb);
|
||||
if (runwait(cmd) == 0) {
|
||||
fprintf(stderr, "787[%s]: w6c_ww ACCEPTED foreign variant "
|
||||
"(#13 false-accept), expected reject\n", sc->label);
|
||||
rc = -1; goto done;
|
||||
}
|
||||
goto done;
|
||||
}
|
||||
|
||||
/* cstage driver build + run: pins runtime routing. */
|
||||
snprintf(cmd, sizeof cmd, "cd %s && %s/ww build -I %s %s/main.ww",
|
||||
dir, bin, dir, dir);
|
||||
if (runwait(cmd) != 0) {
|
||||
fprintf(stderr, "787[%s]: cstage build failed\n", sc->label);
|
||||
rc = -1; goto done;
|
||||
}
|
||||
snprintf(path, sizeof path, "%s/main", dir);
|
||||
int got = runwait(path);
|
||||
if (got != sc->want_exit) {
|
||||
fprintf(stderr, "787[%s]: cstage exit %d, want %d\n",
|
||||
sc->label, got, sc->want_exit);
|
||||
rc = -1;
|
||||
}
|
||||
|
||||
/* The #13 discriminator: raw w6c_ww on the driver-produced
|
||||
* combined.ww. Pre-fix the wwstage checker REJECTED the
|
||||
* cross-module variant arms (non-zero exit here). Post-fix it
|
||||
* accepts AND is byte-id with cstage's w6c. */
|
||||
char cs_s[1024], ws_s[1024];
|
||||
snprintf(cs_s, sizeof cs_s, "%s/cs.s", dir);
|
||||
snprintf(ws_s, sizeof ws_s, "%s/ww.s", dir);
|
||||
snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", w6c, cs_s, comb);
|
||||
if (runwait(cmd) != 0) {
|
||||
fprintf(stderr, "787[%s]: w6c on combined failed\n", sc->label);
|
||||
rc = -1; goto done;
|
||||
}
|
||||
snprintf(cmd, sizeof cmd, "%s -o %s %s 2>/dev/null", w6c_ww, ws_s, comb);
|
||||
if (runwait(cmd) != 0) {
|
||||
fprintf(stderr, "787[%s]: w6c_ww on combined failed "
|
||||
"(#13 cross-module variant reject?)\n", sc->label);
|
||||
rc = -1; goto done;
|
||||
}
|
||||
if (slurp_eq(cs_s, ws_s) != 0) {
|
||||
fprintf(stderr, "787[%s]: cs.s/ww.s DIFFER (rule-10 byte-id "
|
||||
"violation)\n", sc->label);
|
||||
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;
|
||||
}
|
||||
|
||||
char w6c[2100], w6c_ww[2100];
|
||||
snprintf(w6c, sizeof w6c, "%s/w6c", bin);
|
||||
snprintf(w6c_ww, sizeof w6c_ww, "%s/w6c_ww", bin);
|
||||
if (access(w6c_ww, X_OK) != 0) {
|
||||
fprintf(stderr, "787: w6c_ww missing — cannot run the cs==ww "
|
||||
"byte-id gate (the whole point of this test)\n");
|
||||
return 1;
|
||||
}
|
||||
|
||||
int n = (int)(sizeof scenarios / sizeof scenarios[0]);
|
||||
int fail = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
if (run_scenario(bin, w6c, w6c_ww, &scenarios[i]) != 0)
|
||||
fail++;
|
||||
}
|
||||
|
||||
if (fail) {
|
||||
fprintf(stderr, "787 xmod_variant_match: %d/%d scenarios failed\n",
|
||||
fail, n);
|
||||
return 1;
|
||||
}
|
||||
printf("xmod_variant_match: %d/%d ok (cstage run + cs==ww byte-id)\n",
|
||||
n, n);
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user