Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[red-knot] Implement more types in binary and unary expressions #13803

Merged
merged 23 commits into from
Oct 20, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
cd88837
Implement unary op for any and unknown
Glyphack Oct 17, 2024
c612089
Handle division by zero for boolean rhs
Glyphack Oct 17, 2024
0fcf286
Handle booleans in binary expression
Glyphack Oct 17, 2024
b056730
Handle instances in unary operation
Glyphack Oct 17, 2024
00df2f9
Remove unknown test case
Glyphack Oct 17, 2024
cad456f
Add more tests without unnecessary variables
Glyphack Oct 18, 2024
3aa7b6c
Check for Bool class in division by zero
Glyphack Oct 19, 2024
de190b8
Convert booleans to integer literal in binary operations
Glyphack Oct 19, 2024
729ddb7
Separate handling unary not for instance types
Glyphack Oct 19, 2024
097678b
Merge branch 'main' into unary-op
Glyphack Oct 19, 2024
ce45e6b
Update crates/red_knot_python_semantic/src/types/infer.rs
Glyphack Oct 19, 2024
f4f425f
Update crates/red_knot_python_semantic/src/types/infer.rs
Glyphack Oct 19, 2024
21e249e
Update crates/red_knot_python_semantic/src/types/infer.rs
Glyphack Oct 19, 2024
05df52f
Apply suggestions
Glyphack Oct 19, 2024
24b8a43
Add tests for integer power op
Glyphack Oct 19, 2024
aca5e41
Merge branch 'main' into unary-op
Glyphack Oct 19, 2024
d962bce
Correct return type
Glyphack Oct 19, 2024
f0b4eef
Update integers.md
Glyphack Oct 19, 2024
d1b3abf
Reformat markdown tests
Glyphack Oct 19, 2024
35555c3
Add debug assert for instance not op
Glyphack Oct 19, 2024
f99644e
Use unreachable
Glyphack Oct 19, 2024
6f8f74b
Update crates/red_knot_python_semantic/src/types/infer.rs
Glyphack Oct 19, 2024
8292567
Apply suggestions from code review
carljm Oct 20, 2024
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
## Binary operations on booleans

## Basic Arithmetic

We try to be precise and all operations except for division will result in Literal type.

```py
a = True
b = False

reveal_type(a + a) # revealed: Literal[2]
reveal_type(a + b) # revealed: Literal[1]
reveal_type(b + a) # revealed: Literal[1]
reveal_type(b + b) # revealed: Literal[0]

reveal_type(a - a) # revealed: Literal[0]
reveal_type(a - b) # revealed: Literal[1]
reveal_type(b - a) # revealed: Literal[-1]
reveal_type(b - b) # revealed: Literal[0]

reveal_type(a * a) # revealed: Literal[1]
reveal_type(a * b) # revealed: Literal[0]
reveal_type(b * a) # revealed: Literal[0]
reveal_type(b * b) # revealed: Literal[0]

reveal_type(a % a) # revealed: Literal[0]
reveal_type(b % a) # revealed: Literal[0]

reveal_type(a // a) # revealed: Literal[1]
reveal_type(b // a) # revealed: Literal[0]

reveal_type(a ** a) # revealed: Literal[1]
reveal_type(a ** b) # revealed: Literal[1]
reveal_type(b ** a) # revealed: Literal[0]
reveal_type(b ** b) # revealed: Literal[1]

# Division
reveal_type(a / a) # revealed: float
reveal_type(b / a) # revealed: float
b / b # error: [division-by-zero] "Cannot divide object of type `Literal[False]` by zero"
a / b # error: [division-by-zero] "Cannot divide object of type `Literal[True]` by zero"

# bitwise OR
reveal_type(a | a) # revealed: Literal[True]
reveal_type(a | b) # revealed: Literal[True]
reveal_type(b | a) # revealed: Literal[True]
reveal_type(b | b) # revealed: Literal[False]
```
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ subclass; we only emit the error if the LHS type is exactly `int` or `float`, no

