diff --git a/crates/ty_python_semantic/resources/mdtest/assignment/annotations.md b/crates/ty_python_semantic/resources/mdtest/assignment/annotations.md index b9e37b651811b..18f185f351fb1 100644 --- a/crates/ty_python_semantic/resources/mdtest/assignment/annotations.md +++ b/crates/ty_python_semantic/resources/mdtest/assignment/annotations.md @@ -615,8 +615,7 @@ e: list[Any] | None = [1] reveal_type(e) # revealed: list[Any] f: list[Any] | None = f2(1) -# TODO: Better constraint solver. -reveal_type(f) # revealed: list[int] | None +reveal_type(f) # revealed: list[Any] | None g: list[Any] | dict[Any, Any] = f3(1) # TODO: Better constraint solver. diff --git a/crates/ty_python_semantic/resources/mdtest/promotion.md b/crates/ty_python_semantic/resources/mdtest/promotion.md index c0987d7613234..1ddedb2c5b832 100644 --- a/crates/ty_python_semantic/resources/mdtest/promotion.md +++ b/crates/ty_python_semantic/resources/mdtest/promotion.md @@ -238,7 +238,7 @@ x11: list[Literal[1] | Literal[2] | Literal[3]] = [1, 2, 3] reveal_type(x11) # revealed: list[Literal[1, 2, 3]] x12: Y[Y[Literal[1]]] = [[1]] -reveal_type(x12) # revealed: list[Y[Literal[1]]] +reveal_type(x12) # revealed: list[list[Literal[1]]] x13: list[tuple[Literal[1], Literal[2], Literal[3]]] = [(1, 2, 3)] reveal_type(x13) # revealed: list[tuple[Literal[1], Literal[2], Literal[3]]] diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index c406b2e201a92..f490e097a5574 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -4,7 +4,6 @@ use ruff_diagnostics::{Edit, Fix}; use rustc_hash::FxHashMap; use std::borrow::Cow; -use std::cell::RefCell; use std::time::Duration; use bitflags::bitflags; @@ -51,7 +50,7 @@ use crate::types::bound_super::BoundSuperType; use crate::types::call::{Binding, Bindings, CallArguments, CallableBinding}; pub(crate) use crate::types::callable::{CallableType, CallableTypes}; pub(crate) use crate::types::class_base::ClassBase; -use crate::types::constraints::ConstraintSetBuilder; +use crate::types::constraints::{ConstraintSetBuilder, Solutions}; use crate::types::context::{LintDiagnosticGuard, LintDiagnosticGuardBuilder}; use crate::types::diagnostic::{INVALID_AWAIT, INVALID_TYPE_FORM}; pub use crate::types::display::{DisplaySettings, TypeDetail, TypeDisplayDetails}; @@ -60,10 +59,10 @@ use crate::types::function::{ DataclassTransformerFlags, DataclassTransformerParams, FunctionDecorators, FunctionSpans, FunctionType, KnownFunction, }; +pub(crate) use crate::types::generics::GenericContext; use crate::types::generics::{ ApplySpecialization, InferableTypeVars, Specialization, bind_typevar, }; -pub(crate) use crate::types::generics::{GenericContext, SpecializationBuilder}; use crate::types::infer::InferenceFlags; use crate::types::known_instance::{InternedConstraintSet, InternedType, UnionTypeInstance}; pub use crate::types::method::{BoundMethodType, KnownBoundMethodType, WrapperDescriptorKind}; @@ -1902,17 +1901,38 @@ impl<'db> Type<'db> { let generic_context = specialization.generic_context(db); // Collect the type mappings used to narrow the type context. - let tcx_mappings = { - let mut builder = - SpecializationBuilder::new(db, generic_context.inferable_typevars(db)); - - if let Some(tcx) = tcx.annotation { + // + // We use a forward CSA check (`alias_instance ≤ tcx`) to infer what each typevar + // in the identity specialization maps to in the type context. For example, if + // `tcx = list[int]` and `alias_instance = list[T]`, the CSA produces `T = int`. + let tcx_mappings: FxHashMap<_, _> = tcx + .annotation + .and_then(|tcx| { let alias_instance = Type::instance(db, class_literal.identity_specialization(db)); - let _ = builder.infer_reverse(constraints, tcx, alias_instance); - } - - builder.into_type_mappings() - }; + let set = alias_instance.when_constraint_set_assignable_to(db, tcx, constraints); + match set.solutions(db, constraints) { + Solutions::Constrained(solutions) => { + let mut mappings = FxHashMap::default(); + for solution in solutions.iter() { + for binding in solution { + mappings + .entry(binding.bound_typevar.identity(db)) + .and_modify(|existing| { + *existing = UnionType::from_two_elements( + db, + *existing, + binding.solution, + ); + }) + .or_insert(binding.solution); + } + } + Some(mappings) + } + _ => None, + } + }) + .unwrap_or_default(); for (type_var, ty) in generic_context.variables(db).zip(specialization.types(db)) { let variance = type_var.variance_with_polarity(db, polarity); @@ -5501,12 +5521,6 @@ impl<'db> Type<'db> { match type_mapping { TypeMapping::EagerExpansion => unreachable!("handled above"), - // For UniqueSpecialization, get raw value type, apply specialization, then apply mapping. - TypeMapping::UniqueSpecialization { .. } => { - let value_type = alias.raw_value_type(db); - alias.apply_function_specialization(db, value_type).apply_type_mapping_impl(db, type_mapping, tcx, visitor) - } - _ => { let value_type = alias.raw_value_type(db).apply_type_mapping_impl(db, type_mapping, tcx, visitor); alias.apply_function_specialization(db, value_type).apply_type_mapping_impl(db, type_mapping, tcx, visitor) @@ -5531,7 +5545,6 @@ impl<'db> Type<'db> { Type::LiteralValue(_) => match type_mapping { TypeMapping::ApplySpecialization(_) | TypeMapping::ApplySpecializationWithMaterialization { .. } | - TypeMapping::UniqueSpecialization { .. } | TypeMapping::BindLegacyTypevars(_) | TypeMapping::BindSelf { .. } | TypeMapping::ReplaceSelf { .. } | @@ -5547,7 +5560,6 @@ impl<'db> Type<'db> { Type::Dynamic(_) => match type_mapping { TypeMapping::ApplySpecialization(_) | TypeMapping::ApplySpecializationWithMaterialization { .. } | - TypeMapping::UniqueSpecialization { .. } | TypeMapping::BindLegacyTypevars(_) | TypeMapping::BindSelf(..) | TypeMapping::ReplaceSelf { .. } | @@ -6328,11 +6340,6 @@ pub enum TypeMapping<'a, 'db> { specialization: ApplySpecialization<'a, 'db>, materialization_kind: MaterializationKind, }, - /// Resets any specializations to contain unique synthetic type variables. - UniqueSpecialization { - // A list of synthetic type variables, and the types they replaced. - specialization: RefCell, Type<'db>)>>, - }, /// Replaces any literal types with their corresponding promoted type form (e.g. `Literal["string"]` /// to `str`, or `def _() -> int` to `Callable[[], int]`). Promote(PromotionMode, PromotionKind), @@ -6384,8 +6391,7 @@ impl<'db> TypeMapping<'_, 'db> { }), ) } - TypeMapping::UniqueSpecialization { .. } - | TypeMapping::Promote(..) + TypeMapping::Promote(..) | TypeMapping::BindLegacyTypevars(_) | TypeMapping::Materialize(_) | TypeMapping::ReplaceParameterDefaults @@ -6430,7 +6436,6 @@ impl<'db> TypeMapping<'_, 'db> { }, TypeMapping::Promote(mode, kind) => TypeMapping::Promote(mode.flip(), *kind), TypeMapping::ApplySpecialization(_) - | TypeMapping::UniqueSpecialization { .. } | TypeMapping::BindLegacyTypevars(_) | TypeMapping::BindSelf(..) | TypeMapping::ReplaceSelf { .. } diff --git a/crates/ty_python_semantic/src/types/call/bind.rs b/crates/ty_python_semantic/src/types/call/bind.rs index 35cccec6df633..d3f7e61f601bc 100644 --- a/crates/ty_python_semantic/src/types/call/bind.rs +++ b/crates/ty_python_semantic/src/types/call/bind.rs @@ -27,7 +27,7 @@ use crate::place::{DefinedPlace, Definedness, Place, known_module_symbol}; use crate::subscript::PyIndex; use crate::types::call::arguments::{CallArgumentTypes, Expansion, is_expandable_type}; use crate::types::callable::CallableTypeKind; -use crate::types::constraints::{ConstraintSet, ConstraintSetBuilder}; +use crate::types::constraints::{ConstraintSet, ConstraintSetBuilder, PathBounds, Solutions}; use crate::types::diagnostic::{ CALL_NON_CALLABLE, CALL_TOP_CALLABLE, CONFLICTING_ARGUMENT_FORMS, INVALID_ARGUMENT_TYPE, INVALID_DATACLASS, MISSING_ARGUMENT, NO_MATCHING_OVERLOAD, PARAMETER_ALREADY_ASSIGNED, @@ -3754,7 +3754,7 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { .zip(self.call_expression_tcx.annotation); self.inferable_typevars = generic_context.inferable_typevars(self.db); - let mut builder = SpecializationBuilder::new(self.db, self.inferable_typevars); + let mut builder = SpecializationBuilder::new(self.db, constraints, self.inferable_typevars); // Type variables for which we inferred a declared type based on a partially specialized // type from an outer generic context. For these type variables, we may infer types that @@ -3763,54 +3763,129 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { let mut partially_specialized_declared_type: FxHashSet> = FxHashSet::default(); - // Attempt to to solve the specialization while preferring the declared type of non-covariant + // Attempt to solve the specialization while preferring the declared type of non-covariant // type parameters from generic classes. + // + // We use an assignability check (`return_ty ≤ tcx`) to infer what each typevar in the + // function's return type maps to in the type context. (We use _constraint set_ + // assignability so that we get a constraint set describing the typevars.) For example, if + // the return type is `list[T]` and the type context is `list[int]`, the check produces + // `T = int`, from which we extract the preferred type `int`. + // + // TODO: This two-phase approach (extract preferred types from the type context, then check + // argument compatibility) should eventually be replaced by conjoining the type context + // constraint set directly with the argument constraint sets in the builder. The current + // solution-level filtering (variance, inferable typevars, concrete content) works around + // extracting solutions too early. When the builder maintains a single constraint set, the + // combined set `(return_ty ≤ tcx) ∧ (∧ᵢ actual_i ≤ formal_i)` will naturally resolve the + // tension between type context preferences and argument constraints. If the combined set + // is unsatisfiable, we will fall back to argument constraints alone (which the current + // code does via `assignable_to_declared_type`). let preferred_type_mappings = return_with_tcx .and_then(|(return_ty, tcx)| { tcx.filter_union(self.db, |ty| ty.class_specialization(self.db).is_some()) .class_specialization(self.db)?; - builder - .infer_reverse_map( - constraints, - tcx, - return_ty, - |(identity, variance, inferred_ty)| { - // Avoid unnecessarily widening the return type based on a covariant - // type parameter from the type context, as it can lead to argument - // assignability errors if the type variable is constrained by a narrower - // parameter type. - if variance.is_covariant() { - return None; - } + let return_ty = + return_ty.filter_disjoint_elements(self.db, tcx, self.inferable_typevars); + let tcx = tcx.filter_disjoint_elements(self.db, return_ty, self.inferable_typevars); + let set = return_ty.when_constraint_set_assignable_to(self.db, tcx, constraints); + + // Use `solutions_with` to determine per-typevar variance from the raw + // lower/upper bounds on each BDD path. + let mut variance_map: FxHashMap, TypeVarVariance> = + FxHashMap::default(); + let solutions = set.solutions_with_inferable( + self.db, + constraints, + self.inferable_typevars, + |typevar, variance, lower, upper| { + if !typevar.is_inferable(self.db, self.inferable_typevars) { + return Ok(None); + } - // Avoid inferring a preferred type based on partially specialized type context - // from an outer generic call. If the type context is a union, we try to keep - // any concrete elements. - let inferred_ty = inferred_ty.filter_union(self.db, |ty| { - if ty.has_unspecialized_type_var(self.db) { - partially_specialized_declared_type.insert(identity); - false - } else { - true - } - }); - if inferred_ty.has_unspecialized_type_var(self.db) { - return None; + let identity = typevar.identity(self.db); + variance_map + .entry(identity) + .and_modify(|current| *current = current.join(variance)) + .or_insert(variance); + PathBounds::default_solve(self.db, typevar, lower, upper) + }, + ); + + let Solutions::Constrained(solutions) = solutions else { + return None; + }; + + let mut preferred: FxHashMap, Type<'db>> = + FxHashMap::default(); + + for solution in &solutions { + for binding in solution { + let identity = binding.bound_typevar.identity(self.db); + + // Avoid unnecessarily widening the return type based on a covariant + // type parameter from the type context, as it can lead to argument + // assignability errors if the type variable is constrained by a narrower + // parameter type. + if variance_map + .get(&identity) + .is_some_and(|v| v.is_covariant()) + { + continue; + } + + // Filter out inferable typevars (cross-typevar references from + // SequentMap transitivity) and unspecialized typevars (from partially + // specialized contexts). + let inferred_ty = binding.solution.filter_union(self.db, |ty| { + if ty.has_unspecialized_type_var(self.db) { + partially_specialized_declared_type.insert(identity); + return false; } + true + }); + if inferred_ty.has_unspecialized_type_var(self.db) { + continue; + } - Some(inferred_ty) - }, - ) - .ok()?; + // Skip preferred types where every non-TypeVar union element still + // deeply contains non-inferable typevars. Such types (e.g., + // `T@h | list[T@h]` from an outer generic scope) don't provide + // useful concrete information and would cause over-expansion. + let concrete_content = + inferred_ty.filter_union(self.db, |ty| !ty.has_typevar(self.db)); + if concrete_content.is_never() && inferred_ty.has_typevar(self.db) { + continue; + } + + preferred + .entry(identity) + .and_modify(|existing| { + *existing = + UnionType::from_two_elements(self.db, *existing, inferred_ty); + }) + .or_insert(inferred_ty); + } + } + + // Add preferred types to the builder so they serve as the base mapping + // when argument inference adds more types. + for solution in &solutions { + for binding in solution { + let identity = binding.bound_typevar.identity(self.db); + if let Some(&ty) = preferred.get(&identity) { + builder.insert_type_mapping(binding.bound_typevar, ty); + } + } + } - Some(builder.type_mappings().clone()) + Some(preferred) }) .unwrap_or_default(); let mut specialization_errors = Vec::new(); let assignable_to_declared_type = self.infer_argument_constraints( - constraints, &mut builder, &preferred_type_mappings, &partially_specialized_declared_type, @@ -3823,11 +3898,10 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { // Note that this will still lead to an invalid specialization, but may // produce more precise diagnostics. if !assignable_to_declared_type { - builder = SpecializationBuilder::new(self.db, self.inferable_typevars); + builder = SpecializationBuilder::new(self.db, constraints, self.inferable_typevars); specialization_errors.clear(); self.infer_argument_constraints( - constraints, &mut builder, &FxHashMap::default(), &FxHashSet::default(), @@ -3838,64 +3912,64 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { self.errors.extend(specialization_errors); // Attempt to promote any promotable types assigned to the specialization. - let maybe_promote = |typevar: BoundTypeVarInstance<'db>, ty: Type<'db>| { - let bound_or_constraints = typevar.typevar(self.db).bound_or_constraints(self.db); - - // For constrained TypeVars, the inferred type is already one of the - // constraints. Promoting literals would produce a type that doesn't - // match any constraint. - if matches!( - bound_or_constraints, - Some(TypeVarBoundOrConstraints::Constraints(_)) - ) { - return ty; - } + // The hook receives (typevar, lower_bound, upper_bound) and returns Some(ty) to + // override the default solution, or None to keep it. + let maybe_promote = + |typevar: BoundTypeVarInstance<'db>, lower: Type<'db>, _upper: Type<'db>| { + let bound_or_constraints = typevar.typevar(self.db).bound_or_constraints(self.db); + + // For constrained TypeVars, the inferred type is already one of the + // constraints. Promoting literals would produce a type that doesn't + // match any constraint. + if matches!( + bound_or_constraints, + Some(TypeVarBoundOrConstraints::Constraints(_)) + ) { + return None; + } - let return_ty = self.constructor_instance_type.unwrap_or(self.return_ty); - let mut variance_in_return = TypeVarVariance::Bivariant; + let return_ty = self.constructor_instance_type.unwrap_or(self.return_ty); + let mut variance_in_return = TypeVarVariance::Bivariant; - // Find all occurrences of the type variable in the return type. - let visit_return_ty = |_, ty, variance, _| { - if ty != Type::TypeVar(typevar) { - return; - } + // Find all occurrences of the type variable in the return type. + let visit_return_ty = |_, ty, variance, _| { + if ty != Type::TypeVar(typevar) { + return; + } - variance_in_return = variance_in_return.join(variance); - }; + variance_in_return = variance_in_return.join(variance); + }; - return_ty.visit_specialization(self.db, self.call_expression_tcx, visit_return_ty); + return_ty.visit_specialization(self.db, self.call_expression_tcx, visit_return_ty); - // Promotion is only useful if the type variable is in invariant or contravariant - // position in the return type. - if variance_in_return.is_covariant() { - return ty; - } + // Promotion is only useful if the type variable is in invariant or contravariant + // position in the return type. + if variance_in_return.is_covariant() { + return None; + } - let promoted = ty.promote(self.db); + let promoted = lower.promote(self.db); - // If the TypeVar has an upper bound, only use the promoted type if it - // still satisfies the bound. - if let Some(TypeVarBoundOrConstraints::UpperBound(bound)) = bound_or_constraints { - if !promoted.is_assignable_to(self.db, bound) { - return ty; + // If the TypeVar has an upper bound, only use the promoted type if it + // still satisfies the bound. + if let Some(TypeVarBoundOrConstraints::UpperBound(bound)) = bound_or_constraints { + if !promoted.is_assignable_to(self.db, bound) { + return None; + } } - } - promoted - }; + Some(promoted) + }; - let specialization = builder - .mapped(generic_context, maybe_promote) - .build(generic_context); + let specialization = builder.build_with(generic_context, maybe_promote); self.return_ty = self.return_ty.apply_specialization(self.db, specialization); self.specialization = Some(specialization); } - fn infer_argument_constraints( + fn infer_argument_constraints<'c>( &mut self, - constraints: &ConstraintSetBuilder<'db>, - builder: &mut SpecializationBuilder<'db>, + builder: &mut SpecializationBuilder<'db, 'c>, preferred_type_mappings: &FxHashMap, Type<'db>>, partially_specialized_declared_type: &FxHashSet>, specialization_errors: &mut Vec>, @@ -3913,7 +3987,6 @@ impl<'a, 'db> ArgumentTypeChecker<'a, 'db> { let argument_type = argument_types.get_for_declared_type(declared_type); let specialization_result = builder.infer_map( - constraints, declared_type, variadic_argument_type.unwrap_or(argument_type), |(identity, _, inferred_ty)| { diff --git a/crates/ty_python_semantic/src/types/class.rs b/crates/ty_python_semantic/src/types/class.rs index d435ebcf02d0e..d6e76197b01b6 100644 --- a/crates/ty_python_semantic/src/types/class.rs +++ b/crates/ty_python_semantic/src/types/class.rs @@ -1842,6 +1842,24 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> { source: ClassType<'db>, target: ClassType<'db>, ) -> ConstraintSet<'db, 'c> { + // Fast path: if source and target are the same class (possibly with different + // specializations), we can compare them directly without walking the MRO. + match (source, target) { + (ClassType::NonGeneric(source), ClassType::NonGeneric(target)) if source == target => { + return self.always(); + } + (ClassType::Generic(source_alias), ClassType::Generic(target_alias)) + if source_alias.origin(db) == target_alias.origin(db) => + { + return self.check_specialization_pair( + db, + source_alias.specialization(db), + target_alias.specialization(db), + ); + } + _ => {} + } + source.iter_mro(db).when_any(db, self.constraints, |base| { match base { ClassBase::Dynamic(_) => match self.relation { diff --git a/crates/ty_python_semantic/src/types/constraints.rs b/crates/ty_python_semantic/src/types/constraints.rs index 2250b5bf12e9f..308bd1479ad52 100644 --- a/crates/ty_python_semantic/src/types/constraints.rs +++ b/crates/ty_python_semantic/src/types/constraints.rs @@ -106,7 +106,8 @@ use crate::types::visitor::{ TypeCollector, TypeVisitor, any_over_type, walk_type_with_recursion_guard, }; use crate::types::{ - BoundTypeVarInstance, IntersectionType, Type, TypeVarBoundOrConstraints, UnionType, + BoundTypeVarInstance, IntersectionType, Type, TypeVarBoundOrConstraints, TypeVarVariance, + UnionType, }; use crate::{Db, FxIndexMap, FxIndexSet}; @@ -337,6 +338,14 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { /// neither bound _is_ a typevar. And it's not something we can create a specialization from, /// since we would endlessly substitute until we stack overflow. pub(crate) fn is_cyclic(self, db: &'db dyn Db) -> bool { + self.is_cyclic_impl(db, None) + } + + fn is_cyclic_impl( + self, + db: &'db dyn Db, + inferable: Option>, + ) -> bool { #[derive(Default)] struct CollectReachability<'db> { reachable_typevars: RefCell>>, @@ -403,21 +412,33 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { false } - // First find all of the typevars that each constraint directly mentions. + // First find all of the typevars that each constraint directly mentions. When `inferable` + // is provided, only include inferable typevars as sources and targets in the graph. let mut reachable_typevars: FxHashMap< BoundTypeVarIdentity<'db>, FxHashSet>, > = FxHashMap::default(); self.node .for_each_constraint(self.builder, &mut |constraint, _| { - let visitor = CollectReachability::default(); let constraint = self.builder.constraint_data(constraint); + let identity = constraint.typevar.identity(db); + if inferable.is_some_and(|inferable| !identity.is_inferable(inferable)) { + return; + } + let visitor = CollectReachability::default(); visitor.visit_type(db, constraint.lower); visitor.visit_type(db, constraint.upper); - reachable_typevars - .entry(constraint.typevar.identity(db)) - .or_default() - .extend(visitor.reachable_typevars.into_inner()); + let reachable = visitor.reachable_typevars.into_inner(); + let entry = reachable_typevars.entry(identity).or_default(); + if let Some(inferable) = inferable { + entry.extend( + reachable + .into_iter() + .filter(|tv| tv.is_inferable(inferable)), + ); + } else { + entry.extend(reachable); + } }); // Then perform a depth-first search to see if there are any cycles. @@ -597,7 +618,7 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { self, db: &'db dyn Db, builder: &'c ConstraintSetBuilder<'db>, - ) -> Solutions<'db, 'c> { + ) -> Solutions>>> { self.verify_builder(builder); // If the constraint set is cyclic, we'll hit an infinite expansion when trying to add type @@ -609,6 +630,40 @@ impl<'db, 'c> ConstraintSet<'db, 'c> { self.node.solutions(db, builder) } + /// Computes solutions for each BDD path, using a caller-provided hook to select solutions. + /// + /// We only consider cycles among inferable typevars. Non-inferable typevars (e.g., from outer + /// scopes that appear due to BDD constraint reordering) are skipped during both cycle + /// detection and solution extraction. + /// + /// The `choose` hook is called for each typevar on each BDD path with the typevar's + /// materialized lower and upper bounds. It returns: + /// - `Some(ty)` to use `ty` as the solution for this typevar on this path + /// - `None` to fall back to the default solution selection logic + /// + /// For multi-path BDDs, the hook is called per-path. The caller is responsible for combining + /// results across paths (typically via union). + pub(crate) fn solutions_with_inferable( + self, + db: &'db dyn Db, + builder: &'c ConstraintSetBuilder<'db>, + inferable: InferableTypeVars<'_, 'db>, + choose: impl FnMut( + BoundTypeVarInstance<'db>, + TypeVarVariance, + Type<'db>, + Type<'db>, + ) -> Result>, ()>, + ) -> Solutions>> { + self.verify_builder(builder); + + if self.is_cyclic_impl(db, Some(inferable)) { + return Solutions::Unsatisfiable; + } + + self.node.solutions_with(db, builder, choose) + } + #[expect(dead_code)] // Keep this around for debugging purposes pub(crate) fn display(self, db: &'db dyn Db) -> impl Display { self.node @@ -1724,7 +1779,7 @@ impl NodeId { self, db: &'db dyn Db, builder: &'c ConstraintSetBuilder<'db>, - ) -> Solutions<'db, 'c> { + ) -> Solutions>>> { match self.node() { Node::AlwaysTrue => Solutions::Unconstrained, Node::AlwaysFalse => Solutions::Unsatisfiable, @@ -1732,6 +1787,24 @@ impl NodeId { } } + fn solutions_with<'db>( + self, + db: &'db dyn Db, + builder: &ConstraintSetBuilder<'db>, + choose: impl FnMut( + BoundTypeVarInstance<'db>, + TypeVarVariance, + Type<'db>, + Type<'db>, + ) -> Result>, ()>, + ) -> Solutions>> { + match self.node() { + Node::AlwaysTrue => Solutions::Unconstrained, + Node::AlwaysFalse => Solutions::Unsatisfiable, + Node::Interior(interior) => interior.solutions_with(db, builder, choose), + } + } + /// Returns the negation of this BDD. fn negate(self, builder: &ConstraintSetBuilder<'_>) -> Self { match self.node() { @@ -2598,6 +2671,252 @@ struct InteriorNodeData { max_source_order: usize, } +/// Accumulated lower and upper bounds for a single typevar on a single BDD path. +/// +/// Lower bounds are collected into a union (they are alternatives for the minimum type the +/// typevar can specialize to). Upper bounds are collected into an intersection (the typevar +/// must satisfy all of them simultaneously). +#[derive(Default)] +struct Bounds<'db> { + lower: FxIndexSet>, + upper: FxIndexSet>, +} + +impl<'db> Bounds<'db> { + fn add_lower(&mut self, _db: &'db dyn Db, ty: Type<'db>) { + // Lower bounds are unioned. Our type representation is in DNF, so unioning a new + // element is typically cheap (in that it does not involve a combinatorial + // explosion from distributing the clause through an existing disjunction). So we + // don't need to be as clever here as in `add_upper`. + self.lower.insert(ty); + } + + fn add_upper(&mut self, db: &'db dyn Db, ty: Type<'db>) { + // Upper bounds are intersectioned. If `ty` is a union, that involves distributing + // the union elements through the existing type. That makes it worth checking first + // whether any of the types in the upper bound are redundant. + + // First check if there's an existing upper bound clause that is a subtype of the + // new type. If so, adding the new type does nothing to the intersection. + if self + .upper + .iter() + .any(|existing| existing.is_redundant_with(db, ty)) + { + return; + } + + // Otherwise remove any existing clauses that are a supertype of the new type, + // since the intersection will clip them to the new type. + self.upper + .retain(|existing| !ty.is_redundant_with(db, *existing)); + self.upper.insert(ty); + } +} + +/// Materialized lower and upper bounds for a single typevar on a single BDD path. +struct TypeVarBounds<'db> { + bound_typevar: BoundTypeVarInstance<'db>, + /// The union of all lower bounds on this path. + lower: Type<'db>, + /// The intersection of all upper bounds on this path (NOT including the typevar's declared + /// upper bound). + upper: Type<'db>, +} + +/// Per-path bounds for all typevars. Each element is the set of typevar bounds for one BDD path. +pub(crate) struct PathBounds<'db>(Vec>>); + +impl<'db> PathBounds<'db> { + /// Computes sorted BDD paths and accumulates per-typevar lower/upper bounds for each path. + /// + /// Returns a list of paths, where each path contains the materialized lower/upper bounds for + /// each typevar that appears in the path's constraints. + fn compute(db: &'db dyn Db, builder: &ConstraintSetBuilder<'db>, node: NodeId) -> Self { + // Sort the constraints in each path by their `source_order`s, to ensure that we construct + // any unions or intersections in our type mappings in a stable order. Constraints might + // come out of `PathAssignment`s with identical `source_order`s, but if they do, those + // "tied" constraints will still be ordered in a stable way. So we need a stable sort to + // retain that stable per-tie ordering. + let mut sorted_paths = Vec::new(); + node.for_each_path(db, builder, |path| { + let mut path: Vec<_> = path.positive_constraints().collect(); + path.sort_by_key(|(_, source_order)| *source_order); + sorted_paths.push(path); + }); + sorted_paths.sort_by(|path1, path2| { + let source_orders1 = path1.iter().map(|(_, source_order)| *source_order); + let source_orders2 = path2.iter().map(|(_, source_order)| *source_order); + source_orders1.cmp(source_orders2) + }); + + let mut result = Vec::with_capacity(sorted_paths.len()); + let mut mappings: FxHashMap, Bounds<'db>> = FxHashMap::default(); + + for path in sorted_paths { + mappings.clear(); + for (constraint, _) in path { + let constraint = builder.constraint_data(constraint); + let typevar = constraint.typevar; + let lower = constraint.lower; + let upper = constraint.upper; + let bounds = mappings.entry(typevar).or_default(); + bounds.add_lower(db, lower); + bounds.add_upper(db, upper); + + if let Type::TypeVar(lower_bound_typevar) = lower { + let bounds = mappings.entry(lower_bound_typevar).or_default(); + bounds.add_upper(db, Type::TypeVar(typevar)); + } + + if let Type::TypeVar(upper_bound_typevar) = upper { + let bounds = mappings.entry(upper_bound_typevar).or_default(); + bounds.add_lower(db, Type::TypeVar(typevar)); + } + } + + let path_bounds = mappings + .drain() + .map(|(bound_typevar, bounds)| TypeVarBounds { + bound_typevar, + lower: UnionType::from_elements(db, bounds.lower), + upper: IntersectionType::from_elements(db, bounds.upper), + }) + .collect(); + result.push(path_bounds); + } + + Self(result) + } + + fn solve(&self, db: &'db dyn Db) -> Vec> { + self.solve_with(|bound_typevar, _variance, lower, upper| { + Self::default_solve(db, bound_typevar, lower, upper) + }) + } + + /// Solves each path by applying a per-typevar solver function, collecting valid solutions. + /// + /// The solver receives the typevar and its materialized lower/upper bounds, and returns: + /// - `Ok(Some(solution))` to add a solution for this typevar on this path + /// - `Ok(None)` to leave this typevar unsolved on this path + /// - `Err(())` to invalidate the entire path + fn solve_with( + &self, + mut choose: impl FnMut( + BoundTypeVarInstance<'db>, + TypeVarVariance, + Type<'db>, + Type<'db>, + ) -> Result>, ()>, + ) -> Vec> { + let mut solutions = Vec::with_capacity(self.0.len()); + 'paths: for path in &self.0 { + let mut solution = Vec::with_capacity(path.len()); + for bounds in path { + let TypeVarBounds { + bound_typevar, + lower, + upper, + } = *bounds; + + // Determine variance from the constraint bounds: + // - Only upper bound (lower = Never) → covariant position + // - Only lower bound (upper = object) → contravariant position + // - Both bounds set → invariant position + let variance = if lower.is_never() { + TypeVarVariance::Covariant + } else if upper == Type::object() { + TypeVarVariance::Contravariant + } else { + TypeVarVariance::Invariant + }; + + match choose(bound_typevar, variance, lower, upper) { + Ok(Some(ty)) => solution.push(TypeVarSolution { + bound_typevar, + solution: ty, + }), + Ok(None) => {} + Err(()) => continue 'paths, + } + } + solutions.push(solution); + } + solutions + } + + /// The default solution selection logic for a single typevar on a single BDD path. + /// + /// Given the materialized lower and upper bounds for a typevar, selects the solution type. + /// Returns: + /// - `Ok(Some(solution))` if the typevar is solved on this path + /// - `Ok(None)` if the typevar is unsolved (no solution added) + /// - `Err(())` if the path is invalid (bounds violate the typevar's declared constraints) + pub(crate) fn default_solve( + db: &'db dyn Db, + bound_typevar: BoundTypeVarInstance<'db>, + lower: Type<'db>, + upper: Type<'db>, + ) -> Result>, ()> { + match bound_typevar.typevar(db).require_bound_or_constraints(db) { + TypeVarBoundOrConstraints::UpperBound(bound) => { + let bound = bound.top_materialization(db); + if !lower.is_assignable_to(db, bound) { + // This path does not satisfy the typevar's upper bound, and is + // therefore not a valid specialization. + return Err(()); + } + + // Prefer the lower bound (often the concrete actual type seen) over the + // upper bound (which may include TypeVar bounds/constraints). The upper bound + // should only be used as a fallback when no concrete type was inferred. + if !lower.is_never() { + return Ok(Some(lower)); + } + + let upper = IntersectionType::from_elements( + db, + std::iter::once(upper).chain(std::iter::once(bound)), + ); + if upper != bound { + Ok(Some(upper)) + } else { + Ok(None) + } + } + + TypeVarBoundOrConstraints::Constraints(constraints) => { + // Filter out the typevar constraints that aren't satisfied by this path. + let compatible_constraints = constraints.elements(db).iter().filter(|constraint| { + let constraint_lower = constraint.bottom_materialization(db); + let constraint_upper = constraint.top_materialization(db); + lower.is_assignable_to(db, constraint_lower) + && constraint_upper.is_assignable_to(db, upper) + }); + + // If only one constraint remains, that's our specialization for this path. + match compatible_constraints.at_most_one() { + Ok(None) => { + // This path does not satisfy any of the constraints, and is + // therefore not a valid specialization. + Err(()) + } + + Ok(Some(compatible_constraint)) => Ok(Some(*compatible_constraint)), + + Err(_) => { + // This path satisfies multiple constraints. For now, don't + // prefer any of them, and fall back on the default + // specialization for this typevar. + Ok(None) + } + } + } + } + } +} + impl InteriorNode { fn node(self) -> NodeId { self.0 @@ -3112,45 +3431,7 @@ impl InteriorNode { self, db: &'db dyn Db, builder: &'c ConstraintSetBuilder<'db>, - ) -> Solutions<'db, 'c> { - #[derive(Default)] - struct Bounds<'db> { - lower: FxIndexSet>, - upper: FxIndexSet>, - } - - impl<'db> Bounds<'db> { - fn add_lower(&mut self, _db: &'db dyn Db, ty: Type<'db>) { - // Lower bounds are unioned. Our type representation is in DNF, so unioning a new - // element is typically cheap (in that it does not involve a combinatorial - // explosion from distributing the clause through an existing disjunction). So we - // don't need to be as clever here as in `add_upper`. - self.lower.insert(ty); - } - - fn add_upper(&mut self, db: &'db dyn Db, ty: Type<'db>) { - // Upper bounds are intersectioned. If `ty` is a union, that involves distributing - // the union elements through the existing type. That makes it worth checking first - // whether any of the types in the upper bound are redundant. - - // First check if there's an existing upper bound clause that is a subtype of the - // new type. If so, adding the new type does nothing to the intersection. - if self - .upper - .iter() - .any(|existing| existing.is_redundant_with(db, ty)) - { - return; - } - - // Otherwise remove any existing clauses that are a supertype of the new type, - // since the intersection will clip them to the new type. - self.upper - .retain(|existing| !ty.is_redundant_with(db, *existing)); - self.upper.insert(ty); - } - } - + ) -> Solutions>>> { fn solutions_inner<'db, 'c>( db: &'db dyn Db, builder: &'c ConstraintSetBuilder<'db>, @@ -3164,122 +3445,8 @@ impl InteriorNode { return solutions; } - // Sort the constraints in each path by their `source_order`s, to ensure that we construct - // any unions or intersections in our type mappings in a stable order. Constraints might - // come out of `PathAssignment`s with identical `source_order`s, but if they do, those - // "tied" constraints will still be ordered in a stable way. So we need a stable sort to - // retain that stable per-tie ordering. - let mut sorted_paths = Vec::new(); - interior.for_each_path(db, builder, |path| { - let mut path: Vec<_> = path.positive_constraints().collect(); - path.sort_by_key(|(_, source_order)| *source_order); - sorted_paths.push(path); - }); - sorted_paths.sort_by(|path1, path2| { - let source_orders1 = path1.iter().map(|(_, source_order)| *source_order); - let source_orders2 = path2.iter().map(|(_, source_order)| *source_order); - source_orders1.cmp(source_orders2) - }); - - let mut solutions = Vec::with_capacity(sorted_paths.len()); - let mut mappings: FxHashMap, Bounds<'db>> = - FxHashMap::default(); - 'paths: for path in sorted_paths { - mappings.clear(); - for (constraint, _) in path { - let constraint = builder.constraint_data(constraint); - let typevar = constraint.typevar; - let lower = constraint.lower; - let upper = constraint.upper; - let bounds = mappings.entry(typevar).or_default(); - bounds.add_lower(db, lower); - bounds.add_upper(db, upper); - - if let Type::TypeVar(lower_bound_typevar) = lower { - let bounds = mappings.entry(lower_bound_typevar).or_default(); - bounds.add_upper(db, Type::TypeVar(typevar)); - } - - if let Type::TypeVar(upper_bound_typevar) = upper { - let bounds = mappings.entry(upper_bound_typevar).or_default(); - bounds.add_lower(db, Type::TypeVar(typevar)); - } - } - - let mut solution = Vec::with_capacity(mappings.len()); - for (bound_typevar, bounds) in mappings.drain() { - match bound_typevar.typevar(db).require_bound_or_constraints(db) { - TypeVarBoundOrConstraints::UpperBound(bound) => { - let bound = bound.top_materialization(db); - let lower = UnionType::from_elements(db, bounds.lower); - if !lower.is_assignable_to(db, bound) { - // This path does not satisfy the typevar's upper bound, and is - // therefore not a valid specialization. - continue 'paths; - } - - // Prefer the lower bound (often the concrete actual type seen) over the - // upper bound (which may include TypeVar bounds/constraints). The upper bound - // should only be used as a fallback when no concrete type was inferred. - if !lower.is_never() { - solution.push(TypeVarSolution { - bound_typevar, - solution: lower, - }); - continue; - } - - let upper = IntersectionType::from_elements( - db, - std::iter::chain(bounds.upper, [bound]), - ); - if upper != bound { - solution.push(TypeVarSolution { - bound_typevar, - solution: upper, - }); - } - } - - TypeVarBoundOrConstraints::Constraints(constraints) => { - // Filter out the typevar constraints that aren't satisfied by this path. - let lower = UnionType::from_elements(db, bounds.lower); - let upper = IntersectionType::from_elements(db, bounds.upper); - let compatible_constraints = - constraints.elements(db).iter().filter(|constraint| { - let constraint_lower = constraint.bottom_materialization(db); - let constraint_upper = constraint.top_materialization(db); - lower.is_assignable_to(db, constraint_lower) - && constraint_upper.is_assignable_to(db, upper) - }); - - // If only one constraint remains, that's our specialization for this path. - match compatible_constraints.at_most_one() { - Ok(None) => { - // This path does not satisfy any of the constraints, and is - // therefore not a valid specialization. - continue 'paths; - } - - Ok(Some(compatible_constraint)) => { - solution.push(TypeVarSolution { - bound_typevar, - solution: *compatible_constraint, - }); - } - - Err(_) => { - // This path satisfies multiple constraints. For now, don't - // prefer any of them, and fall back on the default - // specialization for this typevar. - } - } - } - } - } - - solutions.push(solution); - } + let path_bounds = PathBounds::compute(db, builder, interior); + let solutions = path_bounds.solve(db); let mut storage = builder.storage.borrow_mut(); storage.solutions_cache.insert(key, solutions); @@ -3296,6 +3463,25 @@ impl InteriorNode { Solutions::Constrained(solutions) } + fn solutions_with<'db>( + self, + db: &'db dyn Db, + builder: &ConstraintSetBuilder<'db>, + choose: impl FnMut( + BoundTypeVarInstance<'db>, + TypeVarVariance, + Type<'db>, + Type<'db>, + ) -> Result>, ()>, + ) -> Solutions>> { + let path_bounds = PathBounds::compute(db, builder, self.node()); + let solutions = path_bounds.solve_with(choose); + if solutions.is_empty() { + return Solutions::Unsatisfiable; + } + Solutions::Constrained(solutions) + } + fn path_assignments(self, builder: &ConstraintSetBuilder<'_>) -> PathAssignments { // Sort the constraints in this BDD by their `source_order`s before adding them to the // sequent map. This ensures that constraints appear in the sequent map in a stable order. @@ -3738,11 +3924,17 @@ impl InteriorNode { } } +/// The result of solving a constraint set for per-typevar specializations. +/// +/// Generic over the container type `S`: cached solutions use +/// `Ref<'c, Vec>>` (borrowed from the builder's cache), while +/// hook-based solutions use `Vec>` (owned, since the hook makes +/// caching inappropriate). #[derive(Debug)] -pub(crate) enum Solutions<'db, 'c> { +pub(crate) enum Solutions { Unsatisfiable, Unconstrained, - Constrained(Ref<'c, Vec>>), + Constrained(S), } pub(crate) type Solution<'db> = Vec>; diff --git a/crates/ty_python_semantic/src/types/generics.rs b/crates/ty_python_semantic/src/types/generics.rs index cf8cf1ddb9bd2..b6cdd3f17be1e 100644 --- a/crates/ty_python_semantic/src/types/generics.rs +++ b/crates/ty_python_semantic/src/types/generics.rs @@ -5,7 +5,6 @@ use std::fmt::Display; use itertools::{Either, Itertools}; use ruff_python_ast as ast; -use ruff_python_ast::name::Name; use rustc_hash::{FxHashMap, FxHashSet}; use crate::node_key::NodeKey; @@ -218,19 +217,25 @@ pub(crate) enum InferableTypeVars<'a, 'db> { ), } +impl<'db> BoundTypeVarIdentity<'db> { + pub(crate) fn is_inferable(self, inferable: InferableTypeVars<'_, 'db>) -> bool { + match inferable { + InferableTypeVars::None => false, + InferableTypeVars::One(typevars) => typevars.contains(&self), + InferableTypeVars::Two(left, right) => { + self.is_inferable(*left) || self.is_inferable(*right) + } + } + } +} + impl<'db> BoundTypeVarInstance<'db> { pub(crate) fn is_inferable( self, db: &'db dyn Db, inferable: InferableTypeVars<'_, 'db>, ) -> bool { - match inferable { - InferableTypeVars::None => false, - InferableTypeVars::One(typevars) => typevars.contains(&self.identity(db)), - InferableTypeVars::Two(left, right) => { - self.is_inferable(db, *left) || self.is_inferable(db, *right) - } - } + self.identity(db).is_inferable(inferable) } } @@ -1099,39 +1104,20 @@ impl<'db> Specialization<'db> { return self.materialize_impl(db, *materialization_kind, visitor); } - let types: Box<[_]> = if let TypeMapping::UniqueSpecialization { specialization } = - type_mapping - { - let mut specialization = specialization.borrow_mut(); - - self.types(db) - .iter() - .zip(self.generic_context(db).variables(db)) - .map(|(ty, typevar)| { - // Create a unique synthetic type variable. - let name = format!("_T{}", specialization.len()); - let synthetic = - BoundTypeVarInstance::synthetic(db, Name::new(name), typevar.variance(db)); - - specialization.push((synthetic, *ty)); - Type::TypeVar(synthetic) - }) - .collect() - } else { - self.types(db) - .iter() - .zip(self.generic_context(db).variables(db)) - .enumerate() - .map(|(i, (ty, typevar))| { - let tcx = TypeContext::new(tcx.get(i).copied()); - if typevar.variance(db).is_covariant() { - ty.apply_type_mapping_impl(db, type_mapping, tcx, visitor) - } else { - ty.apply_type_mapping_impl(db, &type_mapping.flip(), tcx, visitor) - } - }) - .collect() - }; + let types: Box<[_]> = self + .types(db) + .iter() + .zip(self.generic_context(db).variables(db)) + .enumerate() + .map(|(i, (ty, typevar))| { + let tcx = TypeContext::new(tcx.get(i).copied()); + if typevar.variance(db).is_covariant() { + ty.apply_type_mapping_impl(db, type_mapping, tcx, visitor) + } else { + ty.apply_type_mapping_impl(db, &type_mapping.flip(), tcx, visitor) + } + }) + .collect(); let tuple_inner = self.tuple_inner(db).and_then(|tuple| { tuple.apply_type_mapping_impl(db, type_mapping, TypeContext::default(), visitor) @@ -1639,8 +1625,9 @@ impl<'db> ApplySpecialization<'_, 'db> { /// Performs type inference between parameter annotations and argument types, producing a /// specialization of a generic function. -pub(crate) struct SpecializationBuilder<'db> { +pub(crate) struct SpecializationBuilder<'db, 'c> { db: &'db dyn Db, + constraints: &'c ConstraintSetBuilder<'db>, inferable: InferableTypeVars<'db, 'db>, types: FxHashMap, Type<'db>>, } @@ -1649,93 +1636,58 @@ pub(crate) struct SpecializationBuilder<'db> { /// type with respect to the type variable. pub(crate) type TypeVarAssignment<'db> = (BoundTypeVarIdentity<'db>, TypeVarVariance, Type<'db>); -impl<'db> SpecializationBuilder<'db> { - pub(crate) fn new(db: &'db dyn Db, inferable: InferableTypeVars<'db, 'db>) -> Self { +impl<'db, 'c> SpecializationBuilder<'db, 'c> { + pub(crate) fn new( + db: &'db dyn Db, + constraints: &'c ConstraintSetBuilder<'db>, + inferable: InferableTypeVars<'db, 'db>, + ) -> Self { Self { db, + constraints, inferable, types: FxHashMap::default(), } } - /// Returns the current set of type mappings for this specialization. - pub(crate) fn type_mappings(&self) -> &FxHashMap, Type<'db>> { - &self.types - } - - /// Returns the current set of type mappings for this specialization. - pub(crate) fn into_type_mappings(self) -> FxHashMap, Type<'db>> { - self.types - } - - /// Map the types that have been assigned in this specialization. - pub(crate) fn mapped( - &self, - generic_context: GenericContext<'db>, - f: impl Fn(BoundTypeVarInstance<'db>, Type<'db>) -> Type<'db>, - ) -> Self { - let mut types = self.types.clone(); - for (identity, variable) in generic_context.variables_inner(self.db) { - if let Some(ty) = types.get_mut(identity) { - *ty = f(*variable, *ty); - } - } - - Self { - db: self.db, - inferable: self.inferable, - types, - } - } - - pub(crate) fn with_default( - &self, + /// Build a specialization, using a caller-provided hook to select the solution for each + /// typevar. + /// + /// The `choose` hook is called for each *inferred* typevar (those with entries in the type + /// mappings) with the typevar's materialized lower and upper bounds. Currently, both bounds + /// are set to the inferred type (representing an equality constraint). Unmapped typevars + /// are left to `specialize_recursive` to fill in with defaults. + /// + /// The hook should return `Some(ty)` to use `ty` as the specialization for this typevar, or + /// `None` to use the inferred type unchanged. + pub(crate) fn build_with( + &mut self, generic_context: GenericContext<'db>, - default_ty: impl Fn(BoundTypeVarInstance<'db>) -> Type<'db>, - ) -> Self { - let mut types = self.types.clone(); - for (identity, variable) in generic_context.variables_inner(self.db) { - if let Entry::Vacant(entry) = types.entry(*identity) { - entry.insert(default_ty(*variable)); - } - } - - Self { - db: self.db, - inferable: self.inferable, - types, - } - } - - /// Apply a transformation to all accumulated type variable assignments. - pub(crate) fn map_types(&mut self, mut f: impl FnMut(Type<'db>) -> Type<'db>) { - for ty in self.types.values_mut() { - *ty = f(*ty); - } - } - - pub(crate) fn build(&mut self, generic_context: GenericContext<'db>) -> Specialization<'db> { + mut choose: impl FnMut(BoundTypeVarInstance<'db>, Type<'db>, Type<'db>) -> Option>, + ) -> Specialization<'db> { let types = generic_context .variables_inner(self.db) .iter() - .map(|(identity, _)| self.types.get(identity).copied()); + .map(|(identity, variable)| { + let mapped_ty = self.types.get(identity).copied()?; + // The typevar was inferred — present both bounds as the inferred type. + let chosen = choose(*variable, mapped_ty, mapped_ty); + Some(chosen.unwrap_or(mapped_ty)) + }); - // TODO Infer the tuple spec for a tuple type generic_context.specialize_recursive(self.db, types) } - fn add_type_mapping( + /// Insert a type mapping for a bound typevar. + /// + /// If a mapping already exists for this typevar, the new type is unioned with the existing + /// one. + pub(crate) fn insert_type_mapping( &mut self, bound_typevar: BoundTypeVarInstance<'db>, ty: Type<'db>, - variance: TypeVarVariance, - mut f: impl FnMut(TypeVarAssignment<'db>) -> Option>, ) { let identity = bound_typevar.identity(self.db); - let Some(ty) = f((identity, variance, ty)) else { - return; - }; - match self.types.entry(identity) { Entry::Occupied(mut entry) => { // TODO: The spec says that when a ParamSpec is used multiple times in a signature, @@ -1756,6 +1708,20 @@ impl<'db> SpecializationBuilder<'db> { } } + fn add_type_mapping( + &mut self, + bound_typevar: BoundTypeVarInstance<'db>, + ty: Type<'db>, + variance: TypeVarVariance, + mut f: impl FnMut(TypeVarAssignment<'db>) -> Option>, + ) { + let identity = bound_typevar.identity(self.db); + let Some(ty) = f((identity, variance, ty)) else { + return; + }; + self.insert_type_mapping(bound_typevar, ty); + } + /// Finds all of the valid specializations of a constraint set, and adds their type mappings to /// the specialization that this builder is building up. /// @@ -1764,18 +1730,17 @@ impl<'db> SpecializationBuilder<'db> { /// argument, and the variance of those typevars in the corresponding parameter. /// /// TODO: This is a stopgap! Eventually, the builder will maintain a single constraint set for - /// the main specialization that we are building, and [`build`][Self::build] will build the - /// specialization directly from that constraint set. This method lets us migrate to that brave - /// new world incrementally, by using the new constraint set mechanism piecemeal for certain - /// type comparisons. - fn add_type_mappings_from_constraint_set<'c>( + /// the main specialization that we are building, and [`build_with`][Self::build_with] will + /// build the specialization directly from that constraint set. This method lets us migrate to + /// that brave new world incrementally, by using the new constraint set mechanism piecemeal for + /// certain type comparisons. + fn add_type_mappings_from_constraint_set( &mut self, formal: Type<'db>, set: ConstraintSet<'db, 'c>, - constraints: &'c ConstraintSetBuilder<'db>, mut f: impl FnMut(TypeVarAssignment<'db>) -> Option>, ) -> Result<(), ()> { - let solutions = match set.solutions(self.db, constraints) { + let solutions = match set.solutions(self.db, self.constraints) { Solutions::Unsatisfiable => return Err(()), Solutions::Unconstrained => return Ok(()), Solutions::Constrained(solutions) => solutions, @@ -1800,17 +1765,11 @@ impl<'db> SpecializationBuilder<'db> { let formal_is_single_paramspec = formal_signature.is_single_paramspec().is_some(); for actual_callable in actual_callables.as_slice() { - let constraints = ConstraintSetBuilder::new(); if formal_is_single_paramspec { let when = actual_callable .signatures(self.db) - .when_constraint_set_assignable_to( - self.db, - formal_signature, - &constraints, - self.inferable, - ); - self.add_type_mappings_from_constraint_set(formal, when, &constraints, &mut *f)?; + .when_constraint_set_assignable_to(self.db, formal_signature, self.constraints); + self.add_type_mappings_from_constraint_set(formal, when, &mut *f)?; } else { // An overloaded actual callable is compatible with the formal signature if at // least one of its overloads is. We collect type mappings from all satisfiable @@ -1820,11 +1779,10 @@ impl<'db> SpecializationBuilder<'db> { let when = actual_signature.when_constraint_set_assignable_to_signatures( self.db, formal_signature, - &constraints, - self.inferable, + self.constraints, ); if self - .add_type_mappings_from_constraint_set(formal, when, &constraints, &mut *f) + .add_type_mappings_from_constraint_set(formal, when, &mut *f) .is_ok() { any_satisfiable = true; @@ -1841,11 +1799,10 @@ impl<'db> SpecializationBuilder<'db> { /// Infer type mappings for the specialization based on a given type and its declared type. pub(crate) fn infer( &mut self, - constraints: &ConstraintSetBuilder<'db>, formal: Type<'db>, actual: Type<'db>, ) -> Result<(), SpecializationError<'db>> { - self.infer_map(constraints, formal, actual, |(_, _, ty)| Some(ty)) + self.infer_map(formal, actual, |(_, _, ty)| Some(ty)) } /// Infer type mappings for the specialization based on a given type and its declared type. @@ -1854,13 +1811,11 @@ impl<'db> SpecializationBuilder<'db> { /// optionally modify the inferred type, or filter out the type mapping entirely. pub(crate) fn infer_map( &mut self, - constraints: &ConstraintSetBuilder<'db>, formal: Type<'db>, actual: Type<'db>, mut f: impl FnMut(TypeVarAssignment<'db>) -> Option>, ) -> Result<(), SpecializationError<'db>> { self.infer_map_impl( - constraints, formal, actual, TypeVarVariance::Covariant, @@ -1871,7 +1826,6 @@ impl<'db> SpecializationBuilder<'db> { fn infer_map_impl( &mut self, - constraints: &ConstraintSetBuilder<'db>, formal: Type<'db>, actual: Type<'db>, polarity: TypeVarVariance, @@ -1908,14 +1862,7 @@ impl<'db> SpecializationBuilder<'db> { // Expand PEP 695 type aliases in the formal type. // This is necessary for solving generics like `def head[T](my_list: MyList[T]) -> T`. (Type::TypeAlias(alias), _) => { - return self.infer_map_impl( - constraints, - alias.value_type(self.db), - actual, - polarity, - f, - seen, - ); + return self.infer_map_impl(alias.value_type(self.db), actual, polarity, f, seen); } // TODO: We haven't implemented a full unification solver yet. If typevars appear in @@ -1981,7 +1928,7 @@ impl<'db> SpecializationBuilder<'db> { if !actual.is_never() { let assignable_elements = union_formal.elements(self.db).iter().filter(|ty| { actual - .when_subtype_of(self.db, **ty, constraints, self.inferable) + .when_subtype_of(self.db, **ty, self.constraints, self.inferable) .is_always_satisfied(self.db) }); if assignable_elements.exactly_one().is_ok() { @@ -2011,14 +1958,8 @@ impl<'db> SpecializationBuilder<'db> { let mut first_error = None; let mut found_matching_element = false; for formal_element in union_formal.elements(self.db) { - let result = self.infer_map_impl( - constraints, - *formal_element, - actual, - polarity, - &mut f, - seen, - ); + let result = + self.infer_map_impl(*formal_element, actual, polarity, &mut f, seen); if let Err(err) = result { first_error.get_or_insert(err); } else { @@ -2028,7 +1969,7 @@ impl<'db> SpecializationBuilder<'db> { .when_assignable_to( self.db, *formal_element, - constraints, + self.constraints, self.inferable, ) .is_never_satisfied(self.db) @@ -2064,7 +2005,7 @@ impl<'db> SpecializationBuilder<'db> { return Ok(()); } if !ty - .when_assignable_to(self.db, bound, constraints, self.inferable) + .when_assignable_to(self.db, bound, self.constraints, self.inferable) .is_always_satisfied(self.db) { return Err(SpecializationError::MismatchedBound { @@ -2117,13 +2058,18 @@ impl<'db> SpecializationBuilder<'db> { for constraint in typevar_constraints.elements(self.db) { let is_satisfied = if polarity.is_contravariant() { constraint - .when_assignable_to(self.db, ty, constraints, self.inferable) + .when_assignable_to( + self.db, + ty, + self.constraints, + self.inferable, + ) .is_always_satisfied(self.db) } else { ty.when_assignable_to( self.db, *constraint, - constraints, + self.constraints, self.inferable, ) .is_always_satisfied(self.db) @@ -2149,7 +2095,7 @@ impl<'db> SpecializationBuilder<'db> { // actual type must also be disjoint from every negative element of the // intersection, but that doesn't help us infer any type mappings.) for positive in formal_intersection.iter_positive(self.db) { - self.infer_map_impl(constraints, positive, actual, polarity, f, seen)?; + self.infer_map_impl(positive, actual, polarity, f, seen)?; } } (_, Type::Intersection(actual_intersection)) => { @@ -2171,8 +2117,7 @@ impl<'db> SpecializationBuilder<'db> { let mut first_error = None; let mut found_matching_element = false; for positive in actual_intersection.iter_positive(self.db) { - let result = - self.infer_map_impl(constraints, formal, positive, polarity, f, seen); + let result = self.infer_map_impl(formal, positive, polarity, f, seen); if let Err(err) = result { // TODO: `infer_map_impl` can have side effects even in the error case, so // to be fully correct here we'd need to snapshot `self.types` before each @@ -2183,7 +2128,7 @@ impl<'db> SpecializationBuilder<'db> { // The recursive call to `infer_map_impl` may succeed even if the actual // type is not assignable to the formal element. if !positive - .when_assignable_to(self.db, formal, constraints, self.inferable) + .when_assignable_to(self.db, formal, self.constraints, self.inferable) .is_never_satisfied(self.db) { found_matching_element = true; @@ -2201,7 +2146,6 @@ impl<'db> SpecializationBuilder<'db> { let formal_instance = Type::TypeVar(subclass_of.into_type_var().unwrap()); if let Some(actual_instance) = ty.to_instance(self.db) { return self.infer_map_impl( - constraints, formal_instance, actual_instance, polarity, @@ -2218,14 +2162,7 @@ impl<'db> SpecializationBuilder<'db> { // Retry specialization with the literal's fallback instance so literals can // contribute to generic inference for nominal and protocol formals. let actual_instance = literal.fallback_instance(self.db); - return self.infer_map_impl( - constraints, - formal, - actual_instance, - polarity, - f, - seen, - ); + return self.infer_map_impl(formal, actual_instance, polarity, f, seen); } (formal, Type::ProtocolInstance(actual_protocol)) => { @@ -2236,7 +2173,6 @@ impl<'db> SpecializationBuilder<'db> { // infer the specialization of the protocol that the class implements. if let Some(actual_nominal) = actual_protocol.to_nominal_instance() { return self.infer_map_impl( - constraints, formal, Type::NominalInstance(actual_nominal), polarity, @@ -2270,7 +2206,6 @@ impl<'db> SpecializationBuilder<'db> { { let variance = TypeVarVariance::Covariant.compose(polarity); self.infer_map_impl( - constraints, *formal_element, *actual_element, variance, @@ -2295,8 +2230,7 @@ impl<'db> SpecializationBuilder<'db> { let when = actual.when_constraint_set_assignable_to( self.db, formal, - constraints, - self.inferable, + self.constraints, ); // For protocol inference via constraint sets, we currently treat // unsatisfiable results as "no inference" instead of an immediate @@ -2304,12 +2238,7 @@ impl<'db> SpecializationBuilder<'db> { // unsatisfied comparisons simply produced no type mappings), and avoids // false positives for callable-wrapper patterns while this path is still // a hybrid of old and new solver logic. - let _ = self.add_type_mappings_from_constraint_set( - formal, - when, - constraints, - &mut f, - ); + let _ = self.add_type_mappings_from_constraint_set(formal, when, &mut f); return Ok(()); } @@ -2338,14 +2267,7 @@ impl<'db> SpecializationBuilder<'db> { base_specialization ) { let variance = typevar.variance_with_polarity(self.db, polarity); - self.infer_map_impl( - constraints, - *formal_ty, - *base_ty, - variance, - &mut f, - seen, - )?; + self.infer_map_impl(*formal_ty, *base_ty, variance, &mut f, seen)?; } return Ok(()); } @@ -2404,14 +2326,7 @@ impl<'db> SpecializationBuilder<'db> { // when it can be matched directly against a type variable in the formal type, // e.g., `reveal_type(alias)` should reveal the type alias, not its value type. (formal, Type::TypeAlias(alias)) => { - return self.infer_map_impl( - constraints, - formal, - alias.value_type(self.db), - polarity, - f, - seen, - ); + return self.infer_map_impl(formal, alias.value_type(self.db), polarity, f, seen); } // TODO: Add more forms that we can structurally induct into: type[C], callables @@ -2420,112 +2335,6 @@ impl<'db> SpecializationBuilder<'db> { Ok(()) } - - /// Infer type mappings for the specialization in the reverse direction, i.e., where the - /// actual type, not the formal type, contains inferable type variables. - pub(crate) fn infer_reverse( - &mut self, - constraints: &ConstraintSetBuilder<'db>, - formal: Type<'db>, - actual: Type<'db>, - ) -> Result<(), SpecializationError<'db>> { - self.infer_reverse_map(constraints, formal, actual, |(_, _, ty)| Some(ty)) - } - - /// Infer type mappings for the specialization in the reverse direction, i.e., where the - /// actual type, not the formal type, contains inferable type variables. - /// - /// The provided function will be called before any type mappings are created, and can - /// optionally modify the inferred type, or filter out the type mapping entirely. - pub(crate) fn infer_reverse_map( - &mut self, - constraints: &ConstraintSetBuilder<'db>, - formal: Type<'db>, - actual: Type<'db>, - mut f: impl FnMut(TypeVarAssignment<'db>) -> Option>, - ) -> Result<(), SpecializationError<'db>> { - self.infer_reverse_map_impl( - constraints, - formal, - actual, - TypeVarVariance::Covariant, - &mut f, - &mut FxHashSet::default(), - ) - } - - fn infer_reverse_map_impl( - &mut self, - constraints: &ConstraintSetBuilder<'db>, - formal: Type<'db>, - actual: Type<'db>, - polarity: TypeVarVariance, - f: &mut dyn FnMut(TypeVarAssignment<'db>) -> Option>, - seen: &mut FxHashSet<(Type<'db>, Type<'db>)>, - ) -> Result<(), SpecializationError<'db>> { - // Avoid infinite recursion - if !seen.insert((formal, actual)) { - return Ok(()); - } - - // Assign each type variable on the formal type to a unique synthetic type variable. - let type_mapping = TypeMapping::UniqueSpecialization { - specialization: RefCell::new(Vec::new()), - }; - let synthetic_formal = - formal.apply_type_mapping(self.db, &type_mapping, TypeContext::default()); - - // Recover the synthetic type variables. - let synthetic_specialization = match type_mapping { - TypeMapping::UniqueSpecialization { specialization } => specialization.into_inner(), - _ => unreachable!(), - }; - - let inferable = GenericContext::from_typevar_instances( - self.db, - synthetic_specialization.iter().map(|(typevar, _)| *typevar), - ) - .inferable_typevars(self.db); - - // Collect the actual type to which each synthetic type variable is mapped. - let forward_type_mappings = { - let mut builder = SpecializationBuilder::new(self.db, inferable); - builder.infer(constraints, synthetic_formal, actual)?; - builder.into_type_mappings() - }; - - // If there are no forward type mappings, try the other direction. - // - // This is the base case for when `actual` is an inferable type variable. - if forward_type_mappings.is_empty() { - return self.infer_map_impl(constraints, actual, formal, polarity, f, seen); - } - - // Consider the reverse inference of `Sequence[int]` given `list[T]`. - // - // Given a forward type mapping of `T@Sequence` -> `T@list`, and a synthetic type mapping of - // `T@Sequence` -> `int`, we want to infer the reverse type mapping `T@list` -> `int`. - for (synthetic_type_var, formal_type) in synthetic_specialization { - if let Some(actual_type) = - forward_type_mappings.get(&synthetic_type_var.identity(self.db)) - { - let variance = synthetic_type_var.variance_with_polarity(self.db, polarity); - - // Note that it is possible that we need to recurse deeper, so we continue - // to perform a reverse inference on the nested types. - self.infer_reverse_map_impl( - constraints, - formal_type, - *actual_type, - variance, - f, - seen, - )?; - } - } - - Ok(()) - } } #[derive(Clone, Debug, Eq, PartialEq)] diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 6b26a9f348b39..4ee988aa9cad8 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -57,7 +57,7 @@ use crate::types::class::{ ClassLiteral, CodeGeneratorKind, DynamicClassAnchor, DynamicClassLiteral, DynamicMetaclassConflict, MethodDecorator, }; -use crate::types::constraints::ConstraintSetBuilder; +use crate::types::constraints::{ConstraintSetBuilder, PathBounds, Solutions}; use crate::types::context::InferContext; use crate::types::diagnostic::{ self, CALL_NON_CALLABLE, CONFLICTING_DECLARATIONS, CYCLIC_CLASS_DEFINITION, @@ -5189,33 +5189,57 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // If this is a generic call, attempt to specialize the parameter type using the // declared type context, if provided. if let Some(generic_context) = overload.signature.generic_context { - let mut builder = - SpecializationBuilder::new(db, generic_context.inferable_typevars(db)); - + // Use a forward assignability check to infer typevar specializations from the + // declared type context. For example, if the return type is `list[T]` and the + // declared type context is `list[int]`, the check produces `T = int`. + let mut tcx_mappings = FxHashMap::default(); if let Some(declared_return_ty) = call_expression_tcx.annotation { - let _ = builder.infer_reverse( - &constraints, + let return_ty = overload + .constructor_instance_type + .unwrap_or(overload.signature.return_ty); + let set = return_ty.when_constraint_set_assignable_to( + db, declared_return_ty, - overload - .constructor_instance_type - .unwrap_or(overload.signature.return_ty), + &constraints, ); + if let Solutions::Constrained(solutions) = set.solutions(db, &constraints) { + for solution in solutions.iter() { + for binding in solution { + tcx_mappings + .entry(binding.bound_typevar.identity(db)) + .and_modify(|existing| { + *existing = UnionType::from_two_elements( + db, + *existing, + binding.solution, + ); + }) + .or_insert(binding.solution); + } + } + } } - let specialization = builder - // Default specialize any type variables to a marker type, which will be ignored - // during argument inference, allowing the concrete subset of the parameter - // type to still affect argument inference. - // - // TODO: Eventually, we want to "tie together" the typevars of the two calls - // so that we can infer their specializations at the same time — or at least, for - // the specialization of one to influence the specialization of the other. It's - // not yet clear how we're going to do that. (We might have to start inferring - // constraint sets for each expression, instead of simple types?) - .with_default(generic_context, |_| { - Type::Dynamic(DynamicType::UnspecializedTypeVar) - }) - .build(generic_context); + // Default specialize any type variables to a marker type, which will be ignored + // during argument inference, allowing the concrete subset of the parameter + // type to still affect argument inference. + // + // TODO: Eventually, we want to "tie together" the typevars of the two calls + // so that we can infer their specializations at the same time — or at least, for + // the specialization of one to influence the specialization of the other. It's + // not yet clear how we're going to do that. (We might have to start inferring + // constraint sets for each expression, instead of simple types?) + let specialization = generic_context.specialize_recursive( + db, + generic_context.variables(db).map(|typevar| { + Some( + tcx_mappings + .get(&typevar.identity(db)) + .copied() + .unwrap_or(Type::Dynamic(DynamicType::UnspecializedTypeVar)), + ) + }), + ); parameter_type = parameter_type.apply_specialization(db, specialization); } @@ -6014,52 +6038,127 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { }); // Collect type constraints from the declared element types. + // + // We use a forward assignability check (`collection_instance ≤ tcx`) to infer what each + // typevar maps to in the type context. For example, if the type context is `list[int]` and + // `collection_instance` is `list[T]`, the check produces `T = int`. + // + // Variance is determined from the constraint bounds: a constraint with only an upper bound + // (`lower = Never`) indicates a covariant position, while a constraint with only a lower + // bound (`upper = object`) indicates contravariant. This correctly handles cases where the + // type context is a covariant superclass of the collection (e.g., `Sequence[Any]` as type + // context for `list[T]`). let (elt_tcx_constraints, elt_tcx_variance) = { - let mut builder = SpecializationBuilder::new(self.db(), inferable); - - // For a given type variable, we keep track of the variance of any assignments to - // that type variable in the type context. + let mut elt_tcx_constraints: FxHashMap, Type<'db>> = + FxHashMap::default(); let mut elt_tcx_variance: FxHashMap, TypeVarVariance> = FxHashMap::default(); if let Some(tcx) = tcx.annotation && tcx.class_specialization(self.db()).is_some() + // Skip constraint extraction when the type context contains UnspecializedTypeVar + // placeholders (from partially-specialized parameter types during multi-inference + // for overloaded calls). The assignability check would walk through protocol + // supertypes and produce constraints contaminated by the placeholder, which + // collapse to `Any` during solution extraction and bypass downstream filters. + // + // TODO: UnspecializedTypeVar should be removed entirely once the + // SpecializationBuilder uses constraint sets internally, at which point the + // typevars that it currently replaces can instead be existentially quantified away + // (as is already done for generic callable assignability in + // `check_signature_pair`). + && !tcx.has_unspecialized_type_var(self.db()) { - let collection_instance = - Type::instance(self.db(), ClassType::Generic(collection_alias)); + let db = self.db(); + let collection_instance = Type::instance(db, ClassType::Generic(collection_alias)); + + let set = + collection_instance.when_constraint_set_assignable_to(db, tcx, &constraints); + + // Use `solutions_with_inferable` to capture per-typevar variance from the raw + // lower/upper bounds on each BDD path. We must use the inferable-aware variant so + // that non-inferable typevars from outer scopes (which the assignability check + // constrains alongside the collection's own typevars) are excluded from cycle + // detection and solution extraction. Without this, a type context like + // `list[T@MyClass]` would create mutual constraints between `_T` (list's typevar) + // and `T@MyClass`, which `is_cyclic` would flag as a cycle, returning + // `Unsatisfiable` and losing the type context information entirely. + let solutions = set.solutions_with_inferable( + db, + &constraints, + inferable, + |typevar, variance, lower, upper| { + if !typevar.is_inferable(db, inferable) { + return Ok(None); + } - builder - .infer_reverse_map( - &constraints, - tcx, - collection_instance, - |(typevar, variance, inferred_ty)| { - // Avoid inferring a preferred type based on partially specialized type context - // from an outer generic call. If the type context is a union, we try to keep - // any concrete elements. - let inferred_ty = inferred_ty.filter_union(self.db(), |ty| { - !ty.has_unspecialized_type_var(self.db()) - }); - if inferred_ty.has_unspecialized_type_var(self.db()) { - return None; - } + let identity = typevar.identity(db); + elt_tcx_variance + .entry(identity) + .and_modify(|current| *current = current.join(variance)) + .or_insert(variance); + PathBounds::default_solve(db, typevar, lower, upper) + }, + ); - elt_tcx_variance - .entry(typevar) - .and_modify(|current| *current = current.join(variance)) - .or_insert(variance); + match solutions { + // If the type context is not compatible with the collection type (e.g., a + // `list` literal where a `tuple` is expected), the assignability check + // produces an unsatisfiable result. In that case, we simply proceed without + // type context constraints rather than aborting the entire collection literal + // inference. + Solutions::Unsatisfiable | Solutions::Unconstrained => {} + Solutions::Constrained(solutions) => { + for solution in &solutions { + for binding in solution { + // The SequentMap's transitivity reasoning can inject + // cross-typevar references into the solution bounds. + // For example, `_KT ≤ str ∧ str ≤ _VT` derives `_KT ≤ _VT`, + // which adds `_KT` to `_VT`'s lower bound. Filter out any + // inferable typevars from the solution, since they represent + // cross-typevar relationships that are resolved independently. + let inferred_ty = binding.solution.filter_union(db, |ty| { + !ty.as_typevar() + .is_some_and(|tv| tv.is_inferable(db, inferable)) + }); - Some(inferred_ty) - }, - ) - .ok()?; + // Avoid inferring a preferred type based on partially specialized + // type context from an outer generic call. If the type context is + // a union, we try to keep any concrete elements. + let inferred_ty = inferred_ty + .filter_union(db, |ty| !ty.has_unspecialized_type_var(db)); + if inferred_ty.has_unspecialized_type_var(db) { + continue; + } + + let identity = binding.bound_typevar.identity(db); + elt_tcx_constraints + .entry(identity) + .and_modify(|existing| { + *existing = UnionType::from_two_elements( + db, + *existing, + inferred_ty, + ); + }) + .or_insert(inferred_ty); + } + } + + // Remove variance entries for typevars whose solutions were filtered out + // (e.g., due to unspecialized typevars). Variance should only be tracked + // for typevars with actual type context constraints. + elt_tcx_variance + .retain(|identity, _| elt_tcx_constraints.contains_key(identity)); + } + } } - (builder.into_type_mappings(), elt_tcx_variance) + (elt_tcx_constraints, elt_tcx_variance) }; // Create a set of constraints to infer a precise type for `T`. - let mut builder = SpecializationBuilder::new(self.db(), inferable); + let mut builder = SpecializationBuilder::new(self.db(), &constraints, inferable); for elt_ty in elt_tys.clone() { let elt_ty_identity = elt_ty.identity(self.db()); @@ -6088,9 +6187,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { // our inference is compatible with subsequent additions to the collection), but it // matches the behavior of other type checkers and is usually the desired behavior. if let Some(elt_tcx) = elt_tcx { - builder - .infer(&constraints, Type::TypeVar(elt_ty), elt_tcx) - .ok()?; + builder.insert_type_mapping(elt_ty, elt_tcx); } } @@ -6119,12 +6216,10 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { let mut elt_tys = elt_tys.clone(); if let Some((key_ty, value_ty)) = elt_tys.next_tuple() { - builder - .infer(&constraints, Type::TypeVar(key_ty), unpacked_key_ty) - .ok()?; + builder.infer(Type::TypeVar(key_ty), unpacked_key_ty).ok()?; builder - .infer(&constraints, Type::TypeVar(value_ty), unpacked_value_ty) + .infer(Type::TypeVar(value_ty), unpacked_value_ty) .ok()?; } @@ -6171,7 +6266,6 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { builder .infer( - &constraints, Type::TypeVar(elt_ty), if elt.is_starred_expr() { inferred_elt_ty @@ -6185,15 +6279,18 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { } } - // Promote singleton types to `T | Unknown` in inferred type parameters, - // so that e.g. `[None]` is inferred as `list[None | Unknown]`. - if elt_tcx_constraints.is_empty() { - builder.map_types(|ty| ty.promote_singletons(self.db())); - } - let class_type = collection_alias .origin(self.db()) - .apply_specialization(self.db(), |_| builder.build(generic_context)); + .apply_specialization(self.db(), |_| { + builder.build_with(generic_context, |_, lower, _| { + // Promote singleton types to `T | Unknown` in inferred type parameters, + // so that e.g. `[None]` is inferred as `list[None | Unknown]`. + if elt_tcx_constraints.is_empty() { + return Some(lower.promote_singletons(self.db())); + } + None + }) + }); Type::from(class_type).to_instance(self.db()) } diff --git a/crates/ty_python_semantic/src/types/known_instance.rs b/crates/ty_python_semantic/src/types/known_instance.rs index 8ecee6c599d96..29ae40a70ee67 100644 --- a/crates/ty_python_semantic/src/types/known_instance.rs +++ b/crates/ty_python_semantic/src/types/known_instance.rs @@ -284,7 +284,6 @@ impl<'db> KnownInstanceType<'db> { ), TypeMapping::ApplySpecialization(_) | TypeMapping::ApplySpecializationWithMaterialization { .. } - | TypeMapping::UniqueSpecialization { .. } | TypeMapping::Promote(..) | TypeMapping::BindSelf(..) | TypeMapping::ReplaceSelf { .. } diff --git a/crates/ty_python_semantic/src/types/relation.rs b/crates/ty_python_semantic/src/types/relation.rs index aa4d1cb785b17..d44e5762f6a47 100644 --- a/crates/ty_python_semantic/src/types/relation.rs +++ b/crates/ty_python_semantic/src/types/relation.rs @@ -346,7 +346,7 @@ impl<'db> Type<'db> { /// reasoning depending on inferable typevars. pub fn is_constraint_set_assignable_to(self, db: &'db dyn Db, target: Type<'db>) -> bool { let constraints = ConstraintSetBuilder::new(); - self.when_constraint_set_assignable_to(db, target, &constraints, InferableTypeVars::None) + self.when_constraint_set_assignable_to(db, target, &constraints) .is_always_satisfied(db) } @@ -371,13 +371,12 @@ impl<'db> Type<'db> { db: &'db dyn Db, target: Type<'db>, constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'_, 'db>, ) -> ConstraintSet<'db, 'c> { self.has_relation_to( db, target, constraints, - inferable, + InferableTypeVars::None, TypeRelation::ConstraintSetAssignability, ) } @@ -558,13 +557,12 @@ impl<'a, 'c, 'db> TypeRelationChecker<'a, 'c, 'db> { pub(super) fn constraint_set_assignability( constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'a, 'db>, relation_visitor: &'a HasRelationToVisitor<'db, 'c>, disjointness_visitor: &'a IsDisjointVisitor<'db, 'c>, ) -> Self { Self { constraints, - inferable, + inferable: InferableTypeVars::None, relation: TypeRelation::ConstraintSetAssignability, given: ConstraintSet::from_bool(constraints, false), relation_visitor, diff --git a/crates/ty_python_semantic/src/types/signatures.rs b/crates/ty_python_semantic/src/types/signatures.rs index 4b767029ccc67..66fbde4600fb9 100644 --- a/crates/ty_python_semantic/src/types/signatures.rs +++ b/crates/ty_python_semantic/src/types/signatures.rs @@ -319,13 +319,11 @@ impl<'db> CallableSignature<'db> { db: &'db dyn Db, other: &Self, constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'_, 'db>, ) -> ConstraintSet<'db, 'c> { let relation_visitor = HasRelationToVisitor::default(constraints); let disjointness_visitor = IsDisjointVisitor::default(constraints); let checker = TypeRelationChecker::constraint_set_assignability( constraints, - inferable, &relation_visitor, &disjointness_visitor, ); @@ -729,7 +727,6 @@ impl<'db> Signature<'db> { db: &'db dyn Db, other: &CallableSignature<'db>, constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'_, 'db>, ) -> ConstraintSet<'db, 'c> { // If this signature is a paramspec, bind it to the entire overloaded other callable. if let Some(self_bound_typevar) = self.parameters.as_paramspec() @@ -762,7 +759,6 @@ impl<'db> Signature<'db> { db, other_return_type, constraints, - inferable, ) }); return param_spec_matches.and(db, constraints, || return_types_match); @@ -772,7 +768,7 @@ impl<'db> Signature<'db> { .overloads .iter() .when_all(db, constraints, |other_signature| { - self.when_constraint_set_assignable_to(db, other_signature, constraints, inferable) + self.when_constraint_set_assignable_to(db, other_signature, constraints) }) } @@ -781,13 +777,11 @@ impl<'db> Signature<'db> { db: &'db dyn Db, other: &Self, constraints: &'c ConstraintSetBuilder<'db>, - inferable: InferableTypeVars<'_, 'db>, ) -> ConstraintSet<'db, 'c> { let relation_visitor = HasRelationToVisitor::default(constraints); let disjointness_visitor = IsDisjointVisitor::default(constraints); let checker = TypeRelationChecker::constraint_set_assignability( constraints, - inferable, &relation_visitor, &disjointness_visitor, ); diff --git a/crates/ty_python_semantic/src/types/typevar.rs b/crates/ty_python_semantic/src/types/typevar.rs index 5f3e9fee7d727..e02b52e8e4dc9 100644 --- a/crates/ty_python_semantic/src/types/typevar.rs +++ b/crates/ty_python_semantic/src/types/typevar.rs @@ -903,8 +903,7 @@ impl<'db> BoundTypeVarInstance<'db> { Type::TypeVar(self) } } - TypeMapping::UniqueSpecialization { .. } - | TypeMapping::Promote(..) + TypeMapping::Promote(..) | TypeMapping::ReplaceParameterDefaults | TypeMapping::BindLegacyTypevars(_) | TypeMapping::EagerExpansion