diff --git a/pyrefly/lib/solver/subset.rs b/pyrefly/lib/solver/subset.rs index 2f072fb5ec..f6a22c224b 100644 --- a/pyrefly/lib/solver/subset.rs +++ b/pyrefly/lib/solver/subset.rs @@ -1268,9 +1268,23 @@ impl<'a, Ans: LookupAnswer> Subset<'a, Ans> { } (Type::Quantified(q), u) if let Restriction::Constraints(constraints) = q.restriction() - && constraints - .iter() - .all(|constraint| self.is_subset_eq(constraint, u).is_ok()) => + && constraints.iter().all(|constraint| { + // When u is a union containing solver variables, check each + // constraint only against concrete (non-var) members. The + // `.all()` iterator can partially pin vars: e.g. for AnyStr + // <: Var(T) | None, `str <: Var(T)` pins T=str, then `bytes + // <: str|None` fails, leaving T irreversibly pinned to str. + if let Type::Union(box Union { members, .. }) = u + && u.may_contain_quantified_var() + { + members + .iter() + .filter(|m| !m.may_contain_quantified_var()) + .any(|m| self.is_subset_eq(constraint, m).is_ok()) + } else { + self.is_subset_eq(constraint, u).is_ok() + } + }) => { Ok(()) } diff --git a/pyrefly/lib/test/generic_restrictions.rs b/pyrefly/lib/test/generic_restrictions.rs index 4c9591f131..52a831135e 100644 --- a/pyrefly/lib/test/generic_restrictions.rs +++ b/pyrefly/lib/test/generic_restrictions.rs @@ -1213,7 +1213,6 @@ assert_type(f(b"hi"), bytes) ); testcase!( - bug = "Should not produce a type error; https://github.com/facebook/pyrefly/issues/2644", test_anystr_none_passthrough_classmethod, r#" from typing import AnyStr @@ -1223,7 +1222,7 @@ class A: def create(cls, x: AnyStr | None): ... def test(x: AnyStr | None): - A.create(x) # E: Argument `AnyStr | None` is not assignable to parameter `x` with type `str | None` in function `A.create` + A.create(x) "#, );