From 4ea383db9555bd12c9128dba6088da2fe6545917 Mon Sep 17 00:00:00 2001 From: Alex Waygood Date: Mon, 2 Mar 2026 15:20:17 +0000 Subject: [PATCH] [ty] Move `UnionType` and `IntersectionType` to a new `types::set_theoretic` submodule --- crates/ty_python_semantic/src/types.rs | 833 +---------------- .../src/types/infer/builder.rs | 2 +- .../ty_python_semantic/src/types/relation.rs | 2 +- .../src/types/set_theoretic.rs | 854 ++++++++++++++++++ .../src/types/{ => set_theoretic}/builder.rs | 20 +- crates/ty_python_semantic/src/types/tuple.rs | 2 +- 6 files changed, 864 insertions(+), 849 deletions(-) create mode 100644 crates/ty_python_semantic/src/types/set_theoretic.rs rename crates/ty_python_semantic/src/types/{ => set_theoretic}/builder.rs (99%) diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 76235378fb2c3..7cb30b5bc96e3 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -22,7 +22,6 @@ use ruff_text_size::{Ranged, TextRange}; use smallvec::{SmallVec, smallvec_inline}; use ty_module_resolver::{KnownModule, Module, ModuleName, resolve_module}; -pub(crate) use self::builder::{IntersectionBuilder, UnionBuilder}; pub(crate) use self::class::DynamicClassLiteral; pub use self::cyclic::CycleDetector; pub(crate) use self::cyclic::{PairVisitor, TypeTransformer}; @@ -32,6 +31,11 @@ pub(crate) use self::infer::{ TypeContext, infer_complete_scope_types, infer_deferred_types, infer_definition_types, infer_expression_type, infer_expression_types, infer_scope_types, }; +pub(crate) use self::set_theoretic::builder::{IntersectionBuilder, UnionBuilder}; +pub use self::set_theoretic::{ + IntersectionType, NegativeIntersectionElements, NegativeIntersectionElementsIterator, UnionType, +}; +use self::set_theoretic::{KnownUnion, walk_intersection_type, walk_union}; pub use self::signatures::ParameterKind; pub(crate) use self::signatures::{CallableSignature, Signature}; pub(crate) use self::subclass_of::{SubclassOfInner, SubclassOfType}; @@ -46,7 +50,6 @@ use crate::semantic_index::scope::ScopeId; use crate::semantic_index::{imported_modules, place_table, semantic_index}; use crate::suppression::check_suppressions; use crate::types::bound_super::BoundSuperType; -use crate::types::builder::RecursivelyDefined; use crate::types::call::{Binding, Bindings, CallArguments, CallableBinding}; use crate::types::class::NamedTupleSpec; pub(crate) use crate::types::class_base::ClassBase; @@ -92,7 +95,6 @@ pub(crate) use literal::{ pub use special_form::SpecialFormType; mod bound_super; -mod builder; mod call; mod class; mod class_base; @@ -116,6 +118,7 @@ mod newtype; mod overrides; mod protocol_class; pub(crate) mod relation; +mod set_theoretic; mod signatures; mod special_form; mod string_annotation; @@ -11735,830 +11738,6 @@ pub(super) struct MetaclassTransformInfo<'db> { pub(super) from_explicit_metaclass: bool, } -#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -pub struct UnionType<'db> { - /// The union type includes values in any of these types. - #[returns(deref)] - pub elements: Box<[Type<'db>]>, - /// Whether the value pointed to by this type is recursively defined. - /// If `Yes`, union literal widening is performed early. - recursively_defined: RecursivelyDefined, -} - -pub(crate) fn walk_union<'db, V: visitor::TypeVisitor<'db> + ?Sized>( - db: &'db dyn Db, - union: UnionType<'db>, - visitor: &V, -) { - for element in union.elements(db) { - visitor.visit_type(db, *element); - } -} - -// The Salsa heap is tracked separately. -impl get_size2::GetSize for UnionType<'_> {} - -#[salsa::tracked] -impl<'db> UnionType<'db> { - /// Create a union from a list of elements - /// (which may be eagerly simplified into a different variant of [`Type`] altogether). - /// - /// For performance reasons, consider using [`UnionType::from_two_elements`] if - /// the union is constructed from exactly two elements. - pub fn from_elements(db: &'db dyn Db, elements: I) -> Type<'db> - where - I: IntoIterator, - T: Into>, - { - elements - .into_iter() - .fold(UnionBuilder::new(db), |builder, element| { - builder.add(element.into()) - }) - .build() - } - - /// Create a union type `A | B` from two elements `A` and `B`. - #[salsa::tracked( - cycle_initial=|_, id, _, _| Type::divergent(id), - cycle_fn=|db, cycle, previous: &Type<'db>, result: Type<'db>, _, _| { - result.cycle_normalized(db, *previous, cycle) - }, - heap_size=ruff_memory_usage::heap_size - )] - pub fn from_two_elements(db: &'db dyn Db, a: Type<'db>, b: Type<'db>) -> Type<'db> { - UnionBuilder::new(db).add(a).add(b).build() - } - - /// Create a union from a list of elements without unpacking type aliases. - pub(crate) fn from_elements_leave_aliases(db: &'db dyn Db, elements: I) -> Type<'db> - where - I: IntoIterator, - T: Into>, - { - elements - .into_iter() - .fold( - UnionBuilder::new(db).unpack_aliases(false), - |builder, element| builder.add(element.into()), - ) - .build() - } - - fn from_elements_cycle_recovery(db: &'db dyn Db, elements: I) -> Type<'db> - where - I: IntoIterator, - T: Into>, - { - elements - .into_iter() - .fold( - UnionBuilder::new(db).cycle_recovery(true), - |builder, element| builder.add(element.into()), - ) - .build() - } - - /// A fallible version of [`UnionType::from_elements`]. - /// - /// If all items in `elements` are `Some()`, the result of unioning all elements is returned. - /// As soon as a `None` element in the iterable is encountered, - /// the function short-circuits and returns `None`. - pub(crate) fn try_from_elements(db: &'db dyn Db, elements: I) -> Option> - where - I: IntoIterator>, - T: Into>, - { - let mut builder = UnionBuilder::new(db); - for element in elements { - builder = builder.add(element?.into()); - } - Some(builder.build()) - } - - /// Apply a transformation function to all elements of the union, - /// and create a new union from the resulting set of types. - pub(crate) fn map( - self, - db: &'db dyn Db, - transform_fn: impl FnMut(&Type<'db>) -> Type<'db>, - ) -> Type<'db> { - self.elements(db) - .iter() - .map(transform_fn) - .fold(UnionBuilder::new(db), |builder, element| { - builder.add(element) - }) - .recursively_defined(self.recursively_defined(db)) - .build() - } - - /// A version of [`UnionType::map`] that does not unpack type aliases. - pub(crate) fn map_leave_aliases( - self, - db: &'db dyn Db, - transform_fn: impl FnMut(&Type<'db>) -> Type<'db>, - ) -> Type<'db> { - self.elements(db) - .iter() - .map(transform_fn) - .fold( - UnionBuilder::new(db).unpack_aliases(false), - UnionBuilder::add, - ) - .recursively_defined(self.recursively_defined(db)) - .build() - } - - /// A fallible version of [`UnionType::map`]. - /// - /// For each element in `self`, `transform_fn` is called on that element. - /// If `transform_fn` returns `Some()` for all elements in `self`, - /// the result of unioning all transformed elements is returned. - /// As soon as `transform_fn` returns `None` for an element, however, - /// the function short-circuits and returns `None`. - pub(crate) fn try_map( - self, - db: &'db dyn Db, - transform_fn: impl FnMut(&Type<'db>) -> Option>, - ) -> Option> { - let mut builder = UnionBuilder::new(db); - for element in self.elements(db).iter().map(transform_fn) { - builder = builder.add(element?); - } - builder = builder.recursively_defined(self.recursively_defined(db)); - Some(builder.build()) - } - - pub(crate) fn to_instance(self, db: &'db dyn Db) -> Option> { - self.try_map(db, |element| element.to_instance(db)) - } - - pub(crate) fn filter(self, db: &'db dyn Db, f: impl FnMut(&Type<'db>) -> bool) -> Type<'db> { - let current = self.elements(db); - let new: Box<[Type<'db>]> = current.iter().copied().filter(f).collect(); - match &*new { - [] => Type::Never, - [single] => *single, - _ if new.len() == current.len() => Type::Union(self), - _ => Type::Union(UnionType::new(db, new, self.recursively_defined(db))), - } - } - - pub(crate) fn map_with_boundness( - self, - db: &'db dyn Db, - mut transform_fn: impl FnMut(&Type<'db>) -> Place<'db>, - ) -> Place<'db> { - let mut builder = UnionBuilder::new(db); - - let mut all_unbound = true; - let mut possibly_unbound = false; - let mut origin = TypeOrigin::Declared; - for ty in self.elements(db) { - let ty_member = transform_fn(ty); - match ty_member { - Place::Undefined => { - possibly_unbound = true; - } - Place::Defined(DefinedPlace { - ty: ty_member, - origin: member_origin, - definedness: member_boundness, - .. - }) => { - origin = origin.merge(member_origin); - if member_boundness == Definedness::PossiblyUndefined { - possibly_unbound = true; - } - - all_unbound = false; - builder = builder.add(ty_member); - } - } - } - - if all_unbound { - Place::Undefined - } else { - Place::Defined(DefinedPlace { - ty: builder - .recursively_defined(self.recursively_defined(db)) - .build(), - origin, - definedness: if possibly_unbound { - Definedness::PossiblyUndefined - } else { - Definedness::AlwaysDefined - }, - widening: Widening::None, - }) - } - } - - pub(crate) fn map_with_boundness_and_qualifiers( - self, - db: &'db dyn Db, - mut transform_fn: impl FnMut(&Type<'db>) -> PlaceAndQualifiers<'db>, - ) -> PlaceAndQualifiers<'db> { - let mut builder = UnionBuilder::new(db); - let mut qualifiers = TypeQualifiers::empty(); - - let mut all_unbound = true; - let mut possibly_unbound = false; - let mut origin = TypeOrigin::Declared; - for ty in self.elements(db) { - let PlaceAndQualifiers { - place: ty_member, - qualifiers: new_qualifiers, - } = transform_fn(ty); - qualifiers |= new_qualifiers; - match ty_member { - Place::Undefined => { - possibly_unbound = true; - } - Place::Defined(DefinedPlace { - ty: ty_member, - origin: member_origin, - definedness: member_boundness, - .. - }) => { - origin = origin.merge(member_origin); - if member_boundness == Definedness::PossiblyUndefined { - possibly_unbound = true; - } - - all_unbound = false; - builder = builder.add(ty_member); - } - } - } - PlaceAndQualifiers { - place: if all_unbound { - Place::Undefined - } else { - Place::Defined(DefinedPlace { - ty: builder - .recursively_defined(self.recursively_defined(db)) - .build(), - origin, - definedness: if possibly_unbound { - Definedness::PossiblyUndefined - } else { - Definedness::AlwaysDefined - }, - widening: Widening::None, - }) - }, - qualifiers, - } - } - - fn recursive_type_normalized_impl( - self, - db: &'db dyn Db, - div: Type<'db>, - nested: bool, - ) -> Option> { - let mut builder = UnionBuilder::new(db) - .unpack_aliases(false) - .cycle_recovery(true) - .recursively_defined(self.recursively_defined(db)); - let mut empty = true; - for ty in self.elements(db) { - if nested { - // list[T | Divergent] => list[Divergent] - let ty = ty.recursive_type_normalized_impl(db, div, nested)?; - if ty == div { - return Some(ty); - } - builder = builder.add(ty); - empty = false; - } else { - // `Divergent` in a union type does not mean true divergence, so we skip it if not nested. - // e.g. T | Divergent == T | (T | (T | (T | ...))) == T - if ty == &div { - builder = builder.recursively_defined(RecursivelyDefined::Yes); - continue; - } - builder = builder.add( - ty.recursive_type_normalized_impl(db, div, nested) - .unwrap_or(div), - ); - empty = false; - } - } - if empty { - builder = builder.add(div); - } - Some(builder.build()) - } - - /// Identify some specific unions of known classes, currently the ones that `float` and - /// `complex` expand into in type position. - pub(crate) fn known(self, db: &'db dyn Db) -> Option { - let mut has_int = false; - let mut has_float = false; - let mut has_complex = false; - for element in self.elements(db) { - match element.as_nominal_instance()?.known_class(db)? { - KnownClass::Int => has_int = true, - KnownClass::Float => has_float = true, - KnownClass::Complex => has_complex = true, - _ => return None, - } - } - match (has_int, has_float, has_complex) { - (true, true, false) => Some(KnownUnion::Float), - (true, true, true) => Some(KnownUnion::Complex), - _ => None, - } - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub(crate) enum KnownUnion { - Float, // `int | float` - Complex, // `int | float | complex` -} - -impl KnownUnion { - pub(crate) fn to_type(self, db: &dyn Db) -> Type<'_> { - match self { - KnownUnion::Float => UnionType::from_two_elements( - db, - KnownClass::Int.to_instance(db), - KnownClass::Float.to_instance(db), - ), - KnownUnion::Complex => UnionType::from_elements( - db, - [ - KnownClass::Int.to_instance(db), - KnownClass::Float.to_instance(db), - KnownClass::Complex.to_instance(db), - ], - ), - } - } -} - -#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] -pub struct IntersectionType<'db> { - /// The intersection type includes only values in all of these types. - #[returns(ref)] - positive: FxOrderSet>, - - /// The intersection type does not include any value in any of these types. - /// - /// Negation types aren't expressible in annotations, and are most likely to arise from type - /// narrowing along with intersections (e.g. `if not isinstance(...)`), so we represent them - /// directly in intersections rather than as a separate type. - #[returns(ref)] - negative: NegativeIntersectionElements<'db>, -} - -/// To avoid unnecessary allocations for the common case of 1 negative elements, -/// we use this enum to represent the negative elements of an intersection type. -/// -/// It should otherwise have identical behavior to `FxOrderSet>`. -/// -/// Note that we do not try to maintain the invariant that length-0 collections -/// are always represented using `Self::Empty`, and that length-1 collections -/// are always represented using `Self::Single`: `Self::Multiple` is permitted -/// to have 0-1 elements in its wrapped data, and this could happen if you called -/// `Self::swap_remove` or `Self::swap_remove_index` on an instance that is -/// already the `Self::Multiple` variant. Maintaining the invariant that -/// 0-length or 1-length collections are always represented using `Self::Empty` -/// and `Self::Single` would add overhead to methods like `Self::swap_remove`, -/// and would have little value. At the point when you're calling that method, a -/// heap allocation has already taken place. -#[derive(Debug, Clone, get_size2::GetSize, salsa::Update, Default)] -pub enum NegativeIntersectionElements<'db> { - #[default] - Empty, - Single(Type<'db>), - Multiple(FxOrderSet>), -} - -impl<'db> NegativeIntersectionElements<'db> { - pub(crate) fn iter(&self) -> NegativeIntersectionElementsIterator<'_, 'db> { - match self { - Self::Empty => NegativeIntersectionElementsIterator::EmptyOrOne(None), - Self::Single(ty) => NegativeIntersectionElementsIterator::EmptyOrOne(Some(ty)), - Self::Multiple(set) => NegativeIntersectionElementsIterator::Multiple(set.iter()), - } - } - - pub(crate) fn len(&self) -> usize { - match self { - Self::Empty => 0, - Self::Single(_) => 1, - Self::Multiple(set) => set.len(), - } - } - - pub(crate) fn contains(&self, ty: &Type<'db>) -> bool { - match self { - Self::Empty => false, - Self::Single(existing) => existing == ty, - Self::Multiple(set) => set.contains(ty), - } - } - - pub(crate) fn is_empty(&self) -> bool { - // See struct-level comment: we don't try to maintain the invariant that empty - // collections are representend as `Self::Empty` - self.len() == 0 - } - - /// Insert the type into the collection. - /// - /// Returns `true` if the elements was newly added. - /// Returns `false` if the element was already present in the collection. - pub(crate) fn insert(&mut self, ty: Type<'db>) -> bool { - match self { - Self::Empty => { - *self = Self::Single(ty); - true - } - Self::Single(existing) => { - if ty != *existing { - *self = Self::Multiple(FxOrderSet::from_iter([*existing, ty])); - true - } else { - false - } - } - Self::Multiple(set) => set.insert(ty), - } - } - - /// Shrink the capacity of the collection as much as possible. - pub(crate) fn shrink_to_fit(&mut self) { - match self { - Self::Empty | Self::Single(_) => {} - Self::Multiple(set) => set.shrink_to_fit(), - } - } - - /// Remove `ty` from the collection. - /// - /// Returns `true` if `ty` was previously in the collection and has now been removed. - /// Returns `false` if `ty` was never present in the collection. - /// - /// If `ty` was previously present in the collection, - /// the last element in the collection is popped off the end of the collection - /// and placed at the index where `ty` was previously, allowing this method to complete - /// in O(1) time (average). - pub(crate) fn swap_remove(&mut self, ty: &Type<'db>) -> bool { - match self { - Self::Empty => false, - Self::Single(existing) => { - if existing == ty { - *self = Self::Empty; - true - } else { - false - } - } - // See struct-level comment: we don't try to maintain the invariant that collections - // with size 0 or 1 are represented as `Empty` or `Single`. - Self::Multiple(set) => set.swap_remove(ty), - } - } - - /// Remove the element at `index` from the collection. - /// - /// The element is removed by swapping it with the last element - /// of the collection and popping it off, allowing this method to complete - /// in O(1) time (average). - pub(crate) fn swap_remove_index(&mut self, index: usize) -> Option> { - match self { - Self::Empty => None, - Self::Single(existing) => { - if index == 0 { - let ty = *existing; - *self = Self::Empty; - Some(ty) - } else { - None - } - } - // See struct-level comment: we don't try to maintain the invariant that collections - // with size 0 or 1 are represented as `Empty` or `Single`. - Self::Multiple(set) => set.swap_remove_index(index), - } - } - - /// Apply a transformation to all elements in this collection, - /// and return a new collection of the transformed elements. - fn map(&self, map_fn: impl Fn(&Type<'db>) -> Type<'db>) -> Self { - match self { - NegativeIntersectionElements::Empty => NegativeIntersectionElements::Empty, - NegativeIntersectionElements::Single(ty) => { - NegativeIntersectionElements::Single(map_fn(ty)) - } - NegativeIntersectionElements::Multiple(set) => { - NegativeIntersectionElements::Multiple(set.iter().map(map_fn).collect()) - } - } - } - - /// Apply a fallible transformation to all elements in this collection, - /// and return a new collection of the transformed elements. - /// - /// Returns `None` if `map_fn` fails for any element in the collection. - fn try_map(&self, map_fn: impl Fn(&Type<'db>) -> Option>) -> Option { - match self { - NegativeIntersectionElements::Empty => Some(NegativeIntersectionElements::Empty), - NegativeIntersectionElements::Single(ty) => { - map_fn(ty).map(NegativeIntersectionElements::Single) - } - NegativeIntersectionElements::Multiple(set) => { - Some(NegativeIntersectionElements::Multiple( - set.iter().map(map_fn).collect::>()?, - )) - } - } - } -} - -impl<'a, 'db> IntoIterator for &'a NegativeIntersectionElements<'db> { - type Item = &'a Type<'db>; - type IntoIter = NegativeIntersectionElementsIterator<'a, 'db>; - - fn into_iter(self) -> Self::IntoIter { - self.iter() - } -} - -impl PartialEq for NegativeIntersectionElements<'_> { - fn eq(&self, other: &Self) -> bool { - // Same implementation as `OrderSet::eq` - self.len() == other.len() && self.iter().eq(other) - } -} - -impl Eq for NegativeIntersectionElements<'_> {} - -impl std::hash::Hash for NegativeIntersectionElements<'_> { - fn hash(&self, state: &mut H) { - // Same implementation as `OrderSet::hash` - self.len().hash(state); - for value in self { - value.hash(state); - } - } -} - -#[derive(Debug)] -pub enum NegativeIntersectionElementsIterator<'a, 'db> { - EmptyOrOne(Option<&'a Type<'db>>), - Multiple(ordermap::set::Iter<'a, Type<'db>>), -} - -impl<'a, 'db> Iterator for NegativeIntersectionElementsIterator<'a, 'db> { - type Item = &'a Type<'db>; - - fn next(&mut self) -> Option { - match self { - NegativeIntersectionElementsIterator::EmptyOrOne(opt) => opt.take(), - NegativeIntersectionElementsIterator::Multiple(iter) => iter.next(), - } - } -} - -impl std::iter::FusedIterator for NegativeIntersectionElementsIterator<'_, '_> {} - -// The Salsa heap is tracked separately. -impl get_size2::GetSize for IntersectionType<'_> {} - -pub(super) fn walk_intersection_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( - db: &'db dyn Db, - intersection: IntersectionType<'db>, - visitor: &V, -) { - for element in intersection.positive(db) { - visitor.visit_type(db, *element); - } - for element in intersection.negative(db) { - visitor.visit_type(db, *element); - } -} - -#[salsa::tracked] -impl<'db> IntersectionType<'db> { - /// Create an intersection type `E1 & E2 & ... & En` from a list of (positive) elements. - /// - /// For performance reasons, consider using [`IntersectionType::from_two_elements`] if - /// the intersection is constructed from exactly two elements. - pub(crate) fn from_elements(db: &'db dyn Db, elements: I) -> Type<'db> - where - I: IntoIterator, - T: Into>, - { - IntersectionBuilder::new(db) - .positive_elements(elements) - .build() - } - - /// Create an intersection type `A & B` from two elements `A` and `B`. - #[salsa::tracked( - cycle_initial=|_, id, _, _| Type::divergent(id), - cycle_fn=|db, cycle, previous: &Type<'db>, result: Type<'db>, _, _| { - result.cycle_normalized(db, *previous, cycle) - }, - heap_size=ruff_memory_usage::heap_size - )] - fn from_two_elements(db: &'db dyn Db, a: Type<'db>, b: Type<'db>) -> Type<'db> { - IntersectionBuilder::new(db) - .positive_elements([a, b]) - .build() - } - - pub(crate) fn recursive_type_normalized_impl( - self, - db: &'db dyn Db, - div: Type<'db>, - nested: bool, - ) -> Option { - let positive = if nested { - self.positive(db) - .iter() - .map(|ty| ty.recursive_type_normalized_impl(db, div, nested)) - .collect::>>>()? - } else { - self.positive(db) - .iter() - .map(|ty| { - ty.recursive_type_normalized_impl(db, div, nested) - .unwrap_or(div) - }) - .collect() - }; - - let negative = if nested { - self.negative(db) - .try_map(|ty| ty.recursive_type_normalized_impl(db, div, nested))? - } else { - self.negative(db).map(|ty| { - ty.recursive_type_normalized_impl(db, div, nested) - .unwrap_or(div) - }) - }; - - Some(IntersectionType::new(db, positive, negative)) - } - - /// Returns an iterator over the positive elements of the intersection. If - /// there are no positive elements, returns a single `object` type. - pub(crate) fn positive_elements_or_object( - self, - db: &'db dyn Db, - ) -> impl Iterator> { - if self.positive(db).is_empty() { - Either::Left(std::iter::once(Type::object())) - } else { - Either::Right(self.positive(db).iter().copied()) - } - } - - /// Map a type transformation over all positive elements of the intersection. Leave the - /// negative elements unchanged. - pub(crate) fn map_positive( - self, - db: &'db dyn Db, - mut transform_fn: impl FnMut(&Type<'db>) -> Type<'db>, - ) -> Type<'db> { - let mut builder = IntersectionBuilder::new(db); - for ty in self.positive(db) { - builder = builder.add_positive(transform_fn(ty)); - } - for ty in self.negative(db) { - builder = builder.add_negative(*ty); - } - builder.build() - } - - pub(crate) fn map_with_boundness( - self, - db: &'db dyn Db, - mut transform_fn: impl FnMut(&Type<'db>) -> Place<'db>, - ) -> Place<'db> { - let mut builder = IntersectionBuilder::new(db); - - let mut all_unbound = true; - let mut any_definitely_bound = false; - let mut origin = TypeOrigin::Declared; - for ty in self.positive_elements_or_object(db) { - let ty_member = transform_fn(&ty); - match ty_member { - Place::Undefined => {} - Place::Defined(DefinedPlace { - ty: ty_member, - origin: member_origin, - definedness: member_boundness, - .. - }) => { - origin = origin.merge(member_origin); - all_unbound = false; - if member_boundness == Definedness::AlwaysDefined { - any_definitely_bound = true; - } - - builder = builder.add_positive(ty_member); - } - } - } - - if all_unbound { - Place::Undefined - } else { - Place::Defined(DefinedPlace { - ty: builder.build(), - origin, - definedness: if any_definitely_bound { - Definedness::AlwaysDefined - } else { - Definedness::PossiblyUndefined - }, - widening: Widening::None, - }) - } - } - - pub(crate) fn map_with_boundness_and_qualifiers( - self, - db: &'db dyn Db, - mut transform_fn: impl FnMut(&Type<'db>) -> PlaceAndQualifiers<'db>, - ) -> PlaceAndQualifiers<'db> { - let mut builder = IntersectionBuilder::new(db); - let mut qualifiers = TypeQualifiers::empty(); - - let mut all_unbound = true; - let mut any_definitely_bound = false; - let mut origin = TypeOrigin::Declared; - for ty in self.positive_elements_or_object(db) { - let PlaceAndQualifiers { - place: member, - qualifiers: new_qualifiers, - } = transform_fn(&ty); - qualifiers |= new_qualifiers; - match member { - Place::Undefined => {} - Place::Defined(DefinedPlace { - ty: ty_member, - origin: member_origin, - definedness: member_boundness, - .. - }) => { - origin = origin.merge(member_origin); - all_unbound = false; - if member_boundness == Definedness::AlwaysDefined { - any_definitely_bound = true; - } - - builder = builder.add_positive(ty_member); - } - } - } - - PlaceAndQualifiers { - place: if all_unbound { - Place::Undefined - } else { - Place::Defined(DefinedPlace { - ty: builder.build(), - origin, - definedness: if any_definitely_bound { - Definedness::AlwaysDefined - } else { - Definedness::PossiblyUndefined - }, - widening: Widening::None, - }) - }, - qualifiers, - } - } - - pub fn iter_positive(self, db: &'db dyn Db) -> impl Iterator> { - self.positive(db).iter().copied() - } - - pub fn iter_negative(self, db: &'db dyn Db) -> impl Iterator> { - self.negative(db).iter().copied() - } - - pub(crate) fn has_one_element(self, db: &'db dyn Db) -> bool { - (self.positive(db).len() + self.negative(db).len()) == 1 - } - - pub(crate) fn is_simple_negation(self, db: &'db dyn Db) -> bool { - self.positive(db).is_empty() && self.negative(db).len() == 1 - } -} - #[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] pub struct TypeIsType<'db> { return_type: Type<'db>, diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 6754de4c9538c..da349dc8045c7 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -59,7 +59,6 @@ use crate::semantic_index::{ ApplicableConstraints, EnclosingSnapshotResult, SemanticIndex, attribute_assignments, place_table, }; -use crate::types::builder::RecursivelyDefined; use crate::types::call::bind::MatchingOverloadIndex; use crate::types::call::{Argument, Binding, Bindings, CallArguments, CallError, CallErrorKind}; use crate::types::class::{ @@ -120,6 +119,7 @@ use crate::types::infer::builder::paramspec_validation::validate_paramspec_compo use crate::types::infer::{nearest_enclosing_class, nearest_enclosing_function}; use crate::types::mro::{DynamicMroErrorKind, StaticMroErrorKind}; use crate::types::newtype::NewType; +use crate::types::set_theoretic::RecursivelyDefined; use crate::types::subclass_of::SubclassOfInner; use crate::types::tuple::{Tuple, TupleLength, TupleSpecBuilder, TupleType}; use crate::types::typed_dict::{ diff --git a/crates/ty_python_semantic/src/types/relation.rs b/crates/ty_python_semantic/src/types/relation.rs index da9f7a45fc5ef..f350ee1f874e8 100644 --- a/crates/ty_python_semantic/src/types/relation.rs +++ b/crates/ty_python_semantic/src/types/relation.rs @@ -3,11 +3,11 @@ use ruff_python_ast::name::Name; use rustc_hash::FxHashSet; use crate::place::{DefinedPlace, Place}; -use crate::types::builder::RecursivelyDefined; use crate::types::constraints::{ ConstraintSetBuilder, IteratorConstraintsExtension, OptionConstraintsExtension, }; use crate::types::enums::is_single_member_enum; +use crate::types::set_theoretic::RecursivelyDefined; use crate::types::{ CallableType, ClassBase, ClassType, CycleDetector, DynamicType, KnownClass, KnownInstanceType, LiteralValueTypeKind, MemberLookupPolicy, PairVisitor, ProtocolInstanceType, SubclassOfInner, diff --git a/crates/ty_python_semantic/src/types/set_theoretic.rs b/crates/ty_python_semantic/src/types/set_theoretic.rs new file mode 100644 index 0000000000000..5cec8af50eae0 --- /dev/null +++ b/crates/ty_python_semantic/src/types/set_theoretic.rs @@ -0,0 +1,854 @@ +use itertools::Either; + +use crate::place::{DefinedPlace, Definedness, Place, PlaceAndQualifiers, TypeOrigin, Widening}; +use crate::types::class::KnownClass; +use crate::types::visitor; +use crate::types::{Type, TypeQualifiers}; +use crate::{Db, FxOrderSet}; + +pub(crate) mod builder; + +pub(crate) use builder::{IntersectionBuilder, UnionBuilder}; + +#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] +pub struct UnionType<'db> { + /// The union type includes values in any of these types. + #[returns(deref)] + pub elements: Box<[Type<'db>]>, + /// Whether the value pointed to by this type is recursively defined. + /// If `Yes`, union literal widening is performed early. + pub(crate) recursively_defined: RecursivelyDefined, +} + +pub(crate) fn walk_union<'db, V: visitor::TypeVisitor<'db> + ?Sized>( + db: &'db dyn Db, + union: UnionType<'db>, + visitor: &V, +) { + for element in union.elements(db) { + visitor.visit_type(db, *element); + } +} + +// The Salsa heap is tracked separately. +impl get_size2::GetSize for UnionType<'_> {} + +#[salsa::tracked] +impl<'db> UnionType<'db> { + /// Create a union from a list of elements + /// (which may be eagerly simplified into a different variant of [`Type`] altogether). + /// + /// For performance reasons, consider using [`UnionType::from_two_elements`] if + /// the union is constructed from exactly two elements. + pub fn from_elements(db: &'db dyn Db, elements: I) -> Type<'db> + where + I: IntoIterator, + T: Into>, + { + elements + .into_iter() + .fold(UnionBuilder::new(db), |builder, element| { + builder.add(element.into()) + }) + .build() + } + + /// Create a union type `A | B` from two elements `A` and `B`. + #[salsa::tracked( + cycle_initial=|_, id, _, _| Type::divergent(id), + cycle_fn=|db, cycle, previous: &Type<'db>, result: Type<'db>, _, _| { + result.cycle_normalized(db, *previous, cycle) + }, + heap_size=ruff_memory_usage::heap_size + )] + pub fn from_two_elements(db: &'db dyn Db, a: Type<'db>, b: Type<'db>) -> Type<'db> { + UnionBuilder::new(db).add(a).add(b).build() + } + + /// Create a union from a list of elements without unpacking type aliases. + pub(crate) fn from_elements_leave_aliases(db: &'db dyn Db, elements: I) -> Type<'db> + where + I: IntoIterator, + T: Into>, + { + elements + .into_iter() + .fold( + UnionBuilder::new(db).unpack_aliases(false), + |builder, element| builder.add(element.into()), + ) + .build() + } + + pub(crate) fn from_elements_cycle_recovery(db: &'db dyn Db, elements: I) -> Type<'db> + where + I: IntoIterator, + T: Into>, + { + elements + .into_iter() + .fold( + UnionBuilder::new(db).cycle_recovery(true), + |builder, element| builder.add(element.into()), + ) + .build() + } + + /// A fallible version of [`UnionType::from_elements`]. + /// + /// If all items in `elements` are `Some()`, the result of unioning all elements is returned. + /// As soon as a `None` element in the iterable is encountered, + /// the function short-circuits and returns `None`. + pub(crate) fn try_from_elements(db: &'db dyn Db, elements: I) -> Option> + where + I: IntoIterator>, + T: Into>, + { + let mut builder = UnionBuilder::new(db); + for element in elements { + builder = builder.add(element?.into()); + } + Some(builder.build()) + } + + /// Apply a transformation function to all elements of the union, + /// and create a new union from the resulting set of types. + pub(crate) fn map( + self, + db: &'db dyn Db, + transform_fn: impl FnMut(&Type<'db>) -> Type<'db>, + ) -> Type<'db> { + self.elements(db) + .iter() + .map(transform_fn) + .fold(UnionBuilder::new(db), |builder, element| { + builder.add(element) + }) + .recursively_defined(self.recursively_defined(db)) + .build() + } + + /// A version of [`UnionType::map`] that does not unpack type aliases. + pub(crate) fn map_leave_aliases( + self, + db: &'db dyn Db, + transform_fn: impl FnMut(&Type<'db>) -> Type<'db>, + ) -> Type<'db> { + self.elements(db) + .iter() + .map(transform_fn) + .fold( + UnionBuilder::new(db).unpack_aliases(false), + UnionBuilder::add, + ) + .recursively_defined(self.recursively_defined(db)) + .build() + } + + /// A fallible version of [`UnionType::map`]. + /// + /// For each element in `self`, `transform_fn` is called on that element. + /// If `transform_fn` returns `Some()` for all elements in `self`, + /// the result of unioning all transformed elements is returned. + /// As soon as `transform_fn` returns `None` for an element, however, + /// the function short-circuits and returns `None`. + pub(crate) fn try_map( + self, + db: &'db dyn Db, + transform_fn: impl FnMut(&Type<'db>) -> Option>, + ) -> Option> { + let mut builder = UnionBuilder::new(db); + for element in self.elements(db).iter().map(transform_fn) { + builder = builder.add(element?); + } + builder = builder.recursively_defined(self.recursively_defined(db)); + Some(builder.build()) + } + + pub(crate) fn to_instance(self, db: &'db dyn Db) -> Option> { + self.try_map(db, |element| element.to_instance(db)) + } + + pub(crate) fn filter(self, db: &'db dyn Db, f: impl FnMut(&Type<'db>) -> bool) -> Type<'db> { + let current = self.elements(db); + let new: Box<[Type<'db>]> = current.iter().copied().filter(f).collect(); + match &*new { + [] => Type::Never, + [single] => *single, + _ if new.len() == current.len() => Type::Union(self), + _ => Type::Union(UnionType::new(db, new, self.recursively_defined(db))), + } + } + + pub(crate) fn map_with_boundness( + self, + db: &'db dyn Db, + mut transform_fn: impl FnMut(&Type<'db>) -> Place<'db>, + ) -> Place<'db> { + let mut builder = UnionBuilder::new(db); + + let mut all_unbound = true; + let mut possibly_unbound = false; + let mut origin = TypeOrigin::Declared; + for ty in self.elements(db) { + let ty_member = transform_fn(ty); + match ty_member { + Place::Undefined => { + possibly_unbound = true; + } + Place::Defined(DefinedPlace { + ty: ty_member, + origin: member_origin, + definedness: member_boundness, + .. + }) => { + origin = origin.merge(member_origin); + if member_boundness == Definedness::PossiblyUndefined { + possibly_unbound = true; + } + + all_unbound = false; + builder = builder.add(ty_member); + } + } + } + + if all_unbound { + Place::Undefined + } else { + Place::Defined(DefinedPlace { + ty: builder + .recursively_defined(self.recursively_defined(db)) + .build(), + origin, + definedness: if possibly_unbound { + Definedness::PossiblyUndefined + } else { + Definedness::AlwaysDefined + }, + widening: Widening::None, + }) + } + } + + pub(crate) fn map_with_boundness_and_qualifiers( + self, + db: &'db dyn Db, + mut transform_fn: impl FnMut(&Type<'db>) -> PlaceAndQualifiers<'db>, + ) -> PlaceAndQualifiers<'db> { + let mut builder = UnionBuilder::new(db); + let mut qualifiers = TypeQualifiers::empty(); + + let mut all_unbound = true; + let mut possibly_unbound = false; + let mut origin = TypeOrigin::Declared; + for ty in self.elements(db) { + let PlaceAndQualifiers { + place: ty_member, + qualifiers: new_qualifiers, + } = transform_fn(ty); + qualifiers |= new_qualifiers; + match ty_member { + Place::Undefined => { + possibly_unbound = true; + } + Place::Defined(DefinedPlace { + ty: ty_member, + origin: member_origin, + definedness: member_boundness, + .. + }) => { + origin = origin.merge(member_origin); + if member_boundness == Definedness::PossiblyUndefined { + possibly_unbound = true; + } + + all_unbound = false; + builder = builder.add(ty_member); + } + } + } + PlaceAndQualifiers { + place: if all_unbound { + Place::Undefined + } else { + Place::Defined(DefinedPlace { + ty: builder + .recursively_defined(self.recursively_defined(db)) + .build(), + origin, + definedness: if possibly_unbound { + Definedness::PossiblyUndefined + } else { + Definedness::AlwaysDefined + }, + widening: Widening::None, + }) + }, + qualifiers, + } + } + + pub(crate) fn recursive_type_normalized_impl( + self, + db: &'db dyn Db, + div: Type<'db>, + nested: bool, + ) -> Option> { + let mut builder = UnionBuilder::new(db) + .unpack_aliases(false) + .cycle_recovery(true) + .recursively_defined(self.recursively_defined(db)); + let mut empty = true; + for ty in self.elements(db) { + if nested { + // list[T | Divergent] => list[Divergent] + let ty = ty.recursive_type_normalized_impl(db, div, nested)?; + if ty == div { + return Some(ty); + } + builder = builder.add(ty); + empty = false; + } else { + // `Divergent` in a union type does not mean true divergence, so we skip it if not nested. + // e.g. T | Divergent == T | (T | (T | (T | ...))) == T + if ty == &div { + builder = builder.recursively_defined(RecursivelyDefined::Yes); + continue; + } + builder = builder.add( + ty.recursive_type_normalized_impl(db, div, nested) + .unwrap_or(div), + ); + empty = false; + } + } + if empty { + builder = builder.add(div); + } + Some(builder.build()) + } + + /// Identify some specific unions of known classes, currently the ones that `float` and + /// `complex` expand into in type position. + pub(crate) fn known(self, db: &'db dyn Db) -> Option { + let mut has_int = false; + let mut has_float = false; + let mut has_complex = false; + for element in self.elements(db) { + match element.as_nominal_instance()?.known_class(db)? { + KnownClass::Int => has_int = true, + KnownClass::Float => has_float = true, + KnownClass::Complex => has_complex = true, + _ => return None, + } + } + match (has_int, has_float, has_complex) { + (true, true, false) => Some(KnownUnion::Float), + (true, true, true) => Some(KnownUnion::Complex), + _ => None, + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub(crate) enum KnownUnion { + Float, // `int | float` + Complex, // `int | float | complex` +} + +impl KnownUnion { + pub(crate) fn to_type(self, db: &dyn Db) -> Type<'_> { + match self { + KnownUnion::Float => UnionType::from_two_elements( + db, + KnownClass::Int.to_instance(db), + KnownClass::Float.to_instance(db), + ), + KnownUnion::Complex => UnionType::from_elements( + db, + [ + KnownClass::Int.to_instance(db), + KnownClass::Float.to_instance(db), + KnownClass::Complex.to_instance(db), + ], + ), + } + } +} + +#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)] +pub struct IntersectionType<'db> { + /// The intersection type includes only values in all of these types. + #[returns(ref)] + pub(crate) positive: FxOrderSet>, + + /// The intersection type does not include any value in any of these types. + /// + /// Negation types aren't expressible in annotations, and are most likely to arise from type + /// narrowing along with intersections (e.g. `if not isinstance(...)`), so we represent them + /// directly in intersections rather than as a separate type. + #[returns(ref)] + pub(crate) negative: NegativeIntersectionElements<'db>, +} + +/// To avoid unnecessary allocations for the common case of 1 negative elements, +/// we use this enum to represent the negative elements of an intersection type. +/// +/// It should otherwise have identical behavior to `FxOrderSet>`. +/// +/// Note that we do not try to maintain the invariant that length-0 collections +/// are always represented using `Self::Empty`, and that length-1 collections +/// are always represented using `Self::Single`: `Self::Multiple` is permitted +/// to have 0-1 elements in its wrapped data, and this could happen if you called +/// `Self::swap_remove` or `Self::swap_remove_index` on an instance that is +/// already the `Self::Multiple` variant. Maintaining the invariant that +/// 0-length or 1-length collections are always represented using `Self::Empty` +/// and `Self::Single` would add overhead to methods like `Self::swap_remove`, +/// and would have little value. At the point when you're calling that method, a +/// heap allocation has already taken place. +#[derive(Debug, Clone, get_size2::GetSize, salsa::Update, Default)] +pub enum NegativeIntersectionElements<'db> { + #[default] + Empty, + Single(Type<'db>), + Multiple(FxOrderSet>), +} + +impl<'db> NegativeIntersectionElements<'db> { + pub(crate) fn iter(&self) -> NegativeIntersectionElementsIterator<'_, 'db> { + match self { + Self::Empty => NegativeIntersectionElementsIterator::EmptyOrOne(None), + Self::Single(ty) => NegativeIntersectionElementsIterator::EmptyOrOne(Some(ty)), + Self::Multiple(set) => NegativeIntersectionElementsIterator::Multiple(set.iter()), + } + } + + pub(crate) fn len(&self) -> usize { + match self { + Self::Empty => 0, + Self::Single(_) => 1, + Self::Multiple(set) => set.len(), + } + } + + pub(crate) fn contains(&self, ty: &Type<'db>) -> bool { + match self { + Self::Empty => false, + Self::Single(existing) => existing == ty, + Self::Multiple(set) => set.contains(ty), + } + } + + pub(crate) fn is_empty(&self) -> bool { + // See struct-level comment: we don't try to maintain the invariant that empty + // collections are representend as `Self::Empty` + self.len() == 0 + } + + /// Insert the type into the collection. + /// + /// Returns `true` if the elements was newly added. + /// Returns `false` if the element was already present in the collection. + pub(crate) fn insert(&mut self, ty: Type<'db>) -> bool { + match self { + Self::Empty => { + *self = Self::Single(ty); + true + } + Self::Single(existing) => { + if ty != *existing { + *self = Self::Multiple(FxOrderSet::from_iter([*existing, ty])); + true + } else { + false + } + } + Self::Multiple(set) => set.insert(ty), + } + } + + /// Shrink the capacity of the collection as much as possible. + pub(crate) fn shrink_to_fit(&mut self) { + match self { + Self::Empty | Self::Single(_) => {} + Self::Multiple(set) => set.shrink_to_fit(), + } + } + + /// Remove `ty` from the collection. + /// + /// Returns `true` if `ty` was previously in the collection and has now been removed. + /// Returns `false` if `ty` was never present in the collection. + /// + /// If `ty` was previously present in the collection, + /// the last element in the collection is popped off the end of the collection + /// and placed at the index where `ty` was previously, allowing this method to complete + /// in O(1) time (average). + pub(crate) fn swap_remove(&mut self, ty: &Type<'db>) -> bool { + match self { + Self::Empty => false, + Self::Single(existing) => { + if existing == ty { + *self = Self::Empty; + true + } else { + false + } + } + // See struct-level comment: we don't try to maintain the invariant that collections + // with size 0 or 1 are represented as `Empty` or `Single`. + Self::Multiple(set) => set.swap_remove(ty), + } + } + + /// Remove the element at `index` from the collection. + /// + /// The element is removed by swapping it with the last element + /// of the collection and popping it off, allowing this method to complete + /// in O(1) time (average). + pub(crate) fn swap_remove_index(&mut self, index: usize) -> Option> { + match self { + Self::Empty => None, + Self::Single(existing) => { + if index == 0 { + let ty = *existing; + *self = Self::Empty; + Some(ty) + } else { + None + } + } + // See struct-level comment: we don't try to maintain the invariant that collections + // with size 0 or 1 are represented as `Empty` or `Single`. + Self::Multiple(set) => set.swap_remove_index(index), + } + } + + /// Apply a transformation to all elements in this collection, + /// and return a new collection of the transformed elements. + fn map(&self, map_fn: impl Fn(&Type<'db>) -> Type<'db>) -> Self { + match self { + NegativeIntersectionElements::Empty => NegativeIntersectionElements::Empty, + NegativeIntersectionElements::Single(ty) => { + NegativeIntersectionElements::Single(map_fn(ty)) + } + NegativeIntersectionElements::Multiple(set) => { + NegativeIntersectionElements::Multiple(set.iter().map(map_fn).collect()) + } + } + } + + /// Apply a fallible transformation to all elements in this collection, + /// and return a new collection of the transformed elements. + /// + /// Returns `None` if `map_fn` fails for any element in the collection. + fn try_map(&self, map_fn: impl Fn(&Type<'db>) -> Option>) -> Option { + match self { + NegativeIntersectionElements::Empty => Some(NegativeIntersectionElements::Empty), + NegativeIntersectionElements::Single(ty) => { + map_fn(ty).map(NegativeIntersectionElements::Single) + } + NegativeIntersectionElements::Multiple(set) => { + Some(NegativeIntersectionElements::Multiple( + set.iter().map(map_fn).collect::>()?, + )) + } + } + } +} + +impl<'a, 'db> IntoIterator for &'a NegativeIntersectionElements<'db> { + type Item = &'a Type<'db>; + type IntoIter = NegativeIntersectionElementsIterator<'a, 'db>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +impl PartialEq for NegativeIntersectionElements<'_> { + fn eq(&self, other: &Self) -> bool { + // Same implementation as `OrderSet::eq` + self.len() == other.len() && self.iter().eq(other) + } +} + +impl Eq for NegativeIntersectionElements<'_> {} + +impl std::hash::Hash for NegativeIntersectionElements<'_> { + fn hash(&self, state: &mut H) { + // Same implementation as `OrderSet::hash` + self.len().hash(state); + for value in self { + value.hash(state); + } + } +} + +#[derive(Debug)] +pub enum NegativeIntersectionElementsIterator<'a, 'db> { + EmptyOrOne(Option<&'a Type<'db>>), + Multiple(ordermap::set::Iter<'a, Type<'db>>), +} + +impl<'a, 'db> Iterator for NegativeIntersectionElementsIterator<'a, 'db> { + type Item = &'a Type<'db>; + + fn next(&mut self) -> Option { + match self { + NegativeIntersectionElementsIterator::EmptyOrOne(opt) => opt.take(), + NegativeIntersectionElementsIterator::Multiple(iter) => iter.next(), + } + } +} + +impl std::iter::FusedIterator for NegativeIntersectionElementsIterator<'_, '_> {} + +// The Salsa heap is tracked separately. +impl get_size2::GetSize for IntersectionType<'_> {} + +pub(crate) fn walk_intersection_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>( + db: &'db dyn Db, + intersection: IntersectionType<'db>, + visitor: &V, +) { + for element in intersection.positive(db) { + visitor.visit_type(db, *element); + } + for element in intersection.negative(db) { + visitor.visit_type(db, *element); + } +} + +#[salsa::tracked] +impl<'db> IntersectionType<'db> { + /// Create an intersection type `E1 & E2 & ... & En` from a list of (positive) elements. + /// + /// For performance reasons, consider using [`IntersectionType::from_two_elements`] if + /// the intersection is constructed from exactly two elements. + pub(crate) fn from_elements(db: &'db dyn Db, elements: I) -> Type<'db> + where + I: IntoIterator, + T: Into>, + { + IntersectionBuilder::new(db) + .positive_elements(elements) + .build() + } + + /// Create an intersection type `A & B` from two elements `A` and `B`. + #[salsa::tracked( + cycle_initial=|_, id, _, _| Type::divergent(id), + cycle_fn=|db, cycle, previous: &Type<'db>, result: Type<'db>, _, _| { + result.cycle_normalized(db, *previous, cycle) + }, + heap_size=ruff_memory_usage::heap_size + )] + pub(crate) fn from_two_elements(db: &'db dyn Db, a: Type<'db>, b: Type<'db>) -> Type<'db> { + IntersectionBuilder::new(db) + .positive_elements([a, b]) + .build() + } + + pub(crate) fn recursive_type_normalized_impl( + self, + db: &'db dyn Db, + div: Type<'db>, + nested: bool, + ) -> Option { + let positive = if nested { + self.positive(db) + .iter() + .map(|ty| ty.recursive_type_normalized_impl(db, div, nested)) + .collect::>>>()? + } else { + self.positive(db) + .iter() + .map(|ty| { + ty.recursive_type_normalized_impl(db, div, nested) + .unwrap_or(div) + }) + .collect() + }; + + let negative = if nested { + self.negative(db) + .try_map(|ty| ty.recursive_type_normalized_impl(db, div, nested))? + } else { + self.negative(db).map(|ty| { + ty.recursive_type_normalized_impl(db, div, nested) + .unwrap_or(div) + }) + }; + + Some(IntersectionType::new(db, positive, negative)) + } + + /// Returns an iterator over the positive elements of the intersection. If + /// there are no positive elements, returns a single `object` type. + pub(crate) fn positive_elements_or_object( + self, + db: &'db dyn Db, + ) -> impl Iterator> { + if self.positive(db).is_empty() { + Either::Left(std::iter::once(Type::object())) + } else { + Either::Right(self.positive(db).iter().copied()) + } + } + + /// Map a type transformation over all positive elements of the intersection. Leave the + /// negative elements unchanged. + pub(crate) fn map_positive( + self, + db: &'db dyn Db, + mut transform_fn: impl FnMut(&Type<'db>) -> Type<'db>, + ) -> Type<'db> { + let mut builder = IntersectionBuilder::new(db); + for ty in self.positive(db) { + builder = builder.add_positive(transform_fn(ty)); + } + for ty in self.negative(db) { + builder = builder.add_negative(*ty); + } + builder.build() + } + + pub(crate) fn map_with_boundness( + self, + db: &'db dyn Db, + mut transform_fn: impl FnMut(&Type<'db>) -> Place<'db>, + ) -> Place<'db> { + let mut builder = IntersectionBuilder::new(db); + + let mut all_unbound = true; + let mut any_definitely_bound = false; + let mut origin = TypeOrigin::Declared; + for ty in self.positive_elements_or_object(db) { + let ty_member = transform_fn(&ty); + match ty_member { + Place::Undefined => {} + Place::Defined(DefinedPlace { + ty: ty_member, + origin: member_origin, + definedness: member_boundness, + .. + }) => { + origin = origin.merge(member_origin); + all_unbound = false; + if member_boundness == Definedness::AlwaysDefined { + any_definitely_bound = true; + } + + builder = builder.add_positive(ty_member); + } + } + } + + if all_unbound { + Place::Undefined + } else { + Place::Defined(DefinedPlace { + ty: builder.build(), + origin, + definedness: if any_definitely_bound { + Definedness::AlwaysDefined + } else { + Definedness::PossiblyUndefined + }, + widening: Widening::None, + }) + } + } + + pub(crate) fn map_with_boundness_and_qualifiers( + self, + db: &'db dyn Db, + mut transform_fn: impl FnMut(&Type<'db>) -> PlaceAndQualifiers<'db>, + ) -> PlaceAndQualifiers<'db> { + let mut builder = IntersectionBuilder::new(db); + let mut qualifiers = TypeQualifiers::empty(); + + let mut all_unbound = true; + let mut any_definitely_bound = false; + let mut origin = TypeOrigin::Declared; + for ty in self.positive_elements_or_object(db) { + let PlaceAndQualifiers { + place: member, + qualifiers: new_qualifiers, + } = transform_fn(&ty); + qualifiers |= new_qualifiers; + match member { + Place::Undefined => {} + Place::Defined(DefinedPlace { + ty: ty_member, + origin: member_origin, + definedness: member_boundness, + .. + }) => { + origin = origin.merge(member_origin); + all_unbound = false; + if member_boundness == Definedness::AlwaysDefined { + any_definitely_bound = true; + } + + builder = builder.add_positive(ty_member); + } + } + } + + PlaceAndQualifiers { + place: if all_unbound { + Place::Undefined + } else { + Place::Defined(DefinedPlace { + ty: builder.build(), + origin, + definedness: if any_definitely_bound { + Definedness::AlwaysDefined + } else { + Definedness::PossiblyUndefined + }, + widening: Widening::None, + }) + }, + qualifiers, + } + } + + pub fn iter_positive(self, db: &'db dyn Db) -> impl Iterator> { + self.positive(db).iter().copied() + } + + pub fn iter_negative(self, db: &'db dyn Db) -> impl Iterator> { + self.negative(db).iter().copied() + } + + pub(crate) fn has_one_element(self, db: &'db dyn Db) -> bool { + (self.positive(db).len() + self.negative(db).len()) == 1 + } + + pub(crate) fn is_simple_negation(self, db: &'db dyn Db) -> bool { + self.positive(db).is_empty() && self.negative(db).len() == 1 + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize)] +pub enum RecursivelyDefined { + Yes, + No, +} + +impl RecursivelyDefined { + const fn is_yes(self) -> bool { + matches!(self, RecursivelyDefined::Yes) + } + + const fn or(self, other: RecursivelyDefined) -> RecursivelyDefined { + match (self, other) { + (RecursivelyDefined::Yes, _) | (_, RecursivelyDefined::Yes) => RecursivelyDefined::Yes, + _ => RecursivelyDefined::No, + } + } +} diff --git a/crates/ty_python_semantic/src/types/builder.rs b/crates/ty_python_semantic/src/types/set_theoretic/builder.rs similarity index 99% rename from crates/ty_python_semantic/src/types/builder.rs rename to crates/ty_python_semantic/src/types/set_theoretic/builder.rs index 3ae43a51d9cd2..291667e3e1145 100644 --- a/crates/ty_python_semantic/src/types/builder.rs +++ b/crates/ty_python_semantic/src/types/set_theoretic/builder.rs @@ -36,6 +36,7 @@ //! shares exactly the same possible super-types, and none of them are subtypes of each other //! (unless exactly the same literal type), we can avoid many unnecessary redundancy checks. +use super::RecursivelyDefined; use crate::types::enums::{enum_member_literals, enum_metadata}; use crate::types::{ BytesLiteralType, ClassLiteral, EnumLiteralType, IntersectionType, KnownClass, @@ -315,25 +316,6 @@ enum ReduceResult<'db> { Type(Type<'db>), } -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize)] -pub enum RecursivelyDefined { - Yes, - No, -} - -impl RecursivelyDefined { - const fn is_yes(self) -> bool { - matches!(self, RecursivelyDefined::Yes) - } - - const fn or(self, other: RecursivelyDefined) -> RecursivelyDefined { - match (self, other) { - (RecursivelyDefined::Yes, _) | (_, RecursivelyDefined::Yes) => RecursivelyDefined::Yes, - _ => RecursivelyDefined::No, - } - } -} - /// If the value ​​is defined recursively, widening is performed from fewer literal elements, /// resulting in faster convergence of the fixed-point iteration. const MAX_RECURSIVE_UNION_LITERALS: usize = 5; diff --git a/crates/ty_python_semantic/src/types/tuple.rs b/crates/ty_python_semantic/src/types/tuple.rs index b35646e53ca60..4c602584db19d 100644 --- a/crates/ty_python_semantic/src/types/tuple.rs +++ b/crates/ty_python_semantic/src/types/tuple.rs @@ -24,13 +24,13 @@ use smallvec::{SmallVec, smallvec_inline}; use crate::semantic_index::definition::Definition; use crate::subscript::{Nth, OutOfBoundsError, PyIndex, PySlice, StepSizeZeroError}; -use crate::types::builder::RecursivelyDefined; use crate::types::class::{ClassType, KnownClass}; use crate::types::constraints::{ ConstraintSet, ConstraintSetBuilder, IteratorConstraintsExtension, }; use crate::types::generics::InferableTypeVars; use crate::types::relation::{HasRelationToVisitor, IsDisjointVisitor, TypeRelation}; +use crate::types::set_theoretic::RecursivelyDefined; use crate::types::{ ApplyTypeMappingVisitor, BoundTypeVarInstance, FindLegacyTypeVarsVisitor, IntersectionType, Type, TypeMapping, UnionBuilder, UnionType,