```py
a = 1 / 0 # error: "Cannot divide object of type `Literal[1]` by zero"

reveal_type(a) # revealed: float

b = 2 // 0 # error: "Cannot floor divide object of type `Literal[2]` by zero"
Expand All @@ -38,6 +39,11 @@ d = int() / 0 # error: "Cannot divide object of type `int` by zero"
# TODO should be int
reveal_type(d) # revealed: @Todo

f = 1 / False # error: "Cannot divide object of type `Literal[1]` by zero"
reveal_type(f) # revealed: float
True / False # error: [division-by-zero] "Cannot divide object of type `Literal[True]` by zero"
bool(1) / False # error: [division-by-zero] "Cannot divide object of type `Literal[True]` by zero"

e = 1.0 / 0 # error: "Cannot divide object of type `float` by zero"
# TODO should be float
reveal_type(e) # revealed: @Todo
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Unary Operations

```py
class Number:
Glyphack marked this conversation as resolved.
Show resolved Hide resolved
def __init__(self, value: int):
self.value = 1

def __pos__(self) -> int:
return +self.value

def __neg__(self) -> int:
return -self.value

def __invert__(self) -> Literal[True]:
return True

a = Number()

reveal_type(+a) # revealed: int
reveal_type(-a) # revealed: int
reveal_type(~a) # revealed: @Todo

class NoDunder:
...

b = NoDunder()
+b # error: [unsupported-operator] "Operator `+` is unsupported for type `NoDunder`"
-b # error: [unsupported-operator] "Operator `-` is unsupported for type `NoDunder`"
~b # error: [unsupported-operator] "Operator `~` is unsupported for type `NoDunder`"

