Return bool

This commit is contained in:
Charlie Marsh 2025-12-06 10:26:56 -05:00
parent e20d1a837a
commit 8ef82e96fc
5 changed files with 112 additions and 51 deletions

View File

@ -352,8 +352,8 @@ def f(
c: tuple[str], c: tuple[str],
): ):
# Equality comparisons are always valid # Equality comparisons are always valid
reveal_type(a == b) # revealed: Unknown reveal_type(a == b) # revealed: bool
reveal_type(a != b) # revealed: Unknown reveal_type(a != b) # revealed: bool
# Ordering comparisons between incompatible types should emit errors # Ordering comparisons between incompatible types should emit errors
# error: [unsupported-operator] "Operator `<` is not supported between objects of type `tuple[int, ...]` and `tuple[str, ...]`" # error: [unsupported-operator] "Operator `<` is not supported between objects of type `tuple[int, ...]` and `tuple[str, ...]`"
@ -400,16 +400,16 @@ Comparisons between homogeneous tuples with compatible element types should work
```py ```py
def _(a: tuple[int, ...], b: tuple[int, ...], c: tuple[bool, ...]): def _(a: tuple[int, ...], b: tuple[int, ...], c: tuple[bool, ...]):
# Same element types - always valid # Same element types - always valid
reveal_type(a == b) # revealed: Unknown reveal_type(a == b) # revealed: bool
reveal_type(a != b) # revealed: Unknown reveal_type(a != b) # revealed: bool
reveal_type(a < b) # revealed: Unknown reveal_type(a < b) # revealed: bool
reveal_type(a <= b) # revealed: Unknown reveal_type(a <= b) # revealed: bool
reveal_type(a > b) # revealed: Unknown reveal_type(a > b) # revealed: bool
reveal_type(a >= b) # revealed: Unknown reveal_type(a >= b) # revealed: bool
# int and bool are compatible for comparison # int and bool are compatible for comparison
reveal_type(a < c) # revealed: Unknown reveal_type(a < c) # revealed: bool
reveal_type(c < a) # revealed: Unknown reveal_type(c < a) # revealed: bool
``` ```
### Tuples with Prefixes and Suffixes ### Tuples with Prefixes and Suffixes
@ -441,7 +441,7 @@ def _(
prefix_int_var_bool: tuple[int, *tuple[bool, ...]], prefix_int_var_bool: tuple[int, *tuple[bool, ...]],
): ):
# Prefix `int` vs. prefix `int`, variable `int` vs. variable `bool` are all comparable. # Prefix `int` vs. prefix `int`, variable `int` vs. variable `bool` are all comparable.
reveal_type(prefix_int_var_int < prefix_int_var_bool) # revealed: Unknown reveal_type(prefix_int_var_int < prefix_int_var_bool) # revealed: bool
``` ```
## Chained comparisons with elements that incorrectly implement `__bool__` ## Chained comparisons with elements that incorrectly implement `__bool__`

View File

@ -16,15 +16,15 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/comparison/tuples.md
2 | prefix_int_var_str: tuple[int, *tuple[str, ...]], 2 | prefix_int_var_str: tuple[int, *tuple[str, ...]],
3 | prefix_str_var_int: tuple[str, *tuple[int, ...]], 3 | prefix_str_var_int: tuple[str, *tuple[int, ...]],
4 | ): 4 | ):
5 | # Prefix int vs prefix str - NOT ok 5 | # Prefix `int` vs. prefix `str` are not comparable.
6 | # error: [unsupported-operator] 6 | # error: [unsupported-operator]
7 | prefix_int_var_str < prefix_str_var_int 7 | prefix_int_var_str < prefix_str_var_int
8 | def _( 8 | def _(
9 | prefix_int_var_int: tuple[int, *tuple[int, ...]], 9 | prefix_int_var_int: tuple[int, *tuple[int, ...]],
10 | prefix_int_var_bool: tuple[int, *tuple[bool, ...]], 10 | prefix_int_var_bool: tuple[int, *tuple[bool, ...]],
11 | ): 11 | ):
12 | # Prefix int vs prefix int, variable int vs variable bool - all ok 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: Unknown 13 | reveal_type(prefix_int_var_int < prefix_int_var_bool) # revealed: bool
``` ```
# Diagnostics # Diagnostics
@ -33,7 +33,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/comparison/tuples.md
error[unsupported-operator]: Unsupported `<` operation error[unsupported-operator]: Unsupported `<` operation
--> src/mdtest_snippet.py:7:5 --> src/mdtest_snippet.py:7:5
| |
5 | # Prefix int vs prefix str - NOT ok 5 | # Prefix `int` vs. prefix `str` are not comparable.
6 | # error: [unsupported-operator] 6 | # error: [unsupported-operator]
7 | prefix_int_var_str < prefix_str_var_int 7 | prefix_int_var_str < prefix_str_var_int
| ------------------^^^------------------ | ------------------^^^------------------

