This commit is contained in:
Charlie Marsh 2025-12-16 20:20:07 +03:00 committed by GitHub
commit a779d92683
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 625 additions and 69 deletions

View File

@ -338,7 +338,111 @@ reveal_type(a is not c) # revealed: Literal[True]
For tuples like `tuple[int, ...]`, `tuple[Any, ...]`
// TODO
### Unsupported Comparisons
<!-- snapshot-diagnostics -->
Comparisons between homogeneous tuples with incompatible element types should emit diagnostics for
ordering operators (`<`, `<=`, `>`, `>=`), but not for equality operators (`==`, `!=`).
```py
def f(
a: tuple[int, ...],
b: tuple[str, ...],
c: tuple[str],
):
# Equality comparisons are always valid
reveal_type(a == b) # revealed: bool
reveal_type(a != b) # revealed: bool
# Ordering comparisons between incompatible types should emit errors
# error: [unsupported-operator] "Operator `<` is not supported between objects of type `tuple[int, ...]` and `tuple[str, ...]`"
a < b
# error: [unsupported-operator] "Operator `<` is not supported between objects of type `tuple[str, ...]` and `tuple[int, ...]`"
b < a
# error: [unsupported-operator] "Operator `<` is not supported between objects of type `tuple[int, ...]` and `tuple[str]`"
a < c
# error: [unsupported-operator] "Operator `<` is not supported between objects of type `tuple[str]` and `tuple[int, ...]`"
c < a
```
When comparing fixed-length tuples with variable-length tuples, all element types that could
potentially be compared must be compatible.
```py
def _(
var_int: tuple[int, ...],
var_str: tuple[str, ...],
fixed_int_str: tuple[int, str],
):
# Fixed `tuple[int, str]` vs. variable `tuple[int, ...]`:
# Position 0: `int` vs. `int` are comparable.
# Position 1 (if `var_int` has 2+ elements): `str` vs. `int` are not comparable.
# error: [unsupported-operator]
fixed_int_str < var_int
# Variable `tuple[int, ...]` vs. fixed `tuple[int, str]`:
# Position 0: `int` vs. `int` are comparable.
# Position 1 (if `var_int` has 2+ elements): `int` vs. `str` are not comparable.
# error: [unsupported-operator]
var_int < fixed_int_str
# Variable `tuple[str, ...]` vs. fixed `tuple[int, str]`:
# Position 0: `str` vs. `int` are not comparable.
# error: [unsupported-operator]
var_str < fixed_int_str
```
### Supported Comparisons
Comparisons between homogeneous tuples with compatible element types should work.
```py
def _(a: tuple[int, ...], b: tuple[int, ...], c: tuple[bool, ...]):
# Same element types - always valid
reveal_type(a == b) # revealed: bool
reveal_type(a != b) # revealed: bool
reveal_type(a < b) # revealed: bool
reveal_type(a <= b) # revealed: bool
reveal_type(a > b) # revealed: bool
reveal_type(a >= b) # revealed: bool
# int and bool are compatible for comparison
reveal_type(a < c) # revealed: bool
reveal_type(c < a) # revealed: bool
```
### Tuples with Prefixes and Suffixes
<!-- snapshot-diagnostics -->
Variable-length tuples with prefixes and suffixes are also checked.
```toml
[environment]
python-version = "3.11"
```
```py
def _(
prefix_int_var_str: tuple[int, *tuple[str, ...]],
prefix_str_var_int: tuple[str, *tuple[int, ...]],
):
# Prefix `int` vs. prefix `str` are not comparable.
# error: [unsupported-operator]
prefix_int_var_str < prefix_str_var_int
```
Tuples with compatible prefixes/suffixes are allowed.
```py
def _(
prefix_int_var_int: tuple[int, *tuple[int, ...]],
prefix_int_var_bool: tuple[int, *tuple[bool, ...]],
):
# Prefix `int` vs. prefix `int`, variable `int` vs. variable `bool` are all comparable.
reveal_type(prefix_int_var_int < prefix_int_var_bool) # revealed: bool
```
## Chained comparisons with elements that incorrectly implement `__bool__`