```
7 changes: 7 additions & 0 deletions crates/red_knot_python_semantic/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -355,6 +355,13 @@ impl<'db> Type<'db> {
pub const fn into_int_literal_type(self) -> Option<i64> {
match self {
Type::IntLiteral(value) => Some(value),
Type::BooleanLiteral(b) => {
if b {
Some(1)
} else {
Some(0)
}
}
Glyphack marked this conversation as resolved.
Show resolved Hide resolved
_ => None,
}
}
Expand Down
86 changes: 78 additions & 8 deletions crates/red_knot_python_semantic/src/types/infer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ use crate::types::{
};
use crate::Db;

use super::{KnownClass, UnionBuilder};
use super::{CallOutcome, KnownClass, UnionBuilder};

/// Infer all types for a [`ScopeId`], including all definitions and expressions in that scope.
/// Use when checking a scope, or needing to provide a type for an arbitrary expression in the
Expand Down Expand Up @@ -537,12 +537,18 @@ impl<'db> TypeInferenceBuilder<'db> {
/// Raise a diagnostic if the given type cannot be divided by zero.
///
/// Expects the resolved type of the left side of the binary expression.
fn check_division_by_zero(&mut self, expr: &ast::ExprBinOp, left: Type<'db>) {
fn check_division_by_zero(
&mut self,
expr: &ast::ExprBinOp,
left: Type<'db>,
original_left: Type<'db>,
) {
match left {
Type::IntLiteral(_) => {}
Type::Instance(cls)
if cls.is_known(self.db, KnownClass::Float)
|| cls.is_known(self.db, KnownClass::Int) => {}
if [KnownClass::Float, KnownClass::Int, KnownClass::Bool]
.iter()
.any(|&k| cls.is_known(self.db, k)) => {}
_ => return,
};

Expand All @@ -558,7 +564,7 @@ impl<'db> TypeInferenceBuilder<'db> {
"division-by-zero",
format_args!(
"Cannot {op} object of type `{}` {by_zero}",
left.display(self.db)
original_left.display(self.db)
),
);
}
Expand Down Expand Up @@ -2459,7 +2465,9 @@ impl<'db> TypeInferenceBuilder<'db> {
operand,
} = unary;

match (op, self.infer_expression(operand)) {
let operand_type = self.infer_expression(operand);

match (op, operand_type) {
(UnaryOp::UAdd, Type::IntLiteral(value)) => Type::IntLiteral(value),
(UnaryOp::USub, Type::IntLiteral(value)) => Type::IntLiteral(-value),
(UnaryOp::Invert, Type::IntLiteral(value)) => Type::IntLiteral(!value),
Expand All @@ -2469,7 +2477,24 @@ impl<'db> TypeInferenceBuilder<'db> {
(UnaryOp::Invert, Type::BooleanLiteral(bool)) => Type::IntLiteral(!i64::from(bool)),

(UnaryOp::Not, ty) => ty.bool(self.db).negate().into_type(self.db),
(_, Type::Any) => Type::Any,
(_, Type::Unknown) => Type::Unknown,
(op @ (UnaryOp::UAdd | UnaryOp::USub | UnaryOp::Invert), Type::Instance(class)) => {
let class_member = class.class_member(self.db, op.dunder());
let call = class_member.call(self.db, &[operand_type]);
carljm marked this conversation as resolved.
Show resolved Hide resolved
if matches!(call, CallOutcome::NotCallable { not_callable_ty: _ }) {
carljm marked this conversation as resolved.
Show resolved Hide resolved
self.add_diagnostic(
unary.into(),
"unsupported-operator",
format_args!(
"Operator `{op}` is unsupported for type `{}`",
Glyphack marked this conversation as resolved.
Show resolved Hide resolved
operand_type.display(self.db),
),
);
}

call.return_ty(self.db).unwrap_or(Type::Unknown)
}
_ => Type::Todo, // TODO other unary op types
}
}
Expand All @@ -2484,17 +2509,32 @@ impl<'db> TypeInferenceBuilder<'db> {

let left_ty = self.infer_expression(left);
let right_ty = self.infer_expression(right);
self.infer_binary_expression_type(binary, *op, left_ty, right_ty)
}

fn infer_binary_expression_type(
&mut self,
expr: &ast::ExprBinOp,
op: ast::Operator,
left_ty: Type<'db>,
right_ty: Type<'db>,
) -> Type<'db> {
// Check for division by zero; this doesn't change the inferred type for the expression, but
// may emit a diagnostic
if matches!(
(op, right_ty),
(
ast::Operator::Div | ast::Operator::FloorDiv | ast::Operator::Mod,
Type::IntLiteral(0),
Type::IntLiteral(0)
)
) {
self.check_division_by_zero(binary, left_ty);
// We want to print the original type that was used in the expression. The left type might
carljm marked this conversation as resolved.
Show resolved Hide resolved
// have been converted to other types during the check.
// Since the type was previously inferred in the function that calls this or in the
// previous recursion this id will exist
let expr_id = expr.left.scoped_ast_id(self.db, self.scope());
let previous = self.types.expressions[&expr_id];
self.check_division_by_zero(expr, left_ty, previous);
}

match (left_ty, right_ty, op) {
Expand Down Expand Up @@ -2532,6 +2572,17 @@ impl<'db> TypeInferenceBuilder<'db> {
.map(Type::IntLiteral)
.unwrap_or_else(|| KnownClass::Int.to_instance(self.db)),

(Type::IntLiteral(n), Type::IntLiteral(m), ast::Operator::Pow) => {
carljm marked this conversation as resolved.
Show resolved Hide resolved
let m = u32::try_from(m);
match m {
Ok(m) => n
.checked_pow(m)
.map(Type::IntLiteral)
.unwrap_or_else(|| KnownClass::Int.to_instance(self.db)),
Err(_) => KnownClass::Int.to_instance(self.db),
}
}

(Type::BytesLiteral(lhs), Type::BytesLiteral(rhs), ast::Operator::Add) => {
Type::BytesLiteral(BytesLiteralType::new(
self.db,
Expand Down Expand Up @@ -2589,6 +2640,25 @@ impl<'db> TypeInferenceBuilder<'db> {
}
}

(
Type::BooleanLiteral(b1),
Type::BooleanLiteral(b2),
ruff_python_ast::Operator::BitOr,
) => match (b1, b2) {
(true, true) => Type::BooleanLiteral(true),
(true, false) => Type::BooleanLiteral(true),
(false, true) => Type::BooleanLiteral(true),
(false, false) => Type::BooleanLiteral(false),
},
Glyphack marked this conversation as resolved.
Show resolved Hide resolved

(Type::BooleanLiteral(_), right, op) => {
let int_value = left_ty.expect_int_literal();
self.infer_binary_expression_type(expr, op, Type::IntLiteral(int_value), right)
Glyphack marked this conversation as resolved.
Show resolved Hide resolved
}
(left, Type::BooleanLiteral(_), op) => {
let int_value = right_ty.expect_int_literal();
self.infer_binary_expression_type(expr, op, left, Type::IntLiteral(int_value))
}
_ => Type::Todo, // TODO
}
}
Expand Down
8 changes: 8 additions & 0 deletions crates/ruff_python_ast/src/nodes.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2997,6 +2997,14 @@ impl UnaryOp {
UnaryOp::USub => "-",
}
}
pub const fn dunder(self) -> &'static str {
match self {
UnaryOp::Invert => "__invert__",
UnaryOp::Not => "__bool__",
carljm marked this conversation as resolved.
Show resolved Hide resolved
UnaryOp::UAdd => "__pos__",
UnaryOp::USub => "__neg__",
}
}
}

impl fmt::Display for UnaryOp {
Expand Down
Loading