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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
class C[T: (A, B)]:
def f(foo: T):
try:
pass
except foo:
pass
185 changes: 153 additions & 32 deletions crates/ty_python_semantic/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,7 @@ pub(crate) use crate::types::narrow::infer_narrowing_constraint;
use crate::types::newtype::NewType;
pub(crate) use crate::types::signatures::{Parameter, Parameters};
use crate::types::signatures::{ParameterForm, walk_signature};
use crate::types::tuple::{TupleSpec, TupleSpecBuilder};
use crate::types::tuple::{Tuple, TupleSpec, TupleSpecBuilder};
pub(crate) use crate::types::typed_dict::{TypedDictParams, TypedDictType, walk_typed_dict_type};
pub use crate::types::variance::TypeVarVariance;
use crate::types::variance::VarianceInferable;
Expand Down Expand Up @@ -5389,9 +5389,9 @@ impl<'db> Type<'db> {
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
bound.try_bool_impl(db, allow_short_circuit, visitor)?
}
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
try_union(constraints)?
}
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints
.as_type(db)
.try_bool_impl(db, allow_short_circuit, visitor)?,
}
}

Expand Down Expand Up @@ -6427,7 +6427,7 @@ impl<'db> Type<'db> {
TypeVarBoundOrConstraints::UpperBound(bound) => {
non_async_special_case(db, bound)
}
TypeVarBoundOrConstraints::Constraints(union) => non_async_special_case(db, Type::Union(union)),
TypeVarBoundOrConstraints::Constraints(constraints) => non_async_special_case(db, constraints.as_type(db)),
},
Type::Union(union) => {
let elements = union.elements(db);
Expand Down Expand Up @@ -9441,7 +9441,7 @@ impl<'db> TypeVarInstance<'db> {
TypeVarBoundOrConstraints::UpperBound(upper_bound.to_instance(db)?)
}
TypeVarBoundOrConstraints::Constraints(constraints) => {
TypeVarBoundOrConstraints::Constraints(constraints.to_instance(db)?.as_union()?)
TypeVarBoundOrConstraints::Constraints(constraints.to_instance(db)?)
}
};
let identity = TypeVarIdentity::new(
Expand Down Expand Up @@ -9490,22 +9490,30 @@ impl<'db> TypeVarInstance<'db> {
fn lazy_constraints(self, db: &'db dyn Db) -> Option<TypeVarBoundOrConstraints<'db>> {
let definition = self.definition(db)?;
let module = parsed_module(db, definition.file(db)).load(db);
let ty = match definition.kind(db) {
let constraints = match definition.kind(db) {
// PEP 695 typevar
DefinitionKind::TypeVar(typevar) => {
let typevar_node = typevar.node(&module);
definition_expression_type(db, definition, typevar_node.bound.as_ref()?)
.as_union()?
let bound =
definition_expression_type(db, definition, typevar_node.bound.as_ref()?);
let constraints = if let Some(tuple) = bound
.as_nominal_instance()
.and_then(|instance| instance.tuple_spec(db))
{
if let Tuple::Fixed(tuple) = tuple.into_owned() {
tuple.owned_elements()
} else {
vec![Type::unknown()].into_boxed_slice()
}
} else {
vec![Type::unknown()].into_boxed_slice()
};
TypeVarConstraints::new(db, constraints)
}
// legacy typevar
DefinitionKind::Assignment(assignment) => {
let call_expr = assignment.value(&module).as_call_expr()?;
// We don't use `UnionType::from_elements` or `UnionBuilder` here,
// because we don't want to simplify the list of constraints as we would with
// an actual union type.
// TODO: We probably shouldn't use `UnionType` to store these at all? TypeVar
// constraints are not a union.
UnionType::new(
TypeVarConstraints::new(
db,
call_expr
.arguments
Expand All @@ -9518,7 +9526,7 @@ impl<'db> TypeVarInstance<'db> {
}
_ => return None,
};
Some(TypeVarBoundOrConstraints::Constraints(ty))
Some(TypeVarBoundOrConstraints::Constraints(constraints))
}

#[salsa::tracked(cycle_fn=lazy_default_cycle_recover, cycle_initial=lazy_default_cycle_initial, heap_size=ruff_memory_usage::heap_size)]
Expand Down Expand Up @@ -9908,10 +9916,133 @@ impl<'db> From<TypeVarBoundOrConstraints<'db>> for TypeVarBoundOrConstraintsEval
}
}

/// Type variable constraints (e.g. `T: (int, str)`).
/// This is structurally identical to [`UnionType`], except that it does not perform simplification and preserves the element types.
#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)]
pub struct TypeVarConstraints<'db> {
#[returns(ref)]
elements: Box<[Type<'db>]>,
}

impl get_size2::GetSize for TypeVarConstraints<'_> {}

fn walk_type_var_constraints<'db, V: visitor::TypeVisitor<'db> + ?Sized>(
db: &'db dyn Db,
constraints: TypeVarConstraints<'db>,
visitor: &V,
) {
for ty in constraints.elements(db) {
visitor.visit_type(db, *ty);
}
}

impl<'db> TypeVarConstraints<'db> {
fn as_type(self, db: &'db dyn Db) -> Type<'db> {
let mut builder = UnionBuilder::new(db);
for ty in self.elements(db) {
builder = builder.add(*ty);
}
builder.build()
}

fn to_instance(self, db: &'db dyn Db) -> Option<TypeVarConstraints<'db>> {
let mut instance_elements = Vec::new();
for ty in self.elements(db) {
instance_elements.push(ty.to_instance(db)?);
}
Some(TypeVarConstraints::new(
db,
instance_elements.into_boxed_slice(),
))
}

fn map(self, db: &'db dyn Db, transform_fn: impl FnMut(&Type<'db>) -> Type<'db>) -> Self {
let mapped = self
.elements(db)
.iter()
.map(transform_fn)
.collect::<Box<_>>();
TypeVarConstraints::new(db, mapped)
}

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(ty_member, member_origin, 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(
builder.build(),
origin,
if possibly_unbound {
Definedness::PossiblyUndefined
} else {
Definedness::AlwaysDefined
},
)
},
qualifiers,
}
}

fn normalized_impl(self, db: &'db dyn Db, visitor: &NormalizedVisitor<'db>) -> Self {
let normalized = self
.elements(db)
.iter()
.map(|ty| ty.normalized_impl(db, visitor))
.collect::<Box<_>>();
TypeVarConstraints::new(db, normalized)
}

fn materialize_impl(
self,
db: &'db dyn Db,
materialization_kind: MaterializationKind,
visitor: &ApplyTypeMappingVisitor<'db>,
) -> Self {
let materialized = self
.elements(db)
.iter()
.map(|ty| ty.materialize(db, materialization_kind, visitor))
.collect::<Box<_>>();
TypeVarConstraints::new(db, materialized)
}
}

#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update, get_size2::GetSize)]
pub enum TypeVarBoundOrConstraints<'db> {
UpperBound(Type<'db>),
Constraints(UnionType<'db>),
Constraints(TypeVarConstraints<'db>),
}

fn walk_type_var_bounds<'db, V: visitor::TypeVisitor<'db> + ?Sized>(
Expand All @@ -9922,7 +10053,7 @@ fn walk_type_var_bounds<'db, V: visitor::TypeVisitor<'db> + ?Sized>(
match bounds {
TypeVarBoundOrConstraints::UpperBound(bound) => visitor.visit_type(db, bound),
TypeVarBoundOrConstraints::Constraints(constraints) => {
visitor.visit_union_type(db, constraints);
walk_type_var_constraints(db, constraints, visitor);
}
}
}
Expand All @@ -9937,14 +10068,7 @@ impl<'db> TypeVarBoundOrConstraints<'db> {
// Constraints are a non-normalized union by design (it's not really a union at
// all, we are just using a union to store the types). Normalize the types but not
// the containing union.
TypeVarBoundOrConstraints::Constraints(UnionType::new(
db,
constraints
.elements(db)
.iter()
.map(|ty| ty.normalized_impl(db, visitor))
.collect::<Box<_>>(),
))
TypeVarBoundOrConstraints::Constraints(constraints.normalized_impl(db, visitor))
}
}
}
Expand All @@ -9960,13 +10084,10 @@ impl<'db> TypeVarBoundOrConstraints<'db> {
bound.materialize(db, materialization_kind, visitor),
),
TypeVarBoundOrConstraints::Constraints(constraints) => {
TypeVarBoundOrConstraints::Constraints(UnionType::new(
TypeVarBoundOrConstraints::Constraints(constraints.materialize_impl(
db,
constraints
.elements(db)
.iter()
.map(|ty| ty.materialize(db, materialization_kind, visitor))
.collect::<Box<_>>(),
materialization_kind,
visitor,
))
}
}
Expand Down
4 changes: 2 additions & 2 deletions crates/ty_python_semantic/src/types/bound_super.rs
Original file line number Diff line number Diff line change
Expand Up @@ -157,7 +157,7 @@ impl<'db> BoundSuperError<'db> {
.map(|c| c.display(db))
.join(", ")
));
Type::Union(constraints)
constraints.as_type(db)
}
None => {
diagnostic.info(format_args!(
Expand Down Expand Up @@ -375,7 +375,7 @@ impl<'db> BoundSuperType<'db> {
delegate_with_error_mapped(bound, Some(type_var))
}
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
delegate_with_error_mapped(Type::Union(constraints), Some(type_var))
delegate_with_error_mapped(constraints.as_type(db), Some(type_var))
}
None => delegate_with_error_mapped(Type::object(), Some(type_var)),
};
Expand Down
2 changes: 1 addition & 1 deletion crates/ty_python_semantic/src/types/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1184,7 +1184,7 @@ impl<'db> InnerIntersectionBuilder<'db> {
speculative = speculative.add_positive(bound);
}
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
speculative = speculative.add_positive(Type::Union(constraints));
speculative = speculative.add_positive(constraints.as_type(db));
}
// TypeVars without a bound or constraint implicitly have `object` as their
// upper bound, and it is always a no-op to add `object` to an intersection.
Expand Down
14 changes: 5 additions & 9 deletions crates/ty_python_semantic/src/types/infer/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3259,18 +3259,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
std::mem::replace(&mut self.deferred_state, DeferredExpressionState::Deferred);
match bound.as_deref() {
Some(expr @ ast::Expr::Tuple(ast::ExprTuple { elts, .. })) => {
// We don't use UnionType::from_elements or UnionBuilder here, because we don't
// want to simplify the list of constraints like we do with the elements of an
// actual union type.
// TODO: Consider using a new `OneOfType` connective here instead, since that
// more accurately represents the actual semantics of typevar constraints.
let ty = Type::Union(UnionType::new(
// Here, we interpret `bound` as a heterogeneous tuple and convert it to `TypeVarConstraints` in `TypeVarInstance::lazy_constraints`.
let tuple_ty = Type::heterogeneous_tuple(
self.db(),
elts.iter()
.map(|expr| self.infer_type_expression(expr))
.collect::<Box<[_]>>(),
));
self.store_expression_type(expr, ty);
);
self.store_expression_type(expr, tuple_ty);
}
Some(expr) => {
self.infer_type_expression(expr);
Expand Down Expand Up @@ -11298,7 +11294,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
if provided_type
.when_assignable_to(
db,
Type::Union(constraints),
constraints.as_type(db),
InferableTypeVars::None,
)
.is_never_satisfied(db)
Expand Down
2 changes: 1 addition & 1 deletion crates/ty_python_semantic/src/types/narrow.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ impl ClassInfoConstraintFunction {
self.generate_constraint(db, bound)
}
TypeVarBoundOrConstraints::Constraints(constraints) => {
self.generate_constraint(db, Type::Union(constraints))
self.generate_constraint(db, constraints.as_type(db))
}
}
}
Expand Down
22 changes: 9 additions & 13 deletions crates/ty_python_semantic/src/types/subclass_of.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use crate::types::{
ApplyTypeMappingVisitor, BoundTypeVarInstance, ClassType, DynamicType,
FindLegacyTypeVarsVisitor, HasRelationToVisitor, IsDisjointVisitor, KnownClass,
MaterializationKind, MemberLookupPolicy, NormalizedVisitor, SpecialFormType, Type, TypeContext,
TypeMapping, TypeRelation, TypeVarBoundOrConstraints, UnionType, todo_type,
TypeMapping, TypeRelation, TypeVarBoundOrConstraints, todo_type,
};
use crate::{Db, FxOrderSet};

Expand Down Expand Up @@ -190,7 +190,9 @@ impl<'db> SubclassOfType<'db> {
match bound_typevar.typevar(db).bound_or_constraints(db) {
None => unreachable!(),
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => bound,
Some(TypeVarBoundOrConstraints::Constraints(union)) => Type::Union(union),
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
constraints.as_type(db)
}
}
}
};
Expand Down Expand Up @@ -352,7 +354,7 @@ impl<'db> SubclassOfInner<'db> {
.and_then(|subclass_of| subclass_of.into_class(db))
}
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
match constraints.elements(db) {
match &**constraints.elements(db) {
[bound] => Self::try_from_instance(db, *bound)
.and_then(|subclass_of| subclass_of.into_class(db)),
_ => Some(ClassType::object(db)),
Expand Down Expand Up @@ -417,16 +419,10 @@ impl<'db> SubclassOfInner<'db> {
)
}
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
let constraints = constraints
.elements(db)
.iter()
.map(|constraint| {
SubclassOfType::try_from_instance(db, *constraint)
.unwrap_or(SubclassOfType::subclass_of_unknown())
})
.collect::<Box<_>>();

TypeVarBoundOrConstraints::Constraints(UnionType::new(db, constraints))
TypeVarBoundOrConstraints::Constraints(constraints.map(db, |constraint| {
SubclassOfType::try_from_instance(db, *constraint)
.unwrap_or(SubclassOfType::subclass_of_unknown())
}))
}
})
});
Expand Down
Loading
Loading