View File

@ -0,0 +1,49 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: tuples.md - Comparison: Tuples - Homogeneous - Tuples with Prefixes and Suffixes
mdtest path: crates/ty_python_semantic/resources/mdtest/comparison/tuples.md
---
# Python source files
## mdtest_snippet.py
```
1 | def _(
2 | prefix_int_var_str: tuple[int, *tuple[str, ...]],
3 | prefix_str_var_int: tuple[str, *tuple[int, ...]],
4 | ):
5 | # Prefix `int` vs. prefix `str` are not comparable.
6 | # error: [unsupported-operator]
7 | prefix_int_var_str < prefix_str_var_int
8 | def _(
9 | prefix_int_var_int: tuple[int, *tuple[int, ...]],
10 | prefix_int_var_bool: tuple[int, *tuple[bool, ...]],
11 | ):
12 | # Prefix `int` vs. prefix `int`, variable `int` vs. variable `bool` are all comparable.
13 | reveal_type(prefix_int_var_int < prefix_int_var_bool) # revealed: bool
```
# Diagnostics
```
error[unsupported-operator]: Unsupported `<` operation
--> src/mdtest_snippet.py:7:5
|
5 | # Prefix `int` vs. prefix `str` are not comparable.
6 | # error: [unsupported-operator]
7 | prefix_int_var_str < prefix_str_var_int
| ------------------^^^------------------
| | |
| | Has type `tuple[str, *tuple[int, ...]]`
| Has type `tuple[int, *tuple[str, ...]]`
8 | def _(
9 | prefix_int_var_int: tuple[int, *tuple[int, ...]],
|
info: Operation fails because operator `<` is not supported between objects of type `int` and `str`
info: rule `unsupported-operator` is enabled by default
```

View File

