From fc50a27f3ef41270c2f95d8832526bcbe315d7f6 Mon Sep 17 00:00:00 2001 From: Hojun-Cho Date: Sat, 6 Jun 2026 08:06:37 +0900 Subject: [PATCH] =?UTF-8?q?cgen:=20#95=20c3=20reviewer-fold=20=E2=80=94=20?= =?UTF-8?q?is/as=20gate=20exact-only,=20no=20widening=20leak?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit c1/c2 widened flatvariantidxt (selfhost) with the chain + structural tag-synthesis arms and a >=2 ambiguity os.exit, scoped to the cgen WIDEN consumer. But flatvariantidxt is a choke-point: the wwstage is/as ACCEPTANCE gate (check.ww:4677, the #198 spread fallback) reuses it, so the widening leaked into checker acceptance — vs base 329481c: * `let v:(void|ali)=…; v is base` (ali=base): cstage rejects, wwstage ACCEPTED+built — new cs!=ww acceptance divergence (rule-10 break); * `(void|tb)`, `v is ta` (unrelated same-layout): same leak via the c2 structural arm; * `(ali|ali2)`, `v is base`: wwstage DIED with the cgen-internal fatal "flatvariantidxt: source alias chain reaches >=2 variants" DURING CHECK — a cgen diag surfacing in the checker (layering). cstage is unaffected: its is/as gate (check.c:2036) is independent of cg_tag_for_variant (cgen-phase only), so the fuse was already broken at this site — the cgen-helper change moved wwstage's CHECKER but not cstage's. This contradicts the #95 fold scope ("cgen-tag fold, no acceptance change except the ambiguity hard-error [at the widen site]"). Fix (rob-ruled): the is/as gate needs only nominal variant membership = pass 1. Add an explicit `exactonly` mode to flatvariantidxt — the checker caller passes true (returns after the exact loop: no chain/ structural arms, no os.exit), every cgen caller passes false (full tag-synthesis, unchanged). Two consumers, two modes — the honest representation, not a wrapper. cstage's cg_tag_for_variant has no twin checker caller, so it stays full-only and is UNTOUCHED by c3 (rule-10 satisfied: the param changes no asm — cgen always passes false; the checker now MATCHES cstage's reject). casevariantin still backs the #198 spread fallback. Pins (test/wcc/944_variant_chain_b95_run.c, +4 rows -> 56 checks): isas_chain_reject / isas_unrel_reject — BOTH stages reject the leaked is/as shapes (shared experr substring "not a variant"); the c1 chain + c2 structural arms no longer widen acceptance. isas_amb_reject_notcrash — the (ali|ali2)/`is base` shape rejects CLEANLY (the cgen fatal text would be absent -> red), NOT a crash. twin_prim_alias_amb — rob's obligated mixed prim/alias TWIN: (int | ai) ai=int, source aj=int — both share the int bottom under all-variants counting, so the cgen WIDEN (full mode) hard-errors ("source alias chain reaches >=2 variants"), pinned LOUD both stages. The deferred question (should is/as EVER accept cgen's richer chain/ structural shapes? = a checker-strictness feature, both stages together) is filed as task #107, explicitly NOT folded here. Invariants: c1/c2 cgen behavior unchanged (all cgen callers pass false = full mode); suite byte-id rows + the dissolution corpus hold. make all 0; sizelint 0; peellint 0 (the mode param adds no peel sites); combined.ww regen idempotent; test-unit "all 295 tests passed". c3 touches ZERO cstage bytes — cmd/w6c/cgen.c carries only the c1/c2 additions, and cmd/wcc/check.c is unchanged from base 329481c. --- selfhost/cmd/w6c/main.combined.ww | 36 ++++++++++++------ selfhost/cmd/wcc/cgenexpr.ww | 6 +-- selfhost/cmd/wcc/cgenutil.ww | 23 ++++++++---- selfhost/cmd/wcc/check.ww | 7 +++- selfhost/cmd/wwdump/main.combined.ww | 36 ++++++++++++------ test/wcc/944_variant_chain_b95_run.c | 56 ++++++++++++++++++++++++++++ 6 files changed, 131 insertions(+), 33 deletions(-) diff --git a/selfhost/cmd/w6c/main.combined.ww b/selfhost/cmd/w6c/main.combined.ww index dee27fc7..b2ed1fba 100644 --- a/selfhost/cmd/w6c/main.combined.ww +++ b/selfhost/cmd/w6c/main.combined.ww @@ -14955,7 +14955,12 @@ fn checkisas(c: *checker, n: *node) void = { let utinfo: *tinfo = tinfofornode(c, u); let wanttinfo: *tinfo = tinfofornode(c, want); if (utinfo != nil) { if (wanttinfo != nil) { - if (flatvariantidxt(utinfo, wanttinfo) >= 0) { return; }; + // exact-only (#95-c3): is/as acceptance is nominal variant + // membership; the cgen tag-synthesis chain/structural arms must + // not widen acceptance here, nor surface the >=2 cgen fatal + // during check (rule-10, #107). casevariantin below keeps the + // #198 spread fallback. + if (flatvariantidxt(utinfo, wanttinfo, true) >= 0) { return; }; }; }; if (casevariantin(c, u, want, taggeddefmod(c, st))) { return; }; cerr("is/as: not a variant of operand"); @@ -18917,7 +18922,7 @@ fn taggedvariantindext(c: *cgen, du: *tinfo, rhs: *node) i32 = { ti = tichase(ti); if (ti == nil) { return -1; }; if (ti.kind != tykind.TY_TAGGED) { return -1; }; - let r: i32 = flatvariantidxt(ti, rhs.type_: *tinfo); + let r: i32 = flatvariantidxt(ti, rhs.type_: *tinfo, false); if (r >= 0) { return r; }; // Shape fallback: classify rhs as (str, slice, scalar/other) and // pick the first variant of matching shape. Covers LOOSE concrete @@ -18959,7 +18964,7 @@ fn taggedvariantindext(c: *cgen, du: *tinfo, rhs: *node) i32 = { fn flatvariantidx(c: *cgen, tagged: *node, pat: *node) i32 = { if (tagged == nil) { return -1; }; if (pat == nil) { return -1; }; - return flatvariantidxt(tagged.type_: *tinfo, pat.type_: *tinfo); + return flatvariantidxt(tagged.type_: *tinfo, pat.type_: *tinfo, false); }; // flatvariantidxt — tinfo-keyed core of flatvariantidx: flat 0-based @@ -18971,7 +18976,15 @@ fn flatvariantidx(c: *cgen, tagged: *node, pat: *node) i32 = { // all funnel through it. Mirrors cstage cg_tag_for_variant // (cmd/w6c/cgen.c:503) + cg_variant_match's both-NAMED ptr-id / typeeq // arm (:451). -fn flatvariantidxt(tagged: *tinfo, want: *tinfo) i32 = { +// exactonly (#95-c3): the is/as ACCEPTANCE gate (check.ww route) wants +// only nominal variant membership — pass 1. The chain/structural +// tag-synthesis arms below and their >=2 ambiguity os.exit belong to the +// cgen WIDEN consumer; routing the checker through them widened is/as +// acceptance (cs!=ww) and surfaced a cgen fatal mid-check (#107). Two +// consumers, two modes — not a wrapper. cstage has no twin: its is/as +// gate (check.c:2036) never calls cg_tag_for_variant, so cg_tag_for_variant +// stays full-only there. +fn flatvariantidxt(tagged: *tinfo, want: *tinfo, exactonly: bool) i32 = { if (want == nil) { return -1; }; let ti: *tinfo = tagged; ti = tichase(ti); @@ -18987,6 +19000,7 @@ fn flatvariantidxt(tagged: *tinfo, want: *tinfo) i32 = { p = p.tnext; idx += 1; }; + if (exactonly) { return -1; }; // Pass 1b (#95): NAMED source, no exact variant — chain membership. // An alias IS-A every type on its NAMED chain (ali2 is-a ali is-a // base), so declaring the variant as `ali` admits any source whose @@ -19283,7 +19297,7 @@ fn cgwidentagremap(c: *cgen, du: *tinfo, su: *tinfo, slot_off: i32) void = { let p: *tparam = st.params; let idx: i32 = 0; for (p != nil) { - let di: i32 = flatvariantidxt(dt, p.type_); + let di: i32 = flatvariantidxt(dt, p.type_, false); if (di < 0) { di = 0; }; if (di != idx) { identity = false; p = nil; } else { p = p.tnext; idx += 1; }; @@ -19297,7 +19311,7 @@ fn cgwidentagremap(c: *cgen, du: *tinfo, su: *tinfo, slot_off: i32) void = { idx = 0; for (p != nil) { let next: str = mklabel(c, "remap_next"); - let di: i32 = flatvariantidxt(dt, p.type_); + let di: i32 = flatvariantidxt(dt, p.type_, false); if (di < 0) { di = 0; }; emitline("\tCMPQ\t$"); emitint(idx: i64); @@ -19738,7 +19752,7 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *tinfo, src: *node, slot_off: i32, slot_s let srctagged: bool = (rhstaggedident(c, src) != nil) || rhstaggedabicall(c, src); if (srctagged) { - let nested: i32 = flatvariantidxt(dt, src.type_: *tinfo); + let nested: i32 = flatvariantidxt(dt, src.type_: *tinfo, false); if (nested >= 0) { // drew collision guard: the structural fallback over- // matches if ≥2 nominally-distinct dt variants share the @@ -20052,7 +20066,7 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *tinfo, src: *node, slot_off: i32, slot_s // str/slice shape fallback would silently pick tag 0 for an // unmatched tuple, diverging from cstage cg_tag_for_variant // (which returns -1) and masking the loud-stop below. - let ttag: i32 = flatvariantidxt(dt, src.type_: *tinfo); + let ttag: i32 = flatvariantidxt(dt, src.type_: *tinfo, false); // #242: an untyped/literal tuple element (`(true,7)`) leaves // the src tuple un-matchable, so the variant tag can't // resolve — the supported shape is a tuple of TYPED exprs @@ -21245,7 +21259,7 @@ fn cgtagvariantidx(c: *cgen, tagged: *node, vt: *node) i32 = { let sti: *tinfo = tagged.type_: *tinfo; sti = tichase(sti); if (sti != nil && sti.kind == tykind.TY_TAGGED && vt.type_ != nil) { - return flatvariantidxt(sti, vt.type_: *tinfo); + return flatvariantidxt(sti, vt.type_: *tinfo, false); }; return -1; }; @@ -21434,7 +21448,7 @@ fn cgtryprop(c: *cgen, n: *node) void = { let i: i32 = 0; for (p != nil) { if (p.iserror) { - let j: i32 = flatvariantidxt(r, p.type_); + let j: i32 = flatvariantidxt(r, p.type_, false); if (j < 0) { j = 0; }; if (j != i) { let skip: str = mklabel(c, "tryprop_skip"); @@ -23820,7 +23834,7 @@ fn cgmatch(c: *cgen, n: *node) void = { if (typeisslice(pattype)) { r = flatslicevariantidx(c, scrutt, pat.lhs); } else { - r = flatvariantidxt(scrutt.type_: *tinfo, pattype); + r = flatvariantidxt(scrutt.type_: *tinfo, pattype, false); }; }; if (r >= 0) { want = r; }; diff --git a/selfhost/cmd/wcc/cgenexpr.ww b/selfhost/cmd/wcc/cgenexpr.ww index 74369d59..06175ad1 100644 --- a/selfhost/cmd/wcc/cgenexpr.ww +++ b/selfhost/cmd/wcc/cgenexpr.ww @@ -146,7 +146,7 @@ fn cgtagvariantidx(c: *cgen, tagged: *node, vt: *node) i32 = { let sti: *tinfo = tagged.type_: *tinfo; sti = tichase(sti); if (sti != nil && sti.kind == tykind.TY_TAGGED && vt.type_ != nil) { - return flatvariantidxt(sti, vt.type_: *tinfo); + return flatvariantidxt(sti, vt.type_: *tinfo, false); }; return -1; }; @@ -335,7 +335,7 @@ fn cgtryprop(c: *cgen, n: *node) void = { let i: i32 = 0; for (p != nil) { if (p.iserror) { - let j: i32 = flatvariantidxt(r, p.type_); + let j: i32 = flatvariantidxt(r, p.type_, false); if (j < 0) { j = 0; }; if (j != i) { let skip: str = mklabel(c, "tryprop_skip"); @@ -2721,7 +2721,7 @@ fn cgmatch(c: *cgen, n: *node) void = { if (typeisslice(pattype)) { r = flatslicevariantidx(c, scrutt, pat.lhs); } else { - r = flatvariantidxt(scrutt.type_: *tinfo, pattype); + r = flatvariantidxt(scrutt.type_: *tinfo, pattype, false); }; }; if (r >= 0) { want = r; }; diff --git a/selfhost/cmd/wcc/cgenutil.ww b/selfhost/cmd/wcc/cgenutil.ww index 9be1a9a2..98c66edd 100644 --- a/selfhost/cmd/wcc/cgenutil.ww +++ b/selfhost/cmd/wcc/cgenutil.ww @@ -2813,7 +2813,7 @@ fn taggedvariantindext(c: *cgen, du: *tinfo, rhs: *node) i32 = { ti = tichase(ti); if (ti == nil) { return -1; }; if (ti.kind != tykind.TY_TAGGED) { return -1; }; - let r: i32 = flatvariantidxt(ti, rhs.type_: *tinfo); + let r: i32 = flatvariantidxt(ti, rhs.type_: *tinfo, false); if (r >= 0) { return r; }; // Shape fallback: classify rhs as (str, slice, scalar/other) and // pick the first variant of matching shape. Covers LOOSE concrete @@ -2855,7 +2855,7 @@ fn taggedvariantindext(c: *cgen, du: *tinfo, rhs: *node) i32 = { fn flatvariantidx(c: *cgen, tagged: *node, pat: *node) i32 = { if (tagged == nil) { return -1; }; if (pat == nil) { return -1; }; - return flatvariantidxt(tagged.type_: *tinfo, pat.type_: *tinfo); + return flatvariantidxt(tagged.type_: *tinfo, pat.type_: *tinfo, false); }; // flatvariantidxt — tinfo-keyed core of flatvariantidx: flat 0-based @@ -2867,7 +2867,15 @@ fn flatvariantidx(c: *cgen, tagged: *node, pat: *node) i32 = { // all funnel through it. Mirrors cstage cg_tag_for_variant // (cmd/w6c/cgen.c:503) + cg_variant_match's both-NAMED ptr-id / typeeq // arm (:451). -fn flatvariantidxt(tagged: *tinfo, want: *tinfo) i32 = { +// exactonly (#95-c3): the is/as ACCEPTANCE gate (check.ww route) wants +// only nominal variant membership — pass 1. The chain/structural +// tag-synthesis arms below and their >=2 ambiguity os.exit belong to the +// cgen WIDEN consumer; routing the checker through them widened is/as +// acceptance (cs!=ww) and surfaced a cgen fatal mid-check (#107). Two +// consumers, two modes — not a wrapper. cstage has no twin: its is/as +// gate (check.c:2036) never calls cg_tag_for_variant, so cg_tag_for_variant +// stays full-only there. +fn flatvariantidxt(tagged: *tinfo, want: *tinfo, exactonly: bool) i32 = { if (want == nil) { return -1; }; let ti: *tinfo = tagged; ti = tichase(ti); @@ -2883,6 +2891,7 @@ fn flatvariantidxt(tagged: *tinfo, want: *tinfo) i32 = { p = p.tnext; idx += 1; }; + if (exactonly) { return -1; }; // Pass 1b (#95): NAMED source, no exact variant — chain membership. // An alias IS-A every type on its NAMED chain (ali2 is-a ali is-a // base), so declaring the variant as `ali` admits any source whose @@ -3179,7 +3188,7 @@ fn cgwidentagremap(c: *cgen, du: *tinfo, su: *tinfo, slot_off: i32) void = { let p: *tparam = st.params; let idx: i32 = 0; for (p != nil) { - let di: i32 = flatvariantidxt(dt, p.type_); + let di: i32 = flatvariantidxt(dt, p.type_, false); if (di < 0) { di = 0; }; if (di != idx) { identity = false; p = nil; } else { p = p.tnext; idx += 1; }; @@ -3193,7 +3202,7 @@ fn cgwidentagremap(c: *cgen, du: *tinfo, su: *tinfo, slot_off: i32) void = { idx = 0; for (p != nil) { let next: str = mklabel(c, "remap_next"); - let di: i32 = flatvariantidxt(dt, p.type_); + let di: i32 = flatvariantidxt(dt, p.type_, false); if (di < 0) { di = 0; }; emitline("\tCMPQ\t$"); emitint(idx: i64); @@ -3634,7 +3643,7 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *tinfo, src: *node, slot_off: i32, slot_s let srctagged: bool = (rhstaggedident(c, src) != nil) || rhstaggedabicall(c, src); if (srctagged) { - let nested: i32 = flatvariantidxt(dt, src.type_: *tinfo); + let nested: i32 = flatvariantidxt(dt, src.type_: *tinfo, false); if (nested >= 0) { // drew collision guard: the structural fallback over- // matches if ≥2 nominally-distinct dt variants share the @@ -3948,7 +3957,7 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *tinfo, src: *node, slot_off: i32, slot_s // str/slice shape fallback would silently pick tag 0 for an // unmatched tuple, diverging from cstage cg_tag_for_variant // (which returns -1) and masking the loud-stop below. - let ttag: i32 = flatvariantidxt(dt, src.type_: *tinfo); + let ttag: i32 = flatvariantidxt(dt, src.type_: *tinfo, false); // #242: an untyped/literal tuple element (`(true,7)`) leaves // the src tuple un-matchable, so the variant tag can't // resolve — the supported shape is a tuple of TYPED exprs diff --git a/selfhost/cmd/wcc/check.ww b/selfhost/cmd/wcc/check.ww index d0660028..94e7bdfd 100644 --- a/selfhost/cmd/wcc/check.ww +++ b/selfhost/cmd/wcc/check.ww @@ -4674,7 +4674,12 @@ fn checkisas(c: *checker, n: *node) void = { let utinfo: *tinfo = tinfofornode(c, u); let wanttinfo: *tinfo = tinfofornode(c, want); if (utinfo != nil) { if (wanttinfo != nil) { - if (flatvariantidxt(utinfo, wanttinfo) >= 0) { return; }; + // exact-only (#95-c3): is/as acceptance is nominal variant + // membership; the cgen tag-synthesis chain/structural arms must + // not widen acceptance here, nor surface the >=2 cgen fatal + // during check (rule-10, #107). casevariantin below keeps the + // #198 spread fallback. + if (flatvariantidxt(utinfo, wanttinfo, true) >= 0) { return; }; }; }; if (casevariantin(c, u, want, taggeddefmod(c, st))) { return; }; cerr("is/as: not a variant of operand"); diff --git a/selfhost/cmd/wwdump/main.combined.ww b/selfhost/cmd/wwdump/main.combined.ww index 2df463f2..24d7c3c5 100644 --- a/selfhost/cmd/wwdump/main.combined.ww +++ b/selfhost/cmd/wwdump/main.combined.ww @@ -14955,7 +14955,12 @@ fn checkisas(c: *checker, n: *node) void = { let utinfo: *tinfo = tinfofornode(c, u); let wanttinfo: *tinfo = tinfofornode(c, want); if (utinfo != nil) { if (wanttinfo != nil) { - if (flatvariantidxt(utinfo, wanttinfo) >= 0) { return; }; + // exact-only (#95-c3): is/as acceptance is nominal variant + // membership; the cgen tag-synthesis chain/structural arms must + // not widen acceptance here, nor surface the >=2 cgen fatal + // during check (rule-10, #107). casevariantin below keeps the + // #198 spread fallback. + if (flatvariantidxt(utinfo, wanttinfo, true) >= 0) { return; }; }; }; if (casevariantin(c, u, want, taggeddefmod(c, st))) { return; }; cerr("is/as: not a variant of operand"); @@ -18917,7 +18922,7 @@ fn taggedvariantindext(c: *cgen, du: *tinfo, rhs: *node) i32 = { ti = tichase(ti); if (ti == nil) { return -1; }; if (ti.kind != tykind.TY_TAGGED) { return -1; }; - let r: i32 = flatvariantidxt(ti, rhs.type_: *tinfo); + let r: i32 = flatvariantidxt(ti, rhs.type_: *tinfo, false); if (r >= 0) { return r; }; // Shape fallback: classify rhs as (str, slice, scalar/other) and // pick the first variant of matching shape. Covers LOOSE concrete @@ -18959,7 +18964,7 @@ fn taggedvariantindext(c: *cgen, du: *tinfo, rhs: *node) i32 = { fn flatvariantidx(c: *cgen, tagged: *node, pat: *node) i32 = { if (tagged == nil) { return -1; }; if (pat == nil) { return -1; }; - return flatvariantidxt(tagged.type_: *tinfo, pat.type_: *tinfo); + return flatvariantidxt(tagged.type_: *tinfo, pat.type_: *tinfo, false); }; // flatvariantidxt — tinfo-keyed core of flatvariantidx: flat 0-based @@ -18971,7 +18976,15 @@ fn flatvariantidx(c: *cgen, tagged: *node, pat: *node) i32 = { // all funnel through it. Mirrors cstage cg_tag_for_variant // (cmd/w6c/cgen.c:503) + cg_variant_match's both-NAMED ptr-id / typeeq // arm (:451). -fn flatvariantidxt(tagged: *tinfo, want: *tinfo) i32 = { +// exactonly (#95-c3): the is/as ACCEPTANCE gate (check.ww route) wants +// only nominal variant membership — pass 1. The chain/structural +// tag-synthesis arms below and their >=2 ambiguity os.exit belong to the +// cgen WIDEN consumer; routing the checker through them widened is/as +// acceptance (cs!=ww) and surfaced a cgen fatal mid-check (#107). Two +// consumers, two modes — not a wrapper. cstage has no twin: its is/as +// gate (check.c:2036) never calls cg_tag_for_variant, so cg_tag_for_variant +// stays full-only there. +fn flatvariantidxt(tagged: *tinfo, want: *tinfo, exactonly: bool) i32 = { if (want == nil) { return -1; }; let ti: *tinfo = tagged; ti = tichase(ti); @@ -18987,6 +19000,7 @@ fn flatvariantidxt(tagged: *tinfo, want: *tinfo) i32 = { p = p.tnext; idx += 1; }; + if (exactonly) { return -1; }; // Pass 1b (#95): NAMED source, no exact variant — chain membership. // An alias IS-A every type on its NAMED chain (ali2 is-a ali is-a // base), so declaring the variant as `ali` admits any source whose @@ -19283,7 +19297,7 @@ fn cgwidentagremap(c: *cgen, du: *tinfo, su: *tinfo, slot_off: i32) void = { let p: *tparam = st.params; let idx: i32 = 0; for (p != nil) { - let di: i32 = flatvariantidxt(dt, p.type_); + let di: i32 = flatvariantidxt(dt, p.type_, false); if (di < 0) { di = 0; }; if (di != idx) { identity = false; p = nil; } else { p = p.tnext; idx += 1; }; @@ -19297,7 +19311,7 @@ fn cgwidentagremap(c: *cgen, du: *tinfo, su: *tinfo, slot_off: i32) void = { idx = 0; for (p != nil) { let next: str = mklabel(c, "remap_next"); - let di: i32 = flatvariantidxt(dt, p.type_); + let di: i32 = flatvariantidxt(dt, p.type_, false); if (di < 0) { di = 0; }; emitline("\tCMPQ\t$"); emitint(idx: i64); @@ -19738,7 +19752,7 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *tinfo, src: *node, slot_off: i32, slot_s let srctagged: bool = (rhstaggedident(c, src) != nil) || rhstaggedabicall(c, src); if (srctagged) { - let nested: i32 = flatvariantidxt(dt, src.type_: *tinfo); + let nested: i32 = flatvariantidxt(dt, src.type_: *tinfo, false); if (nested >= 0) { // drew collision guard: the structural fallback over- // matches if ≥2 nominally-distinct dt variants share the @@ -20052,7 +20066,7 @@ fn cgwidentaggedstorebp(c: *cgen, dst: *tinfo, src: *node, slot_off: i32, slot_s // str/slice shape fallback would silently pick tag 0 for an // unmatched tuple, diverging from cstage cg_tag_for_variant // (which returns -1) and masking the loud-stop below. - let ttag: i32 = flatvariantidxt(dt, src.type_: *tinfo); + let ttag: i32 = flatvariantidxt(dt, src.type_: *tinfo, false); // #242: an untyped/literal tuple element (`(true,7)`) leaves // the src tuple un-matchable, so the variant tag can't // resolve — the supported shape is a tuple of TYPED exprs @@ -21245,7 +21259,7 @@ fn cgtagvariantidx(c: *cgen, tagged: *node, vt: *node) i32 = { let sti: *tinfo = tagged.type_: *tinfo; sti = tichase(sti); if (sti != nil && sti.kind == tykind.TY_TAGGED && vt.type_ != nil) { - return flatvariantidxt(sti, vt.type_: *tinfo); + return flatvariantidxt(sti, vt.type_: *tinfo, false); }; return -1; }; @@ -21434,7 +21448,7 @@ fn cgtryprop(c: *cgen, n: *node) void = { let i: i32 = 0; for (p != nil) { if (p.iserror) { - let j: i32 = flatvariantidxt(r, p.type_); + let j: i32 = flatvariantidxt(r, p.type_, false); if (j < 0) { j = 0; }; if (j != i) { let skip: str = mklabel(c, "tryprop_skip"); @@ -23820,7 +23834,7 @@ fn cgmatch(c: *cgen, n: *node) void = { if (typeisslice(pattype)) { r = flatslicevariantidx(c, scrutt, pat.lhs); } else { - r = flatvariantidxt(scrutt.type_: *tinfo, pattype); + r = flatvariantidxt(scrutt.type_: *tinfo, pattype, false); }; }; if (r >= 0) { want = r; }; diff --git a/test/wcc/944_variant_chain_b95_run.c b/test/wcc/944_variant_chain_b95_run.c index d31a5945..feda503d 100644 --- a/test/wcc/944_variant_chain_b95_run.c +++ b/test/wcc/944_variant_chain_b95_run.c @@ -373,6 +373,62 @@ static const struct row rows[] = { " if (!(v is tb)) { return 1; };\n" " return 0;\n" "};\n", 0, 0, K_RUN, NULL }, + /* ---- c3 (#107): is/as ACCEPTANCE must stay nominal-exact. cgen's + * chain/structural tag-synthesis arms widen tag lookup at the WIDEN + * site only; the wwstage is/as gate (check.ww:4677) reuses + * flatvariantidxt and, pre-c3, the widening leaked into ACCEPTANCE + * (cs!=ww) and surfaced the >=2 cgen fatal mid-check. c3 added an + * exact-only mode for that caller. These pin BOTH stages REJECTING + * (cstage's independent gate check.c:2036 already does; wwstage now + * matches it) — NOT a crash for the ambiguity shape. Shared experr + * substring "not a variant" (cstage "X is not a variant of (...)", + * wwstage "is/as: not a variant of operand (X)"). */ + { "isas_chain_reject", + "package main;\n" + "type base = struct { a: int, b: int };\n" + "type ali = base;\n" + "fn f(v: (void | ali)) i32 = {\n" + " if (v is base) { return 1; };\n" + " return 0;\n" + "};\n" + "export fn main() i32 = { return 0; };\n", + 0, 0, K_BUILDERR, "not a variant" }, + { "isas_unrel_reject", + "package main;\n" + "type ta = struct { a: int, b: int };\n" + "type tb = struct { a: int, b: int };\n" + "fn f(v: (void | tb)) i32 = {\n" + " if (v is ta) { return 1; };\n" + " return 0;\n" + "};\n" + "export fn main() i32 = { return 0; };\n", + 0, 0, K_BUILDERR, "not a variant" }, + { "isas_amb_reject_notcrash", + "package main;\n" + "type base = struct { a: int, b: int };\n" + "type ali = base;\n" + "type ali2 = ali;\n" + "fn f(v: (ali | ali2)) i32 = {\n" + " if (v is base) { return 1; };\n" + " return 0;\n" + "};\n" + "export fn main() i32 = { return 0; };\n", + 0, 0, K_BUILDERR, "not a variant" }, + /* ---- rob's obligated mixed prim/alias TWIN: union (int | ai) + * ai=int, source typed aj=int — under all-variants counting both the + * bare `int` variant and `ai` share the int bottom -> the cgen widen + * (full mode) hard-errors (harec nassign>=2 -> NULL). Pinned LOUD + * BOTH stages (the chain-ambiguity path; #95 deviation-2 ratified). */ + { "twin_prim_alias_amb", + "package main;\n" + "type ai = int;\n" + "type aj = int;\n" + "export fn main() i32 = {\n" + " let s: aj = 7;\n" + " let v: (int | ai) = s;\n" + " return 0;\n" + "};\n", 0, 0, K_BUILDERR, + "source alias chain reaches >=2 variants" }, }; static int