View File

@ -18,8 +18,8 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/comparison/tuples.md
4 | c: tuple[str], 4 | c: tuple[str],
5 | ): 5 | ):
6 | # Equality comparisons are always valid 6 | # Equality comparisons are always valid
7 | reveal_type(a == b) # revealed: Unknown 7 | reveal_type(a == b) # revealed: bool
8 | reveal_type(a != b) # revealed: Unknown 8 | reveal_type(a != b) # revealed: bool
9 | 9 |
10 | # Ordering comparisons between incompatible types should emit errors 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, ...]`" 11 | # error: [unsupported-operator] "Operator `<` is not supported between objects of type `tuple[int, ...]` and `tuple[str, ...]`"

View File

@ -10478,7 +10478,6 @@ impl<'db> TypeVarBoundOrConstraints<'db> {
.zip(prev_elements.iter()) .zip(prev_elements.iter())
.map(|(ty, prev_ty)| ty.cycle_normalized(db, *prev_ty, cycle)) .map(|(ty, prev_ty)| ty.cycle_normalized(db, *prev_ty, cycle))
.collect::<Box<_>>(), .collect::<Box<_>>(),
RecursivelyDefined::No,
)) ))
} }
// The choice of whether it's an upper bound or constraints is purely syntactic and // The choice of whether it's an upper bound or constraints is purely syntactic and

View File