@ -0,0 +1,187 @@
---
source: crates/ty_test/src/lib.rs
expression: snapshot
---
---
mdtest name: tuples.md - Comparison: Tuples - Homogeneous - Unsupported Comparisons
mdtest path: crates/ty_python_semantic/resources/mdtest/comparison/tuples.md
---
# Python source files
## mdtest_snippet.py
```
1 | def f(
2 | a: tuple[int, ...],
3 | b: tuple[str, ...],
4 | c: tuple[str],
5 | ):
6 | # Equality comparisons are always valid
7 | reveal_type(a == b) # revealed: bool
8 | reveal_type(a != b) # revealed: bool
9 |
10 | # Ordering comparisons between incompatible types should emit errors
11 | # error: [unsupported-operator] "Operator `<` is not supported between objects of type `tuple[int, ...]` and `tuple[str, ...]`"
12 | a < b
13 | # error: [unsupported-operator] "Operator `<` is not supported between objects of type `tuple[str, ...]` and `tuple[int, ...]`"
14 | b < a
15 | # error: [unsupported-operator] "Operator `<` is not supported between objects of type `tuple[int, ...]` and `tuple[str]`"
16 | a < c
17 | # error: [unsupported-operator] "Operator `<` is not supported between objects of type `tuple[str]` and `tuple[int, ...]`"
18 | c < a
19 | def _(
20 | var_int: tuple[int, ...],
21 | var_str: tuple[str, ...],
22 | fixed_int_str: tuple[int, str],
23 | ):
24 | # Fixed `tuple[int, str]` vs. variable `tuple[int, ...]`:
25 | # Position 0: `int` vs. `int` are comparable.
26 | # Position 1 (if `var_int` has 2+ elements): `str` vs. `int` are not comparable.
27 | # error: [unsupported-operator]
28 | fixed_int_str < var_int
29 |
30 | # Variable `tuple[int, ...]` vs. fixed `tuple[int, str]`:
31 | # Position 0: `int` vs. `int` are comparable.
32 | # Position 1 (if `var_int` has 2+ elements): `int` vs. `str` are not comparable.
33 | # error: [unsupported-operator]
34 | var_int < fixed_int_str
35 |
36 | # Variable `tuple[str, ...]` vs. fixed `tuple[int, str]`:
37 | # Position 0: `str` vs. `int` are not comparable.
38 | # error: [unsupported-operator]
39 | var_str < fixed_int_str
```
# Diagnostics
```
error[unsupported-operator]: Unsupported `<` operation
--> src/mdtest_snippet.py:12:5
|
10 | # Ordering comparisons between incompatible types should emit errors
11 | # error: [unsupported-operator] "Operator `<` is not supported between objects of type `tuple[int, ...]` and `tuple[str, ...]`"
12 | a < b
| -^^^-
| | |
| | Has type `tuple[str, ...]`
| Has type `tuple[int, ...]`
13 | # error: [unsupported-operator] "Operator `<` is not supported between objects of type `tuple[str, ...]` and `tuple[int, ...]`"
14 | b < a
|
info: Operation fails because operator `<` is not supported between objects of type `int` and `str`
info: rule `unsupported-operator` is enabled by default
```
```
error[unsupported-operator]: Unsupported `<` operation
--> src/mdtest_snippet.py:14:5
|
12 | a < b
13 | # error: [unsupported-operator] "Operator `<` is not supported between objects of type `tuple[str, ...]` and `tuple[int, ...]`"
14 | b < a
| -^^^-
| | |
| | Has type `tuple[int, ...]`
| Has type `tuple[str, ...]`
15 | # error: [unsupported-operator] "Operator `<` is not supported between objects of type `tuple[int, ...]` and `tuple[str]`"
16 | a < c
|
info: Operation fails because operator `<` is not supported between objects of type `str` and `int`
info: rule `unsupported-operator` is enabled by default
```
```
error[unsupported-operator]: Unsupported `<` operation
--> src/mdtest_snippet.py:16:5
|
14 | b < a
15 | # error: [unsupported-operator] "Operator `<` is not supported between objects of type `tuple[int, ...]` and `tuple[str]`"
16 | a < c
| -^^^-
| | |
| | Has type `tuple[str]`
| Has type `tuple[int, ...]`
17 | # error: [unsupported-operator] "Operator `<` is not supported between objects of type `tuple[str]` and `tuple[int, ...]`"
18 | c < a
|
info: Operation fails because operator `<` is not supported between objects of type `int` and `str`
info: rule `unsupported-operator` is enabled by default
```
```
error[unsupported-operator]: Unsupported `<` operation
--> src/mdtest_snippet.py:18:5
|
16 | a < c
17 | # error: [unsupported-operator] "Operator `<` is not supported between objects of type `tuple[str]` and `tuple[int, ...]`"
18 | c < a
| -^^^-
| | |
| | Has type `tuple[int, ...]`
| Has type `tuple[str]`
19 | def _(
20 | var_int: tuple[int, ...],
|
info: Operation fails because operator `<` is not supported between objects of type `str` and `int`
info: rule `unsupported-operator` is enabled by default
```
```
error[unsupported-operator]: Unsupported `<` operation
--> src/mdtest_snippet.py:28:5
|
26 | # Position 1 (if `var_int` has 2+ elements): `str` vs. `int` are not comparable.
27 | # error: [unsupported-operator]
28 | fixed_int_str < var_int
| -------------^^^-------
| | |
| | Has type `tuple[int, ...]`
| Has type `tuple[int, str]`
29 |
30 | # Variable `tuple[int, ...]` vs. fixed `tuple[int, str]`:
|
info: Operation fails because operator `<` is not supported between objects of type `str` and `int`
info: rule `unsupported-operator` is enabled by default
```
```
error[unsupported-operator]: Unsupported `<` operation
--> src/mdtest_snippet.py:34:5
|
32 | # Position 1 (if `var_int` has 2+ elements): `int` vs. `str` are not comparable.
33 | # error: [unsupported-operator]
34 | var_int < fixed_int_str
| -------^^^-------------
| | |
| | Has type `tuple[int, str]`
| Has type `tuple[int, ...]`
35 |
36 | # Variable `tuple[str, ...]` vs. fixed `tuple[int, str]`:
|
info: Operation fails because operator `<` is not supported between objects of type `int` and `str`
info: rule `unsupported-operator` is enabled by default
```
```
error[unsupported-operator]: Unsupported `<` operation
--> src/mdtest_snippet.py:39:5
|
37 | # Position 0: `str` vs. `int` are not comparable.
38 | # error: [unsupported-operator]
39 | var_str < fixed_int_str
| -------^^^-------------
| | |
| | Has type `tuple[int, str]`
| Has type `tuple[str, ...]`
|
info: Operation fails because operator `<` is not supported between objects of type `str` and `int`
info: rule `unsupported-operator` is enabled by default
```

