Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 41 additions & 17 deletions crates/ty_python_semantic/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3484,11 +3484,7 @@ impl<'db> Type<'db> {
Type::IntLiteral(value) => (value >= 0).then_some(ty),
Type::BooleanLiteral(value) => Some(Type::IntLiteral(value.into())),
Type::Union(union) => {
let mut builder = UnionBuilder::new(db);
for element in union.elements(db) {
builder = builder.add(non_negative_int_literal(db, *element)?);
}
Some(builder.build())
union.try_map(db, |element| non_negative_int_literal(db, *element))
}
_ => None,
}
Expand Down Expand Up @@ -4801,13 +4797,7 @@ impl<'db> Type<'db> {
Type::ClassLiteral(class) => Some(Type::instance(db, class.default_specialization(db))),
Type::GenericAlias(alias) => Some(Type::instance(db, ClassType::from(*alias))),
Type::SubclassOf(subclass_of_ty) => Some(subclass_of_ty.to_instance(db)),
Type::Union(union) => {
let mut builder = UnionBuilder::new(db);
for element in union.elements(db) {
builder = builder.add(element.to_instance(db)?);
}
Some(builder.build())
}
Type::Union(union) => union.to_instance(db),
// If there is no bound or constraints on a typevar `T`, `T: object` implicitly, which
// has no instance type. Otherwise, synthesize a typevar with bound or constraints
// mapped through `to_instance`.
Expand All @@ -4817,11 +4807,9 @@ impl<'db> Type<'db> {
TypeVarBoundOrConstraints::UpperBound(upper_bound.to_instance(db)?)
}
TypeVarBoundOrConstraints::Constraints(constraints) => {
let mut builder = UnionBuilder::new(db);
for constraint in constraints.elements(db) {
builder = builder.add(constraint.to_instance(db)?);
}
TypeVarBoundOrConstraints::Constraints(builder.build().into_union()?)
TypeVarBoundOrConstraints::Constraints(
constraints.to_instance(db)?.into_union()?,
)
}
};
Some(Type::TypeVar(TypeVarInstance::new(
Expand Down Expand Up @@ -7640,6 +7628,23 @@ impl<'db> UnionType<'db> {
.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<I, T>(db: &'db dyn Db, elements: I) -> Option<Type<'db>>
where
I: IntoIterator<Item = Option<T>>,
T: Into<Type<'db>>,
{
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 fn map(
Expand All @@ -7650,6 +7655,25 @@ impl<'db> UnionType<'db> {
Self::from_elements(db, self.elements(db).iter().map(transform_fn))
}

/// 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(
Copy link
Member

@MichaReiser MichaReiser Jun 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe: try_map and try_map_result or try_map and try_map_ok?

and try_from_elements and try_from_result_elements?

I also think it's important to document what "fallible" means because it wasn't clear to me if it bails if there's any None value or if it skip sover None values.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe: try_map and try_map_result or try_map and try_map_ok?

and try_from_elements and try_from_result_elements?

We could, but I don't think it's worth it right now considering that there's only one callsite that uses Results. We can add it later if that pattern becomes more common, I think.

I also think it's important to document what "fallible" means because it wasn't clear to me if it bails if there's any None value or if it skip sover None values.

👍 I'll push an update

self,
db: &'db dyn Db,
transform_fn: impl FnMut(&Type<'db>) -> Option<Type<'db>>,
) -> Option<Type<'db>> {
Self::try_from_elements(db, self.elements(db).iter().map(transform_fn))
}

pub(crate) fn to_instance(self, db: &'db dyn Db) -> Option<Type<'db>> {
self.try_map(db, |element| element.to_instance(db))
}

pub(crate) fn filter(
self,
db: &'db dyn Db,
Expand Down
46 changes: 18 additions & 28 deletions crates/ty_python_semantic/src/types/infer.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6770,34 +6770,24 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}

match (left_ty, right_ty, op) {
(Type::Union(lhs_union), rhs, _) => {
let mut union = UnionBuilder::new(self.db());
for lhs in lhs_union.elements(self.db()) {
let result = self.infer_binary_expression_type(
node,
emitted_division_by_zero_diagnostic,
*lhs,
rhs,
op,
)?;
union = union.add(result);
}
Some(union.build())
}
(lhs, Type::Union(rhs_union), _) => {
let mut union = UnionBuilder::new(self.db());
for rhs in rhs_union.elements(self.db()) {
let result = self.infer_binary_expression_type(
node,
emitted_division_by_zero_diagnostic,
lhs,
*rhs,
op,
)?;
union = union.add(result);
}
Some(union.build())
}
(Type::Union(lhs_union), rhs, _) => lhs_union.try_map(self.db(), |lhs_element| {
self.infer_binary_expression_type(
node,
emitted_division_by_zero_diagnostic,
*lhs_element,
rhs,
op,
)
}),
(lhs, Type::Union(rhs_union), _) => rhs_union.try_map(self.db(), |rhs_element| {
self.infer_binary_expression_type(
node,
emitted_division_by_zero_diagnostic,
lhs,
*rhs_element,
op,
)
}),

// Non-todo Anys take precedence over Todos (as if we fix this `Todo` in the future,
// the result would then become Any or Unknown, respectively).
Expand Down
20 changes: 8 additions & 12 deletions crates/ty_python_semantic/src/types/narrow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,13 +177,13 @@ impl ClassInfoConstraintFunction {
};

match classinfo {
Type::Tuple(tuple) => {
let mut builder = UnionBuilder::new(db);
for element in tuple.tuple(db).all_elements() {
builder = builder.add(self.generate_constraint(db, element)?);
}
Some(builder.build())
}
Type::Tuple(tuple) => UnionType::try_from_elements(
db,
tuple
.tuple(db)
.all_elements()
.map(|element| self.generate_constraint(db, element)),
),
Type::ClassLiteral(class_literal) => {
// At runtime (on Python 3.11+), this will return `True` for classes that actually
// do inherit `typing.Any` and `False` otherwise. We could accurately model that?
Expand Down Expand Up @@ -214,11 +214,7 @@ impl ClassInfoConstraintFunction {
}
}
Type::Union(union) => {
let mut builder = UnionBuilder::new(db);
for element in union.elements(db) {
builder = builder.add(self.generate_constraint(db, *element)?);
}
Some(builder.build())
union.try_map(db, |element| self.generate_constraint(db, *element))
}
Type::TypeVar(type_var) => match type_var.bound_or_constraints(db)? {
TypeVarBoundOrConstraints::UpperBound(bound) => self.generate_constraint(db, bound),
Expand Down
Loading