@ -11060,12 +11060,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
"infer_binary_type_comparison should never return None for `CmpOp::Eq`", "infer_binary_type_comparison should never return None for `CmpOp::Eq`",
); );
match pairwise_eq_result.try_bool(self.db()).unwrap_or_else(|err| { match pairwise_eq_result
// TODO: We should, whenever possible, pass the range of the left and right elements .try_bool(self.db())
// instead of the range of the whole tuple. .unwrap_or_else(|err| {
err.report_diagnostic(&self.context, range); // TODO: We should, whenever possible, pass the range of the left and right elements
err.fallback_truthiness() // 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 // - AlwaysTrue : Continue to the next pair for lexicographic comparison
Truthiness::AlwaysTrue => continue, Truthiness::AlwaysTrue => continue,
// - AlwaysFalse: // - AlwaysFalse:
@ -11125,71 +11127,88 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
// compare lexicographically. However, we still need to verify that the // compare lexicographically. However, we still need to verify that the
// element types are comparable for ordering comparisons. // element types are comparable for ordering comparisons.
// For equality comparisons (==, !=), any two objects can be compared. // 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(_)) (TupleSpec::Variable(_), _) | (_, TupleSpec::Variable(_))
if matches!(op, RichCompareOperator::Eq | RichCompareOperator::Ne) => if matches!(op, RichCompareOperator::Eq | RichCompareOperator::Ne) =>
{ {
Ok(Type::unknown()) Ok(KnownClass::Bool.to_instance(self.db()))
} }
// Both variable: check all elements that could potentially be compared. // Both variable: check all elements that could potentially be compared.
(TupleSpec::Variable(left_var), TupleSpec::Variable(right_var)) => { (TupleSpec::Variable(left_var), TupleSpec::Variable(right_var)) => {
let mut builder = UnionBuilder::new(self.db());
// 1. Compare prefix elements at matching positions. // 1. Compare prefix elements at matching positions.
for (l_el, r_el) in left_var.prefix_elements().zip(right_var.prefix_elements()) { for (l_el, r_el) in left_var.prefix_elements().zip(right_var.prefix_elements()) {
self.infer_binary_type_comparison(*l_el, op.into(), *r_el, range, visitor)?; 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. // 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()) { for l_el in left_var.prefix_elements().skip(right_var.prefix.len()) {
self.infer_binary_type_comparison( builder = builder.add(self.infer_binary_type_comparison(
*l_el, *l_el,
op.into(), op.into(),
right_var.variable, right_var.variable,
range, range,
visitor, visitor,
)?; )?);
} }
// 3. Right's extra prefix elements are compared with left's variable. // 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()) { for r_el in right_var.prefix_elements().skip(left_var.prefix.len()) {
self.infer_binary_type_comparison( builder = builder.add(self.infer_binary_type_comparison(
left_var.variable, left_var.variable,
op.into(), op.into(),
*r_el, *r_el,
range, range,
visitor, visitor,
)?; )?);
} }
// 4. Variable elements can be compared at any overlapping position. // 4. Variable elements can be compared at any overlapping position.
self.infer_binary_type_comparison( builder = builder.add(self.infer_binary_type_comparison(
left_var.variable, left_var.variable,
op.into(), op.into(),
right_var.variable, right_var.variable,
range, range,
visitor, visitor,
)?; )?);
// 5. Left's extra suffix elements are compared with right's variable. // 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()) { for l_el in left_var
self.infer_binary_type_comparison( .suffix_elements()
.rev()
.skip(right_var.suffix.len())
{
builder = builder.add(self.infer_binary_type_comparison(
*l_el, *l_el,
op.into(), op.into(),
right_var.variable, right_var.variable,
range, range,
visitor, visitor,
)?; )?);
} }
// 6. Right's extra suffix elements are compared with left's variable. // 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()) { for r_el in right_var
self.infer_binary_type_comparison( .suffix_elements()
.rev()
.skip(left_var.suffix.len())
{
builder = builder.add(self.infer_binary_type_comparison(
left_var.variable, left_var.variable,
op.into(), op.into(),
*r_el, *r_el,
range, range,
visitor, visitor,
)?; )?);
} }
// 7. Compare suffix elements at matching positions (from the end). // 7. Compare suffix elements at matching positions (from the end).
@ -11198,17 +11217,34 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
.rev() .rev()
.zip(right_var.suffix_elements().rev()) .zip(right_var.suffix_elements().rev())
{ {
self.infer_binary_type_comparison(*l_el, op.into(), *r_el, range, visitor)?; builder = builder.add(self.infer_binary_type_comparison(
*l_el,
op.into(),
*r_el,
range,
visitor,
)?);
} }
Ok(Type::unknown()) // 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. // Left variable, right fixed: check which elements could be compared.
(TupleSpec::Variable(left_var), TupleSpec::Fixed(right_fixed)) => { (TupleSpec::Variable(left_var), TupleSpec::Fixed(right_fixed)) => {
let mut builder = UnionBuilder::new(self.db());
// Compare left's prefix with right's corresponding elements. // Compare left's prefix with right's corresponding elements.
for (l_el, r_el) in left_var.prefix_elements().zip(right_fixed.elements()) { for (l_el, r_el) in left_var.prefix_elements().zip(right_fixed.elements()) {
self.infer_binary_type_comparison(*l_el, op.into(), *r_el, range, visitor)?; 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). // Compare left's suffix with right's corresponding elements (from end).
@ -11217,7 +11253,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
.rev() .rev()
.zip(right_fixed.elements().rev()) .zip(right_fixed.elements().rev())
{ {
self.infer_binary_type_comparison(*l_el, op.into(), *r_el, range, visitor)?; 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 // Compare left's variable with right's "middle" elements
@ -11229,23 +11271,34 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
.skip(middle_start) .skip(middle_start)
.take(middle_end.saturating_sub(middle_start)) .take(middle_end.saturating_sub(middle_start))
{ {
self.infer_binary_type_comparison( builder = builder.add(self.infer_binary_type_comparison(
left_var.variable, left_var.variable,
op.into(), op.into(),
*r_el, *r_el,
range, range,
visitor, visitor,
)?; )?);
} }
Ok(Type::unknown()) // 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. // Left fixed, right variable: check which elements could be compared.
(TupleSpec::Fixed(left_fixed), TupleSpec::Variable(right_var)) => { (TupleSpec::Fixed(left_fixed), TupleSpec::Variable(right_var)) => {
let mut builder = UnionBuilder::new(self.db());
// Compare left's elements with right's prefix. // Compare left's elements with right's prefix.
for (l_el, r_el) in left_fixed.elements().zip(right_var.prefix_elements()) { for (l_el, r_el) in left_fixed.elements().zip(right_var.prefix_elements()) {
self.infer_binary_type_comparison(*l_el, op.into(), *r_el, range, visitor)?; 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. // Compare left's elements (from end) with right's suffix.
@ -11254,7 +11307,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
.rev() .rev()
.zip(right_var.suffix_elements().rev()) .zip(right_var.suffix_elements().rev())
{ {
self.infer_binary_type_comparison(*l_el, op.into(), *r_el, range, visitor)?; 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. // Compare left's "middle" elements with right's variable.
@ -11265,16 +11324,19 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
.skip(middle_start) .skip(middle_start)
.take(middle_end.saturating_sub(middle_start)) .take(middle_end.saturating_sub(middle_start))
{ {
self.infer_binary_type_comparison( builder = builder.add(self.infer_binary_type_comparison(
*l_el, *l_el,
op.into(), op.into(),
right_var.variable, right_var.variable,
range, range,
visitor, visitor,
)?; )?);
} }
Ok(Type::unknown()) // Length comparison (when all elements are equal) returns bool.
builder = builder.add(KnownClass::Bool.to_instance(self.db()));
Ok(builder.build())
} }
} }
} }