View File

@ -11191,84 +11191,300 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
range: TextRange,
visitor: &BinaryComparisonVisitor<'db>,
) -> Result<Type<'db>, UnsupportedComparisonError<'db>> {
// If either tuple is variable length, we can make no assumptions about the relative
// lengths of the tuples, and therefore neither about how they compare lexicographically.
// TODO: Consider comparing the prefixes of the tuples, since that could give a comparison
// result regardless of how long the variable-length tuple is.
let (TupleSpec::Fixed(left), TupleSpec::Fixed(right)) = (left, right) else {
return Ok(Type::unknown());
};
match (left, right) {
// Both fixed-length: perform full lexicographic comparison.
(TupleSpec::Fixed(left), TupleSpec::Fixed(right)) => {
let left_iter = left.elements().copied();
let right_iter = right.elements().copied();
let left_iter = left.elements().copied();
let right_iter = right.elements().copied();
let mut builder = UnionBuilder::new(self.db());
let mut builder = UnionBuilder::new(self.db());
for (l_ty, r_ty) in left_iter.zip(right_iter) {
let pairwise_eq_result = self
.infer_binary_type_comparison(l_ty, ast::CmpOp::Eq, r_ty, range, visitor)
.expect(
"infer_binary_type_comparison should never return None for `CmpOp::Eq`",
);
for (l_ty, r_ty) in left_iter.zip(right_iter) {
let pairwise_eq_result = self
.infer_binary_type_comparison(l_ty, ast::CmpOp::Eq, r_ty, range, visitor)
.expect("infer_binary_type_comparison should never return None for `CmpOp::Eq`");
match pairwise_eq_result
.try_bool(self.db())
.unwrap_or_else(|err| {
// TODO: We should, whenever possible, pass the range of the left and right elements
// instead of the range of the whole tuple.
err.report_diagnostic(&self.context, range);
err.fallback_truthiness()
}) {
// - AlwaysTrue : Continue to the next pair for lexicographic comparison
Truthiness::AlwaysTrue => continue,
// - AlwaysFalse:
// Lexicographic comparisons will always terminate with this pair.
// Complete the comparison and return the result.
// - Ambiguous:
// Lexicographic comparisons might continue to the next pair (if eq_result is true),
// or terminate here (if eq_result is false).
// To account for cases where the comparison terminates here, add the pairwise comparison result to the union builder.
eq_truthiness @ (Truthiness::AlwaysFalse | Truthiness::Ambiguous) => {
let pairwise_compare_result = match op {
RichCompareOperator::Lt
| RichCompareOperator::Le
| RichCompareOperator::Gt
| RichCompareOperator::Ge => self.infer_binary_type_comparison(
l_ty,
op.into(),
r_ty,
range,
visitor,
)?,
// For `==` and `!=`, we already figure out the result from `pairwise_eq_result`
// NOTE: The CPython implementation does not account for non-boolean return types
// or cases where `!=` is not the negation of `==`, we also do not consider these cases.
RichCompareOperator::Eq => Type::BooleanLiteral(false),
RichCompareOperator::Ne => Type::BooleanLiteral(true),
};
match pairwise_eq_result
.try_bool(self.db())
.unwrap_or_else(|err| {
// TODO: We should, whenever possible, pass the range of the left and right elements
// instead of the range of the whole tuple.
err.report_diagnostic(&self.context, range);
err.fallback_truthiness()
}) {
// - AlwaysTrue : Continue to the next pair for lexicographic comparison
Truthiness::AlwaysTrue => continue,
// - AlwaysFalse:
// Lexicographic comparisons will always terminate with this pair.
// Complete the comparison and return the result.
// - Ambiguous:
// Lexicographic comparisons might continue to the next pair (if eq_result is true),
// or terminate here (if eq_result is false).
// To account for cases where the comparison terminates here, add the pairwise comparison result to the union builder.
eq_truthiness @ (Truthiness::AlwaysFalse | Truthiness::Ambiguous) => {
let pairwise_compare_result = match op {
RichCompareOperator::Lt
| RichCompareOperator::Le
| RichCompareOperator::Gt
| RichCompareOperator::Ge => self.infer_binary_type_comparison(
l_ty,
op.into(),
r_ty,
range,
visitor,
)?,
// For `==` and `!=`, we already figure out the result from `pairwise_eq_result`
// NOTE: The CPython implementation does not account for non-boolean return types
// or cases where `!=` is not the negation of `==`, we also do not consider these cases.
RichCompareOperator::Eq => Type::BooleanLiteral(false),
RichCompareOperator::Ne => Type::BooleanLiteral(true),
};
builder = builder.add(pairwise_compare_result);
builder = builder.add(pairwise_compare_result);
if eq_truthiness.is_ambiguous() {
continue;
}
if eq_truthiness.is_ambiguous() {
continue;
return Ok(builder.build());
}
}
return Ok(builder.build());
}
// if no more items to compare, we just compare sizes
let (left_len, right_len) = (left.len(), right.len());
builder = builder.add(Type::BooleanLiteral(match op {
RichCompareOperator::Eq => left_len == right_len,
RichCompareOperator::Ne => left_len != right_len,
RichCompareOperator::Lt => left_len < right_len,
RichCompareOperator::Le => left_len <= right_len,
RichCompareOperator::Gt => left_len > right_len,
RichCompareOperator::Ge => left_len >= right_len,
}));
Ok(builder.build())
}
// At least one tuple is variable-length. We can make no assumptions about
// the relative lengths of the tuples, and therefore neither about how they
// compare lexicographically. However, we still need to verify that the
// element types are comparable for ordering comparisons.
// For equality comparisons (==, !=), any two objects can be compared,
// and tuple equality always returns bool regardless of element __eq__ return types.
(TupleSpec::Variable(_), _) | (_, TupleSpec::Variable(_))
if matches!(op, RichCompareOperator::Eq | RichCompareOperator::Ne) =>
{
Ok(KnownClass::Bool.to_instance(self.db()))
}
// Both variable: check all elements that could potentially be compared.
(TupleSpec::Variable(left_var), TupleSpec::Variable(right_var)) => {
let mut builder = UnionBuilder::new(self.db());
// 1. Compare prefix elements at matching positions.
for (l_el, r_el) in left_var.prefix_elements().zip(right_var.prefix_elements()) {
builder = builder.add(self.infer_binary_type_comparison(
*l_el,
op.into(),
*r_el,
range,
visitor,
)?);
}
// 2. Left's extra prefix elements are compared with right's variable.
for l_el in left_var.prefix_elements().skip(right_var.prefix.len()) {
builder = builder.add(self.infer_binary_type_comparison(
*l_el,
op.into(),
right_var.variable,
range,
visitor,
)?);
}
// 3. Right's extra prefix elements are compared with left's variable.
for r_el in right_var.prefix_elements().skip(left_var.prefix.len()) {
builder = builder.add(self.infer_binary_type_comparison(
left_var.variable,
op.into(),
*r_el,
range,
visitor,
)?);
}
// 4. Variable elements can be compared at any overlapping position.
builder = builder.add(self.infer_binary_type_comparison(
left_var.variable,
op.into(),
right_var.variable,
range,
visitor,
)?);
// 5. Left's extra suffix elements are compared with right's variable.
for l_el in left_var
.suffix_elements()
.rev()
.skip(right_var.suffix.len())
{
builder = builder.add(self.infer_binary_type_comparison(
*l_el,
op.into(),
right_var.variable,
range,
visitor,
)?);
}
// 6. Right's extra suffix elements are compared with left's variable.
for r_el in right_var
.suffix_elements()
.rev()
.skip(left_var.suffix.len())
{
builder = builder.add(self.infer_binary_type_comparison(
left_var.variable,
op.into(),
*r_el,
range,
visitor,
)?);
}
// 7. Compare suffix elements at matching positions (from the end).
for (l_el, r_el) in left_var
.suffix_elements()
.rev()
.zip(right_var.suffix_elements().rev())
{
builder = builder.add(self.infer_binary_type_comparison(
*l_el,
op.into(),
*r_el,
range,
visitor,
)?);
}
// Length comparison (when all elements are equal) returns bool.
builder = builder.add(KnownClass::Bool.to_instance(self.db()));
Ok(builder.build())
}
// Left variable, right fixed: check which elements could be compared.
(TupleSpec::Variable(left_var), TupleSpec::Fixed(right_fixed)) => {
let mut builder = UnionBuilder::new(self.db());
// Compare left's prefix with right's corresponding elements.
for (l_el, r_el) in left_var.prefix_elements().zip(right_fixed.elements()) {
builder = builder.add(self.infer_binary_type_comparison(
*l_el,
op.into(),
*r_el,
range,
visitor,
)?);
}
// Compare left's suffix with right's corresponding elements (from end).
for (l_el, r_el) in left_var
.suffix_elements()
.rev()
.zip(right_fixed.elements().rev())
{
builder = builder.add(self.infer_binary_type_comparison(
*l_el,
op.into(),
*r_el,
range,
visitor,
)?);
}
// Compare left's variable with right's "middle" elements
// (those not covered by prefix or suffix).
let middle_start = left_var.prefix.len();
let middle_end = right_fixed.len().saturating_sub(left_var.suffix.len());
for r_el in right_fixed
.elements()
.skip(middle_start)
.take(middle_end.saturating_sub(middle_start))
{
builder = builder.add(self.infer_binary_type_comparison(
left_var.variable,
op.into(),
*r_el,
range,
visitor,
)?);
}
// Length comparison (when all elements are equal) returns bool.
builder = builder.add(KnownClass::Bool.to_instance(self.db()));
Ok(builder.build())
}
// Left fixed, right variable: check which elements could be compared.
(TupleSpec::Fixed(left_fixed), TupleSpec::Variable(right_var)) => {
let mut builder = UnionBuilder::new(self.db());
// Compare left's elements with right's prefix.
for (l_el, r_el) in left_fixed.elements().zip(right_var.prefix_elements()) {
builder = builder.add(self.infer_binary_type_comparison(
*l_el,
op.into(),
*r_el,
range,
visitor,
)?);
}
// Compare left's elements (from end) with right's suffix.
for (l_el, r_el) in left_fixed
.elements()
.rev()
.zip(right_var.suffix_elements().rev())
{
builder = builder.add(self.infer_binary_type_comparison(
*l_el,
op.into(),
*r_el,
range,
visitor,
)?);
}
// Compare left's "middle" elements with right's variable.
let middle_start = right_var.prefix.len();
let middle_end = left_fixed.len().saturating_sub(right_var.suffix.len());
for l_el in left_fixed
.elements()
.skip(middle_start)
.take(middle_end.saturating_sub(middle_start))
{
builder = builder.add(self.infer_binary_type_comparison(
*l_el,
op.into(),
right_var.variable,
range,
visitor,
)?);
}
// Length comparison (when all elements are equal) returns bool.
builder = builder.add(KnownClass::Bool.to_instance(self.db()));
Ok(builder.build())
}
}
// if no more items to compare, we just compare sizes
let (left_len, right_len) = (left.len(), right.len());
builder = builder.add(Type::BooleanLiteral(match op {
RichCompareOperator::Eq => left_len == right_len,
RichCompareOperator::Ne => left_len != right_len,
RichCompareOperator::Lt => left_len < right_len,
RichCompareOperator::Le => left_len <= right_len,
RichCompareOperator::Gt => left_len > right_len,
RichCompareOperator::Ge => left_len >= right_len,
}));
Ok(builder.build())
}
fn infer_subscript_expression(&mut self, subscript: &ast::ExprSubscript) -> Type<'db> {