Don't special-case class instances in binary expression inference (#15161)

Just like in #15045 for unary expressions: In binary expressions, we
were only looking for dunder expressions for `Type::Instance` types. We
had some special cases for coercing the various `Literal` types into
their corresponding `Instance` types before doing the lookup. But we can
side-step all of that by using the existing `Type::to_meta_type` and
`Type::to_instance` methods.
This commit is contained in:
Douglas Creager 2025-01-06 13:50:20 -05:00 committed by GitHub
parent d45c1ee44f
commit 5e9259c96c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 555 additions and 60 deletions

View file

@ -3398,6 +3398,8 @@ impl<'db> TypeInferenceBuilder<'db> {
// When interacting with Todo, Any and Unknown should propagate (as if we fix this
// `Todo` in the future, the result would then become Any or Unknown, respectively.)
(Type::Any, _, _) | (_, Type::Any, _) => Some(Type::Any),
(todo @ Type::Todo(_), _, _) | (_, todo @ Type::Todo(_), _) => Some(todo),
(Type::Never, _, _) | (_, Type::Never, _) => Some(Type::Never),
(Type::Unknown, _, _) | (_, Type::Unknown, _) => Some(Type::Unknown),
(Type::IntLiteral(n), Type::IntLiteral(m), ast::Operator::Add) => Some(
@ -3496,54 +3498,78 @@ impl<'db> TypeInferenceBuilder<'db> {
Some(ty)
}
(Type::Instance(_), Type::IntLiteral(_), op) => self.infer_binary_expression_type(
left_ty,
KnownClass::Int.to_instance(self.db()),
(Type::BooleanLiteral(b1), Type::BooleanLiteral(b2), ast::Operator::BitOr) => {
Some(Type::BooleanLiteral(b1 | b2))
}
(Type::BooleanLiteral(bool_value), right, op) => self.infer_binary_expression_type(
Type::IntLiteral(i64::from(bool_value)),
right,
op,
),
(left, Type::BooleanLiteral(bool_value), op) => {
self.infer_binary_expression_type(left, Type::IntLiteral(i64::from(bool_value)), op)
}
(Type::IntLiteral(_), Type::Instance(_), op) => self.infer_binary_expression_type(
KnownClass::Int.to_instance(self.db()),
right_ty,
// We've handled all of the special cases that we support for literals, so we need to
// fall back on looking for dunder methods on one of the operand types.
(
Type::FunctionLiteral(_)
| Type::ModuleLiteral(_)
| Type::ClassLiteral(_)
| Type::SubclassOf(_)
| Type::Instance(_)
| Type::KnownInstance(_)
| Type::Union(_)
| Type::Intersection(_)
| Type::AlwaysTruthy
| Type::AlwaysFalsy
| Type::IntLiteral(_)
| Type::StringLiteral(_)
| Type::LiteralString
| Type::BytesLiteral(_)
| Type::SliceLiteral(_)
| Type::Tuple(_),
Type::FunctionLiteral(_)
| Type::ModuleLiteral(_)
| Type::ClassLiteral(_)
| Type::SubclassOf(_)
| Type::Instance(_)
| Type::KnownInstance(_)
| Type::Union(_)
| Type::Intersection(_)
| Type::AlwaysTruthy
| Type::AlwaysFalsy
| Type::IntLiteral(_)
| Type::StringLiteral(_)
| Type::LiteralString
| Type::BytesLiteral(_)
| Type::SliceLiteral(_)
| Type::Tuple(_),
op,
),
) => {
// We either want to call lhs.__op__ or rhs.__rop__. The full decision tree from
// the Python spec [1] is:
//
// - If rhs is a (proper) subclass of lhs, and it provides a different
// implementation of __rop__, use that.
// - Otherwise, if lhs implements __op__, use that.
// - Otherwise, if lhs and rhs are different types, and rhs implements __rop__,
// use that.
//
// [1] https://docs.python.org/3/reference/datamodel.html#object.__radd__
(Type::Instance(_), Type::Tuple(_), op) => self.infer_binary_expression_type(
left_ty,
KnownClass::Tuple.to_instance(self.db()),
op,
),
(Type::Tuple(_), Type::Instance(_), op) => self.infer_binary_expression_type(
KnownClass::Tuple.to_instance(self.db()),
right_ty,
op,
),
(Type::Instance(_), Type::StringLiteral(_) | Type::LiteralString, op) => self
.infer_binary_expression_type(left_ty, KnownClass::Str.to_instance(self.db()), op),
(Type::StringLiteral(_) | Type::LiteralString, Type::Instance(_), op) => self
.infer_binary_expression_type(KnownClass::Str.to_instance(self.db()), right_ty, op),
(Type::Instance(_), Type::BytesLiteral(_), op) => self.infer_binary_expression_type(
left_ty,
KnownClass::Bytes.to_instance(self.db()),
op,
),
(Type::BytesLiteral(_), Type::Instance(_), op) => self.infer_binary_expression_type(
KnownClass::Bytes.to_instance(self.db()),
right_ty,
op,
),
(Type::Instance(left), Type::Instance(right), op) => {
if left != right && right.is_subtype_of(self.db(), left) {
// Technically we don't have to check left_ty != right_ty here, since if the types
// are the same, they will trivially have the same implementation of the reflected
// dunder, and so we'll fail the inner check. But the type equality check will be
// faster for the common case, and allow us to skip the (two) class member lookups.
let left_class = left_ty.to_meta_type(self.db());
let right_class = right_ty.to_meta_type(self.db());
if left_ty != right_ty && right_ty.is_subtype_of(self.db(), left_ty) {
let reflected_dunder = op.reflected_dunder();
let rhs_reflected = right.class.class_member(self.db(), reflected_dunder);
let rhs_reflected = right_class.member(self.db(), reflected_dunder);
if !rhs_reflected.is_unbound()
&& rhs_reflected != left.class.class_member(self.db(), reflected_dunder)
&& rhs_reflected != left_class.member(self.db(), reflected_dunder)
{
return right_ty
.call_dunder(self.db(), reflected_dunder, &[right_ty, left_ty])
@ -3557,7 +3583,7 @@ impl<'db> TypeInferenceBuilder<'db> {
}
let call_on_left_instance = if let Symbol::Type(class_member, _) =
left.class.class_member(self.db(), op.dunder())
left_class.member(self.db(), op.dunder())
{
class_member
.call(self.db(), &[left_ty, right_ty])
@ -3567,11 +3593,11 @@ impl<'db> TypeInferenceBuilder<'db> {
};
call_on_left_instance.or_else(|| {
if left == right {
if left_ty == right_ty {
None
} else {
if let Symbol::Type(class_member, _) =
right.class.class_member(self.db(), op.reflected_dunder())
right_class.member(self.db(), op.reflected_dunder())
{
class_member
.call(self.db(), &[right_ty, left_ty])
@ -3582,20 +3608,6 @@ impl<'db> TypeInferenceBuilder<'db> {
}
})
}
(Type::BooleanLiteral(b1), Type::BooleanLiteral(b2), ast::Operator::BitOr) => {
Some(Type::BooleanLiteral(b1 | b2))
}
(Type::BooleanLiteral(bool_value), right, op) => self.infer_binary_expression_type(
Type::IntLiteral(i64::from(bool_value)),
right,
op,
),
(left, Type::BooleanLiteral(bool_value), op) => {
self.infer_binary_expression_type(left, Type::IntLiteral(i64::from(bool_value)), op)
}
_ => Some(todo_type!("Support for more binary expressions")),
}
}