Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Expand Up @@ -289,9 +289,12 @@ from typing import TypeVar
T = TypeVar("T", int, str)

def same_constrained_types(t1: T, t2: T) -> T:
# TODO: no error
# error: [unsupported-operator] "Operator `+` is not supported between two objects of type `T@same_constrained_types`"
return t1 + t2

S = TypeVar("S", int, float)

def chained_constrained_types(t1: S, t2: S, t3: S) -> S:
return (t1 + t2) * t3
```

This is _not_ the same as a union type, because of this additional constraint that the two
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -258,9 +258,10 @@ methods that are compatible with the return type, so the `return` expression is

```py
def same_constrained_types[T: (int, str)](t1: T, t2: T) -> T:
# TODO: no error
# error: [unsupported-operator] "Operator `+` is not supported between two objects of type `T@same_constrained_types`"
return t1 + t2

def chained_constrained_types[T: (int, float)](t1: T, t2: T, t3: T) -> T:
return (t1 + t2) * t3
```

This is _not_ the same as a union type, because of this additional constraint that the two
Expand Down
74 changes: 71 additions & 3 deletions crates/ty_python_semantic/src/types/infer/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -122,9 +122,10 @@ use crate::types::{
ParameterForm, Parameters, Signature, SpecialFormType, StaticClassLiteral, SubclassOfType,
TrackedConstraintSet, Truthiness, Type, TypeAliasType, TypeAndQualifiers, TypeContext,
TypeQualifiers, TypeVarBoundOrConstraints, TypeVarBoundOrConstraintsEvaluation,
TypeVarDefaultEvaluation, TypeVarIdentity, TypeVarInstance, TypeVarKind, TypeVarVariance,
TypedDictType, UnionBuilder, UnionType, UnionTypeInstance, any_over_type, binding_type,
definition_expression_type, infer_complete_scope_types, infer_scope_types, todo_type,
TypeVarConstraints, TypeVarDefaultEvaluation, TypeVarIdentity, TypeVarInstance, TypeVarKind,
TypeVarVariance, TypedDictType, UnionBuilder, UnionType, UnionTypeInstance, any_over_type,
binding_type, definition_expression_type, infer_complete_scope_types, infer_scope_types,
todo_type,
};
use crate::types::{CallableTypes, overrides};
use crate::types::{ClassBase, add_inferred_python_version_hint_to_diagnostic};
Expand Down Expand Up @@ -12441,6 +12442,34 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
}

/// Maps an operation over each constraint of a constrained `TypeVar`.
///
/// Returns the original `TypeVar` if each result is equivalent to its input constraint;
/// otherwise returns the union of all results.
fn map_constrained_typevar_constraints(
db: &'db dyn Db,
typevar: Type<'db>,
constraints: TypeVarConstraints<'db>,
mut op: impl FnMut(Type<'db>) -> Option<Type<'db>>,
) -> Option<Type<'db>> {
let mut builder = UnionBuilder::new(db);
let mut any_different = false;

for constraint in constraints.elements(db) {
let result = op(*constraint)?;
if !result.is_equivalent_to(db, *constraint) {
any_different = true;
}
builder = builder.add(result);
}

Some(if any_different {
builder.build()
} else {
typevar
})
}

fn infer_binary_expression_type(
&mut self,
node: AnyNodeRef<'_>,
Expand Down Expand Up @@ -12505,6 +12534,45 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
op,
),

// When both operands are the same constrained TypeVar (e.g., `T: (int, str)`),
// we check if the operation is valid for each constraint paired with itself.
// This is different from treating it as a union, where we'd check all combinations.
// For example, `T + T` where `T: (int, str)` should check `int + int` and `str + str`,
// not `int + str` which would fail.
//
// If each constraint's operation returns the same type as the constraint (e.g.,
// `int + int -> int`), we return the TypeVar to preserve the generic relationship.
// Otherwise, we return the union of the return types.
//
// TODO: We expect to replace this with more general support for handling constrained TypeVars
// in arbitrary method/function calls.
(Type::TypeVar(left_tvar), Type::TypeVar(right_tvar), _)
if left_tvar.identity(self.db()) == right_tvar.identity(self.db()) =>
{
match left_tvar.typevar(self.db()).bound_or_constraints(self.db()) {
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
Self::map_constrained_typevar_constraints(
self.db(),
left_ty,
constraints,
|constraint| {
self.infer_binary_expression_type(
node,
emitted_division_by_zero_diagnostic,
constraint,
constraint,
op,
)
},
)
}
// For bounded TypeVars or unconstrained TypeVars, fall through to the default handling.
_ => Type::try_call_bin_op(self.db(), left_ty, op, right_ty)
.map(|outcome| outcome.return_type(self.db()))
.ok(),
}
}

// `try_call_bin_op` works for almost all `NewType`s, but not for `NewType`s of `float`
// and `complex`, where the concrete base type is a union. In that case it turns out
// the `self` types of the dunder methods in typeshed don't match, because they don't
Expand Down