From 8ef82e96fcbc6a72c7395cd30415847266c23d3d Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 6 Dec 2025 10:26:56 -0500 Subject: [PATCH] Return bool --- .../resources/mdtest/comparison/tuples.md | 22 +-- ...les_with_Prefixes…_(c25079c01f6d8eb3).snap | 8 +- ...upported_Comparis…_(400a427b33d53e00).snap | 4 +- crates/ty_python_semantic/src/types.rs | 1 - .../src/types/infer/builder.rs | 128 +++++++++++++----- 5 files changed, 112 insertions(+), 51 deletions(-) diff --git a/crates/ty_python_semantic/resources/mdtest/comparison/tuples.md b/crates/ty_python_semantic/resources/mdtest/comparison/tuples.md index 83bf091f44..97c712735a 100644 --- a/crates/ty_python_semantic/resources/mdtest/comparison/tuples.md +++ b/crates/ty_python_semantic/resources/mdtest/comparison/tuples.md @@ -352,8 +352,8 @@ def f( c: tuple[str], ): # Equality comparisons are always valid - reveal_type(a == b) # revealed: Unknown - reveal_type(a != b) # revealed: Unknown + 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, ...]`" @@ -400,16 +400,16 @@ 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: Unknown - reveal_type(a != b) # revealed: Unknown - reveal_type(a < b) # revealed: Unknown - reveal_type(a <= b) # revealed: Unknown - reveal_type(a > b) # revealed: Unknown - reveal_type(a >= b) # revealed: Unknown + 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: Unknown - reveal_type(c < a) # revealed: Unknown + reveal_type(a < c) # revealed: bool + reveal_type(c < a) # revealed: bool ``` ### Tuples with Prefixes and Suffixes @@ -441,7 +441,7 @@ def _( 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: Unknown + reveal_type(prefix_int_var_int < prefix_int_var_bool) # revealed: bool ``` ## Chained comparisons with elements that incorrectly implement `__bool__` diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Homogeneous_-_Tuples_with_Prefixes…_(c25079c01f6d8eb3).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Homogeneous_-_Tuples_with_Prefixes…_(c25079c01f6d8eb3).snap index 27ffcc45ea..6b5881dde5 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Homogeneous_-_Tuples_with_Prefixes…_(c25079c01f6d8eb3).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Homogeneous_-_Tuples_with_Prefixes…_(c25079c01f6d8eb3).snap @@ -16,15 +16,15 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/comparison/tuples.md 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 - NOT ok + 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 - all ok -13 | reveal_type(prefix_int_var_int < prefix_int_var_bool) # revealed: Unknown +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 @@ -33,7 +33,7 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/comparison/tuples.md error[unsupported-operator]: Unsupported `<` operation --> 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] 7 | prefix_int_var_str < prefix_str_var_int | ------------------^^^------------------ diff --git a/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Homogeneous_-_Unsupported_Comparis…_(400a427b33d53e00).snap b/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Homogeneous_-_Unsupported_Comparis…_(400a427b33d53e00).snap index 54b517e6b8..47d2dc93f9 100644 --- a/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Homogeneous_-_Unsupported_Comparis…_(400a427b33d53e00).snap +++ b/crates/ty_python_semantic/resources/mdtest/snapshots/tuples.md_-_Comparison___Tuples_-_Homogeneous_-_Unsupported_Comparis…_(400a427b33d53e00).snap @@ -18,8 +18,8 @@ mdtest path: crates/ty_python_semantic/resources/mdtest/comparison/tuples.md 4 | c: tuple[str], 5 | ): 6 | # Equality comparisons are always valid - 7 | reveal_type(a == b) # revealed: Unknown - 8 | reveal_type(a != b) # revealed: Unknown + 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, ...]`" diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index e235802388..23f7a53f79 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -10478,7 +10478,6 @@ impl<'db> TypeVarBoundOrConstraints<'db> { .zip(prev_elements.iter()) .map(|(ty, prev_ty)| ty.cycle_normalized(db, *prev_ty, cycle)) .collect::>(), - RecursivelyDefined::No, )) } // The choice of whether it's an upper bound or constraints is purely syntactic and diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 01ea2673cc..bfcb1b9a46 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -11060,12 +11060,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { "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() - }) { + 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: @@ -11125,71 +11127,88 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // 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. + // 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(Type::unknown()) + 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()) { - 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. 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, 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()) { - self.infer_binary_type_comparison( + 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. - self.infer_binary_type_comparison( + 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()) { - self.infer_binary_type_comparison( + 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()) { - self.infer_binary_type_comparison( + 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). @@ -11198,17 +11217,34 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .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. (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()) { - 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). @@ -11217,7 +11253,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .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 @@ -11229,23 +11271,34 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .skip(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, 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 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()) { - 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. @@ -11254,7 +11307,13 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .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. @@ -11265,16 +11324,19 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { .skip(middle_start) .take(middle_end.saturating_sub(middle_start)) { - self.infer_binary_type_comparison( + builder = builder.add(self.infer_binary_type_comparison( *l_el, op.into(), right_var.variable, 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()) } } }