Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 36 additions & 58 deletions pyrefly/lib/solver/subset.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1867,52 +1867,6 @@ impl<'solver, 'subset, Ans: LookupAnswer> Subset<'solver, 'subset, Ans> {
{
Ok(())
}
// Given `A | B <: C | D` we must always split the LHS first, but a quantified might be hiding a LHS union in its bounds.
// Given (Quantified(bounds = A | B), A | B), we need to examine the bound _before_ splitting up the RHS union.
// But given (T@Quantified(bounds = ...), T | Something), we need to split the union.
// Therefore try these quantified cases, but only pick them if they work.
(Type::Quantified(q), u)
if let Restriction::Bound(bound) = q.restriction()
// A bare inference variable can preserve the quantified type itself. Expanding
// it to its bound here would make inference depend on which argument is checked
// first (https://github.com/facebook/pyrefly/issues/4187).
&& !matches!(u, Type::Union(union) if union.members.iter().any(|t| matches!(t, Type::Var(_))))
&& self
.solver
.with_snapshot(&u.collect_maybe_placeholder_vars(), || {
self.is_subset_eq(bound, u)
})
.is_ok() =>
{
Ok(())
}
(Type::Quantified(q), u)
if let Restriction::ShapeExtension(extension) = q.restriction()
&& self
.solver
.with_snapshot(&u.collect_maybe_placeholder_vars(), || {
self.is_subset_eq(
&extension.upper_bound(self.type_order.stdlib(), &self.solver.heap),
u,
)
})
.is_ok() =>
{
Ok(())
}
(Type::Quantified(q), u)
if let Restriction::Constraints(constraints) = q.restriction()
&& self
.solver
.with_snapshot(&u.collect_maybe_placeholder_vars(), || {
all(constraints.iter(), |constraint| {
self.is_subset_eq(constraint, u)
})
})
.is_ok() =>
{
Ok(())
}
(Type::Quantified(q), u @ Type::Tuple(_)) if q.is_type_var_tuple() => self
.is_subset_eq(
&self.solver.heap.mk_unbounded_tuple(
Expand Down Expand Up @@ -2084,6 +2038,25 @@ impl<'solver, 'subset, Ans: LookupAnswer> Subset<'solver, 'subset, Ans> {
all(members.iter(), |m| {
self.is_subset_eq(&Type::type_of(m.clone()), want)
})
} else if let Type::Quantified(q) = l {
// A quantified type parameter may hide a union in its bound or constraints
// (e.g. `T: (A, B)` or `T: A | B`). If per-member matching against the RHS
// union failed, check whether the bound or all constraints as a whole satisfy
// the RHS union.
match q.restriction() {
Restriction::Bound(bound) => self.is_subset_eq(bound, want),
Restriction::Constraints(constraints) => {
all(constraints.iter(), |constraint| {
self.is_subset_eq(constraint, want)
})
}
Restriction::ShapeExtension(extension) => {
let upper =
extension.upper_bound(self.type_order.stdlib(), &self.solver.heap);
self.is_subset_eq(&upper, want)
}
Restriction::Unrestricted => Err(error.unwrap_or(SubsetError::Other)),
}
} else {
Err(error.unwrap_or(SubsetError::Other))
}
Expand All @@ -2098,13 +2071,21 @@ impl<'solver, 'subset, Ans: LookupAnswer> Subset<'solver, 'subset, Ans> {
_ => result,
}
}
(Type::Quantified(q), u) if !q.restriction().is_restricted() => self.is_subset_eq(
&self
.solver
.heap
.mk_class_type(self.type_order.stdlib().object().clone()),
u,
),
(Type::Quantified(q), u) => match q.restriction() {
Restriction::Bound(bound) => self.is_subset_eq(bound, u),
Restriction::Constraints(constraints) => all(constraints.iter(), |constraint| {
self.is_subset_eq(constraint, u)
}),
Restriction::ShapeExtension(extension) => {
let upper = extension.upper_bound(self.type_order.stdlib(), &self.solver.heap);
self.is_subset_eq(&upper, u)
}
Restriction::Unrestricted => {
let upper = q.upper_bound(self.type_order.stdlib(), &self.solver.heap);
self.is_subset_eq(&upper, u)
}
},

(Type::Module(_), Type::ClassType(cls)) if cls.has_qname("types", "ModuleType") => {
Ok(())
}
Expand Down Expand Up @@ -2652,12 +2633,9 @@ impl<'solver, 'subset, Ans: LookupAnswer> Subset<'solver, 'subset, Ans> {
{
self.is_subset_literal_int_size(n, got, false)
}
(Type::Int(_) | Type::Quantified(_), Type::ClassType(cls))
if is_int_class_type(cls) =>
{
Ok(())
}
(Type::Int(_), Type::ClassType(cls)) if is_int_class_type(cls) => Ok(()),
(Type::QuantifiedValue(_), Type::ClassType(cls)) if is_int_class_type(cls) => Ok(()),

(Type::Literal(l_lit), Type::Literal(u_lit)) => {
ok_or(l_lit.value == u_lit.value, SubsetError::Other)
}
Expand Down
35 changes: 35 additions & 0 deletions pyrefly/lib/test/class_overrides.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2242,3 +2242,38 @@ class D(C):
from stub import C, D
"#,
);

testcase!(
test_override_module_level_typevar,
r#"
from typing import TypeVar
from typing_extensions import override

T = TypeVar("T", bound=int)
TConstrained = TypeVar("TConstrained", int, str)

class Base:
def method(self, x: T | None = None) -> T: ...
def method_no_opt(self, x: T) -> T: ...
def method_constrained(self, x: TConstrained | None = None) -> TConstrained: ...

class Derived(Base):
@override
def method(self, x: T | None = None) -> T:
raise NotImplementedError
@override
def method_no_opt(self, x: T) -> T:
raise NotImplementedError
@override
def method_constrained(self, x: TConstrained | None = None) -> TConstrained:
raise NotImplementedError

class BasePep:
def method[T: int](self, x: T | None = None) -> T: ...

class DerivedPep(BasePep):
@override
def method[T: int](self, x: T | None = None) -> T:
raise NotImplementedError
"#,
);
Loading