diff --git a/crates/red_knot_python_semantic/resources/mdtest/binary/booleans.md b/crates/red_knot_python_semantic/resources/mdtest/binary/booleans.md index 7184c1a631b4d..916e547751861 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/binary/booleans.md +++ b/crates/red_knot_python_semantic/resources/mdtest/binary/booleans.md @@ -46,3 +46,50 @@ reveal_type(a | b) # revealed: Literal[True] reveal_type(b | a) # revealed: Literal[True] reveal_type(b | b) # revealed: Literal[False] ``` + +## Arithmetic with a variable + +```py +a = True +b = False + +def lhs_is_int(x: int): + reveal_type(x + a) # revealed: int + reveal_type(x - a) # revealed: int + reveal_type(x * a) # revealed: int + reveal_type(x // a) # revealed: int + reveal_type(x / a) # revealed: float + reveal_type(x % a) # revealed: int + +def rhs_is_int(x: int): + reveal_type(a + x) # revealed: int + reveal_type(a - x) # revealed: int + reveal_type(a * x) # revealed: int + reveal_type(a // x) # revealed: int + reveal_type(a / x) # revealed: float + reveal_type(a % x) # revealed: int + +def lhs_is_bool(x: bool): + reveal_type(x + a) # revealed: int + reveal_type(x - a) # revealed: int + reveal_type(x * a) # revealed: int + reveal_type(x // a) # revealed: int + reveal_type(x / a) # revealed: float + reveal_type(x % a) # revealed: int + +def rhs_is_bool(x: bool): + reveal_type(a + x) # revealed: int + reveal_type(a - x) # revealed: int + reveal_type(a * x) # revealed: int + reveal_type(a // x) # revealed: int + reveal_type(a / x) # revealed: float + reveal_type(a % x) # revealed: int + +def both_are_bool(x: bool, y: bool): + reveal_type(x + y) # revealed: int + reveal_type(x - y) # revealed: int + reveal_type(x * y) # revealed: int + reveal_type(x // y) # revealed: int + reveal_type(x / y) # revealed: float + reveal_type(x % y) # revealed: int +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/binary/classes.md b/crates/red_knot_python_semantic/resources/mdtest/binary/classes.md new file mode 100644 index 0000000000000..464b2d3e7db92 --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/binary/classes.md @@ -0,0 +1,27 @@ +# Binary operations on classes + +## Union of two classes + +Unioning two classes via the `|` operator is only available in Python 3.10 and later. + +```toml +[environment] +python-version = "3.10" +``` + +```py +class A: ... +class B: ... + +reveal_type(A | B) # revealed: UnionType +``` + +## Union of two classes (prior to 3.10) + +```py +class A: ... +class B: ... + +# error: "Operator `|` is unsupported between objects of type `Literal[A]` and `Literal[B]`" +reveal_type(A | B) # revealed: Unknown +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/binary/custom.md b/crates/red_knot_python_semantic/resources/mdtest/binary/custom.md new file mode 100644 index 0000000000000..70b50f17cefdd --- /dev/null +++ b/crates/red_knot_python_semantic/resources/mdtest/binary/custom.md @@ -0,0 +1,371 @@ +# Custom binary operations + +## Class instances + +```py +class Yes: + def __add__(self, other) -> Literal["+"]: + return "+" + + def __sub__(self, other) -> Literal["-"]: + return "-" + + def __mul__(self, other) -> Literal["*"]: + return "*" + + def __matmul__(self, other) -> Literal["@"]: + return "@" + + def __truediv__(self, other) -> Literal["/"]: + return "/" + + def __mod__(self, other) -> Literal["%"]: + return "%" + + def __pow__(self, other) -> Literal["**"]: + return "**" + + def __lshift__(self, other) -> Literal["<<"]: + return "<<" + + def __rshift__(self, other) -> Literal[">>"]: + return ">>" + + def __or__(self, other) -> Literal["|"]: + return "|" + + def __xor__(self, other) -> Literal["^"]: + return "^" + + def __and__(self, other) -> Literal["&"]: + return "&" + + def __floordiv__(self, other) -> Literal["//"]: + return "//" + +class Sub(Yes): ... +class No: ... + +# Yes implements all of the dunder methods. +reveal_type(Yes() + Yes()) # revealed: Literal["+"] +reveal_type(Yes() - Yes()) # revealed: Literal["-"] +reveal_type(Yes() * Yes()) # revealed: Literal["*"] +reveal_type(Yes() @ Yes()) # revealed: Literal["@"] +reveal_type(Yes() / Yes()) # revealed: Literal["/"] +reveal_type(Yes() % Yes()) # revealed: Literal["%"] +reveal_type(Yes() ** Yes()) # revealed: Literal["**"] +reveal_type(Yes() << Yes()) # revealed: Literal["<<"] +reveal_type(Yes() >> Yes()) # revealed: Literal[">>"] +reveal_type(Yes() | Yes()) # revealed: Literal["|"] +reveal_type(Yes() ^ Yes()) # revealed: Literal["^"] +reveal_type(Yes() & Yes()) # revealed: Literal["&"] +reveal_type(Yes() // Yes()) # revealed: Literal["//"] + +# Sub inherits Yes's implementation of the dunder methods. +reveal_type(Sub() + Sub()) # revealed: Literal["+"] +reveal_type(Sub() - Sub()) # revealed: Literal["-"] +reveal_type(Sub() * Sub()) # revealed: Literal["*"] +reveal_type(Sub() @ Sub()) # revealed: Literal["@"] +reveal_type(Sub() / Sub()) # revealed: Literal["/"] +reveal_type(Sub() % Sub()) # revealed: Literal["%"] +reveal_type(Sub() ** Sub()) # revealed: Literal["**"] +reveal_type(Sub() << Sub()) # revealed: Literal["<<"] +reveal_type(Sub() >> Sub()) # revealed: Literal[">>"] +reveal_type(Sub() | Sub()) # revealed: Literal["|"] +reveal_type(Sub() ^ Sub()) # revealed: Literal["^"] +reveal_type(Sub() & Sub()) # revealed: Literal["&"] +reveal_type(Sub() // Sub()) # revealed: Literal["//"] + +# No does not implement any of the dunder methods. +# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `No` and `No`" +reveal_type(No() + No()) # revealed: Unknown +# error: [unsupported-operator] "Operator `-` is unsupported between objects of type `No` and `No`" +reveal_type(No() - No()) # revealed: Unknown +# error: [unsupported-operator] "Operator `*` is unsupported between objects of type `No` and `No`" +reveal_type(No() * No()) # revealed: Unknown +# error: [unsupported-operator] "Operator `@` is unsupported between objects of type `No` and `No`" +reveal_type(No() @ No()) # revealed: Unknown +# error: [unsupported-operator] "Operator `/` is unsupported between objects of type `No` and `No`" +reveal_type(No() / No()) # revealed: Unknown +# error: [unsupported-operator] "Operator `%` is unsupported between objects of type `No` and `No`" +reveal_type(No() % No()) # revealed: Unknown +# error: [unsupported-operator] "Operator `**` is unsupported between objects of type `No` and `No`" +reveal_type(No() ** No()) # revealed: Unknown +# error: [unsupported-operator] "Operator `<<` is unsupported between objects of type `No` and `No`" +reveal_type(No() << No()) # revealed: Unknown +# error: [unsupported-operator] "Operator `>>` is unsupported between objects of type `No` and `No`" +reveal_type(No() >> No()) # revealed: Unknown +# error: [unsupported-operator] "Operator `|` is unsupported between objects of type `No` and `No`" +reveal_type(No() | No()) # revealed: Unknown +# error: [unsupported-operator] "Operator `^` is unsupported between objects of type `No` and `No`" +reveal_type(No() ^ No()) # revealed: Unknown +# error: [unsupported-operator] "Operator `&` is unsupported between objects of type `No` and `No`" +reveal_type(No() & No()) # revealed: Unknown +# error: [unsupported-operator] "Operator `//` is unsupported between objects of type `No` and `No`" +reveal_type(No() // No()) # revealed: Unknown + +# Yes does not implement any of the reflected dunder methods. +# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `No` and `Yes`" +reveal_type(No() + Yes()) # revealed: Unknown +# error: [unsupported-operator] "Operator `-` is unsupported between objects of type `No` and `Yes`" +reveal_type(No() - Yes()) # revealed: Unknown +# error: [unsupported-operator] "Operator `*` is unsupported between objects of type `No` and `Yes`" +reveal_type(No() * Yes()) # revealed: Unknown +# error: [unsupported-operator] "Operator `@` is unsupported between objects of type `No` and `Yes`" +reveal_type(No() @ Yes()) # revealed: Unknown +# error: [unsupported-operator] "Operator `/` is unsupported between objects of type `No` and `Yes`" +reveal_type(No() / Yes()) # revealed: Unknown +# error: [unsupported-operator] "Operator `%` is unsupported between objects of type `No` and `Yes`" +reveal_type(No() % Yes()) # revealed: Unknown +# error: [unsupported-operator] "Operator `**` is unsupported between objects of type `No` and `Yes`" +reveal_type(No() ** Yes()) # revealed: Unknown +# error: [unsupported-operator] "Operator `<<` is unsupported between objects of type `No` and `Yes`" +reveal_type(No() << Yes()) # revealed: Unknown +# error: [unsupported-operator] "Operator `>>` is unsupported between objects of type `No` and `Yes`" +reveal_type(No() >> Yes()) # revealed: Unknown +# error: [unsupported-operator] "Operator `|` is unsupported between objects of type `No` and `Yes`" +reveal_type(No() | Yes()) # revealed: Unknown +# error: [unsupported-operator] "Operator `^` is unsupported between objects of type `No` and `Yes`" +reveal_type(No() ^ Yes()) # revealed: Unknown +# error: [unsupported-operator] "Operator `&` is unsupported between objects of type `No` and `Yes`" +reveal_type(No() & Yes()) # revealed: Unknown +# error: [unsupported-operator] "Operator `//` is unsupported between objects of type `No` and `Yes`" +reveal_type(No() // Yes()) # revealed: Unknown +``` + +## Subclass reflections override superclass dunders + +```py +class Yes: + def __add__(self, other) -> Literal["+"]: + return "+" + + def __sub__(self, other) -> Literal["-"]: + return "-" + + def __mul__(self, other) -> Literal["*"]: + return "*" + + def __matmul__(self, other) -> Literal["@"]: + return "@" + + def __truediv__(self, other) -> Literal["/"]: + return "/" + + def __mod__(self, other) -> Literal["%"]: + return "%" + + def __pow__(self, other) -> Literal["**"]: + return "**" + + def __lshift__(self, other) -> Literal["<<"]: + return "<<" + + def __rshift__(self, other) -> Literal[">>"]: + return ">>" + + def __or__(self, other) -> Literal["|"]: + return "|" + + def __xor__(self, other) -> Literal["^"]: + return "^" + + def __and__(self, other) -> Literal["&"]: + return "&" + + def __floordiv__(self, other) -> Literal["//"]: + return "//" + +class Sub(Yes): + def __radd__(self, other) -> Literal["r+"]: + return "r+" + + def __rsub__(self, other) -> Literal["r-"]: + return "r-" + + def __rmul__(self, other) -> Literal["r*"]: + return "r*" + + def __rmatmul__(self, other) -> Literal["r@"]: + return "r@" + + def __rtruediv__(self, other) -> Literal["r/"]: + return "r/" + + def __rmod__(self, other) -> Literal["r%"]: + return "r%" + + def __rpow__(self, other) -> Literal["r**"]: + return "r**" + + def __rlshift__(self, other) -> Literal["r<<"]: + return "r<<" + + def __rrshift__(self, other) -> Literal["r>>"]: + return "r>>" + + def __ror__(self, other) -> Literal["r|"]: + return "r|" + + def __rxor__(self, other) -> Literal["r^"]: + return "r^" + + def __rand__(self, other) -> Literal["r&"]: + return "r&" + + def __rfloordiv__(self, other) -> Literal["r//"]: + return "r//" + +class No: + def __radd__(self, other) -> Literal["r+"]: + return "r+" + + def __rsub__(self, other) -> Literal["r-"]: + return "r-" + + def __rmul__(self, other) -> Literal["r*"]: + return "r*" + + def __rmatmul__(self, other) -> Literal["r@"]: + return "r@" + + def __rtruediv__(self, other) -> Literal["r/"]: + return "r/" + + def __rmod__(self, other) -> Literal["r%"]: + return "r%" + + def __rpow__(self, other) -> Literal["r**"]: + return "r**" + + def __rlshift__(self, other) -> Literal["r<<"]: + return "r<<" + + def __rrshift__(self, other) -> Literal["r>>"]: + return "r>>" + + def __ror__(self, other) -> Literal["r|"]: + return "r|" + + def __rxor__(self, other) -> Literal["r^"]: + return "r^" + + def __rand__(self, other) -> Literal["r&"]: + return "r&" + + def __rfloordiv__(self, other) -> Literal["r//"]: + return "r//" + +# Subclass reflected dunder methods take precedence over the superclass's regular dunders. +reveal_type(Yes() + Sub()) # revealed: Literal["r+"] +reveal_type(Yes() - Sub()) # revealed: Literal["r-"] +reveal_type(Yes() * Sub()) # revealed: Literal["r*"] +reveal_type(Yes() @ Sub()) # revealed: Literal["r@"] +reveal_type(Yes() / Sub()) # revealed: Literal["r/"] +reveal_type(Yes() % Sub()) # revealed: Literal["r%"] +reveal_type(Yes() ** Sub()) # revealed: Literal["r**"] +reveal_type(Yes() << Sub()) # revealed: Literal["r<<"] +reveal_type(Yes() >> Sub()) # revealed: Literal["r>>"] +reveal_type(Yes() | Sub()) # revealed: Literal["r|"] +reveal_type(Yes() ^ Sub()) # revealed: Literal["r^"] +reveal_type(Yes() & Sub()) # revealed: Literal["r&"] +reveal_type(Yes() // Sub()) # revealed: Literal["r//"] + +# But for an unrelated class, the superclass regular dunders are used. +reveal_type(Yes() + No()) # revealed: Literal["+"] +reveal_type(Yes() - No()) # revealed: Literal["-"] +reveal_type(Yes() * No()) # revealed: Literal["*"] +reveal_type(Yes() @ No()) # revealed: Literal["@"] +reveal_type(Yes() / No()) # revealed: Literal["/"] +reveal_type(Yes() % No()) # revealed: Literal["%"] +reveal_type(Yes() ** No()) # revealed: Literal["**"] +reveal_type(Yes() << No()) # revealed: Literal["<<"] +reveal_type(Yes() >> No()) # revealed: Literal[">>"] +reveal_type(Yes() | No()) # revealed: Literal["|"] +reveal_type(Yes() ^ No()) # revealed: Literal["^"] +reveal_type(Yes() & No()) # revealed: Literal["&"] +reveal_type(Yes() // No()) # revealed: Literal["//"] +``` + +## Classes + +Dunder methods defined in a class are available to instances of that class, but not to the class +itself. (For these operators to work on the class itself, they would have to be defined on the +class's type, i.e. `type`.) + +```py +class Yes: + def __add__(self, other) -> Literal["+"]: + return "+" + +class Sub(Yes): ... +class No: ... + +# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `Literal[Yes]` and `Literal[Yes]`" +reveal_type(Yes + Yes) # revealed: Unknown +# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `Literal[Sub]` and `Literal[Sub]`" +reveal_type(Sub + Sub) # revealed: Unknown +# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `Literal[No]` and `Literal[No]`" +reveal_type(No + No) # revealed: Unknown +``` + +## Subclass + +```py +class Yes: + def __add__(self, other) -> Literal["+"]: + return "+" + +class Sub(Yes): ... +class No: ... + +def yes() -> type[Yes]: + return Yes + +def sub() -> type[Sub]: + return Sub + +def no() -> type[No]: + return No + +# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `type[Yes]` and `type[Yes]`" +reveal_type(yes() + yes()) # revealed: Unknown +# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `type[Sub]` and `type[Sub]`" +reveal_type(sub() + sub()) # revealed: Unknown +# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `type[No]` and `type[No]`" +reveal_type(no() + no()) # revealed: Unknown +``` + +## Function literals + +```py +def f(): + pass + +# error: [unsupported-operator] "Operator `+` is unsupported between objects of type `Literal[f]` and `Literal[f]`" +reveal_type(f + f) # revealed: Unknown +# error: [unsupported-operator] "Operator `-` is unsupported between objects of type `Literal[f]` and `Literal[f]`" +reveal_type(f - f) # revealed: Unknown +# error: [unsupported-operator] "Operator `*` is unsupported between objects of type `Literal[f]` and `Literal[f]`" +reveal_type(f * f) # revealed: Unknown +# error: [unsupported-operator] "Operator `@` is unsupported between objects of type `Literal[f]` and `Literal[f]`" +reveal_type(f @ f) # revealed: Unknown +# error: [unsupported-operator] "Operator `/` is unsupported between objects of type `Literal[f]` and `Literal[f]`" +reveal_type(f / f) # revealed: Unknown +# error: [unsupported-operator] "Operator `%` is unsupported between objects of type `Literal[f]` and `Literal[f]`" +reveal_type(f % f) # revealed: Unknown +# error: [unsupported-operator] "Operator `**` is unsupported between objects of type `Literal[f]` and `Literal[f]`" +reveal_type(f**f) # revealed: Unknown +# error: [unsupported-operator] "Operator `<<` is unsupported between objects of type `Literal[f]` and `Literal[f]`" +reveal_type(f << f) # revealed: Unknown +# error: [unsupported-operator] "Operator `>>` is unsupported between objects of type `Literal[f]` and `Literal[f]`" +reveal_type(f >> f) # revealed: Unknown +# error: [unsupported-operator] "Operator `|` is unsupported between objects of type `Literal[f]` and `Literal[f]`" +reveal_type(f | f) # revealed: Unknown +# error: [unsupported-operator] "Operator `^` is unsupported between objects of type `Literal[f]` and `Literal[f]`" +reveal_type(f ^ f) # revealed: Unknown +# error: [unsupported-operator] "Operator `&` is unsupported between objects of type `Literal[f]` and `Literal[f]`" +reveal_type(f & f) # revealed: Unknown +# error: [unsupported-operator] "Operator `//` is unsupported between objects of type `Literal[f]` and `Literal[f]`" +reveal_type(f // f) # revealed: Unknown +``` diff --git a/crates/red_knot_python_semantic/resources/mdtest/binary/integers.md b/crates/red_knot_python_semantic/resources/mdtest/binary/integers.md index 0f4fe2d1fbdfc..e6d4b4c90d2ce 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/binary/integers.md +++ b/crates/red_knot_python_semantic/resources/mdtest/binary/integers.md @@ -9,6 +9,34 @@ reveal_type(3 * -1) # revealed: Literal[-3] reveal_type(-3 // 3) # revealed: Literal[-1] reveal_type(-3 / 3) # revealed: float reveal_type(5 % 3) # revealed: Literal[2] + +# TODO: We don't currently verify that the actual parameter to int.__add__ matches the declared +# formal parameter type. +reveal_type(2 + "f") # revealed: int + +def lhs(x: int): + reveal_type(x + 1) # revealed: int + reveal_type(x - 4) # revealed: int + reveal_type(x * -1) # revealed: int + reveal_type(x // 3) # revealed: int + reveal_type(x / 3) # revealed: float + reveal_type(x % 3) # revealed: int + +def rhs(x: int): + reveal_type(2 + x) # revealed: int + reveal_type(3 - x) # revealed: int + reveal_type(3 * x) # revealed: int + reveal_type(-3 // x) # revealed: int + reveal_type(-3 / x) # revealed: float + reveal_type(5 % x) # revealed: int + +def both(x: int): + reveal_type(x + x) # revealed: int + reveal_type(x - x) # revealed: int + reveal_type(x * x) # revealed: int + reveal_type(x // x) # revealed: int + reveal_type(x / x) # revealed: float + reveal_type(x % x) # revealed: int ``` ## Power @@ -21,6 +49,11 @@ largest_u32 = 4_294_967_295 reveal_type(2**2) # revealed: Literal[4] reveal_type(1 ** (largest_u32 + 1)) # revealed: int reveal_type(2**largest_u32) # revealed: int + +def variable(x: int): + reveal_type(x**2) # revealed: @Todo(return type) + reveal_type(2**x) # revealed: @Todo(return type) + reveal_type(x**x) # revealed: @Todo(return type) ``` ## Division by Zero diff --git a/crates/red_knot_python_semantic/resources/mdtest/slots.md b/crates/red_knot_python_semantic/resources/mdtest/slots.md index 4065c5d2f5861..c61c2bf96dee4 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/slots.md +++ b/crates/red_knot_python_semantic/resources/mdtest/slots.md @@ -167,7 +167,7 @@ class A: __slots__ = () __slots__ += ("a", "b") -reveal_type(A.__slots__) # revealed: @Todo(Support for more binary expressions) +reveal_type(A.__slots__) # revealed: @Todo(return type) class B: __slots__ = ("c", "d") diff --git a/crates/red_knot_python_semantic/resources/mdtest/unary/custom.md b/crates/red_knot_python_semantic/resources/mdtest/unary/custom.md index 931521f75c340..ea46a0ae6f31c 100644 --- a/crates/red_knot_python_semantic/resources/mdtest/unary/custom.md +++ b/crates/red_knot_python_semantic/resources/mdtest/unary/custom.md @@ -34,6 +34,10 @@ reveal_type(~No()) # revealed: Unknown ## Classes +Dunder methods defined in a class are available to instances of that class, but not to the class +itself. (For these operators to work on the class itself, they would have to be defined on the +class's type, i.e. `type`.) + ```py class Yes: def __pos__(self) -> bool: diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index d340b428200ac..9a906ff1365ef 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -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()), - op, - ), - - (Type::IntLiteral(_), Type::Instance(_), op) => self.infer_binary_expression_type( - KnownClass::Int.to_instance(self.db()), - right_ty, - op, - ), - - (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::BooleanLiteral(b1), Type::BooleanLiteral(b2), ast::Operator::BitOr) => { + Some(Type::BooleanLiteral(b1 | b2)) + } - (Type::Instance(_), Type::BytesLiteral(_), op) => self.infer_binary_expression_type( - left_ty, - KnownClass::Bytes.to_instance(self.db()), + (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::BytesLiteral(_), Type::Instance(_), op) => self.infer_binary_expression_type( - KnownClass::Bytes.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, - ), - - (Type::Instance(left), Type::Instance(right), op) => { - if left != right && right.is_subtype_of(self.db(), left) { + ) => { + // 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__ + + // 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")), } } diff --git a/crates/red_knot_workspace/tests/check.rs b/crates/red_knot_workspace/tests/check.rs index 04a1ba1c07e7d..8b5aeb1b20c4a 100644 --- a/crates/red_knot_workspace/tests/check.rs +++ b/crates/red_knot_workspace/tests/check.rs @@ -272,7 +272,8 @@ impl SourceOrderVisitor<'_> for PullTypesVisitor<'_> { #[rustfmt::skip] const KNOWN_FAILURES: &[(&str, bool, bool)] = &[ // related to circular references in class definitions - ("crates/ruff_linter/resources/test/fixtures/pyflakes/F821_26.py", true, false), + ("crates/ruff_linter/resources/test/fixtures/pyflakes/F821_26.py", true, true), + ("crates/ruff_linter/resources/test/fixtures/pyflakes/F821_27.py", true, true), ("crates/ruff_linter/resources/test/fixtures/pyflakes/F811_19.py", true, false), ("crates/ruff_linter/resources/test/fixtures/pyupgrade/UP039.py", true, false), // related to circular references in type aliases (salsa cycle panic):