[ty] Use correct top type for default declared upper bound (#28325)

There is quite a lot of code that assumes that an unbounded typevar has
an implicit upper bound of `object`. But this is only true for regular
`TypeVar`s. `ParamSpec`s, for instance, have the top `paramspec_value`
callable as their implicit upper bound.

This patch adds a new `TypeVarDomain` type (also needed for #28249) that
describes what kind of values the typevar takes on in a specialization,
and a method that returns the correct top type for each domain.

Most callers should use `require_bound_or_constraints` to get the upper
bound or constraints of a typevar, without having to worry about whether
the upper bound is implicit or declared. It now returns the correct
implicit upper bound for the particular kind of typevar. There were many
places that were calling `bound_or_constraints`, trying to add a
fast-path for `object` where possible. But since this is only valid for
plain `TypeVar`s, I've updated most of those callers to just call
`require_bound_or_constraints` and not over-think the optimization.
(Most of the time there's a recursive call that will return quickly for
`object` anyway.)
This commit is contained in:
Douglas Creager authored and GitHub committed 2026-09-08 14:05:43 -04:00
1 parent 4cdd004a71
commit d38008443d
12 files changed
+127 -146

No files matched your search

+18 -28
View File
@@ -4308,12 +4308,11 @@ impl<'db> Type<'db> {
}
Type::TypeVar(bound_typevar) => {
match bound_typevar.typevar(db).bound_or_constraints(db, env) {
None => Type::object().instance_member(db, env, name),
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
match bound_typevar.require_bound_or_constraints(db, env) {
TypeVarBoundOrConstraints::UpperBound(bound) => {
bound.instance_member(db, env, name)
}
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => constraints
TypeVarBoundOrConstraints::Constraints(constraints) => constraints
.map_with_boundness_and_qualifiers(db, env, |constraint| {
constraint.instance_member(db, env, name)
}),
@@ -6280,20 +6279,17 @@ impl<'db> Type<'db> {
}
Type::TypeVar(bound_typevar) => {
match bound_typevar.typevar(db).bound_or_constraints(db, env) {
None => CallableBinding::not_callable(self).into(),
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
match bound_typevar.require_bound_or_constraints(db, env) {
TypeVarBoundOrConstraints::UpperBound(bound) => {
bound.bindings_impl(db, env, recursion_guard)
}
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
Bindings::from_union(
self,
constraints
.elements(db)
.iter()
.map(|ty| ty.bindings_impl(db, env, recursion_guard)),
)
}
TypeVarBoundOrConstraints::Constraints(constraints) => Bindings::from_union(
self,
constraints
.elements(db)
.iter()
.map(|ty| ty.bindings_impl(db, env, recursion_guard)),
),
}
}
@@ -6544,7 +6540,7 @@ impl<'db> Type<'db> {
),
SubclassOfInner::TypeVar(tvar) => {
let constructor_instance_type = Type::TypeVar(tvar);
let bindings = match tvar.typevar(db).require_bound_or_constraints(db, env) {
let bindings = match tvar.require_bound_or_constraints(db, env) {
TypeVarBoundOrConstraints::UpperBound(bound) => {
let constructor = bound.constructor_for_typevar_bound(db, env);
if let Type::ClassLiteral(class) = constructor
@@ -7702,18 +7698,12 @@ impl<'db> Type<'db> {
/// into generic types or other nested structures.
fn flatten_typevars(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> Type<'db> {
match self {
Type::TypeVar(tvar) => {
match tvar.typevar(db).bound_or_constraints(db, env) {
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
bound.flatten_typevars(db, env)
}
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
constraints.as_type(db, env).flatten_typevars(db, env)
}
// Unbounded typevar is effectively `object`.
None => Type::object(),
Type::TypeVar(tvar) => match tvar.require_bound_or_constraints(db, env) {
TypeVarBoundOrConstraints::UpperBound(bound) => bound.flatten_typevars(db, env),
TypeVarBoundOrConstraints::Constraints(constraints) => {
constraints.as_type(db, env).flatten_typevars(db, env)
}
}
},
Type::Union(union) => {
// Flatten each element and rebuild through the union builder.
UnionType::from_elements(
@@ -1021,14 +1021,11 @@ fn descriptor_setter_signature_domain<'db>(
return DescriptorSetterSignatureDomain::Deferred;
}
match typevar.typevar(db).bound_or_constraints(db, env) {
None => DescriptorSetterSignatureDomain::Known(Type::object()),
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
match typevar.require_bound_or_constraints(db, env) {
TypeVarBoundOrConstraints::UpperBound(bound) => {
DescriptorSetterSignatureDomain::Known(bound.bind_self_typevars(db, env, descriptor_ty))
}
Some(TypeVarBoundOrConstraints::Constraints(_)) => {
DescriptorSetterSignatureDomain::Deferred
}
TypeVarBoundOrConstraints::Constraints(_) => DescriptorSetterSignatureDomain::Deferred,
}
}
@@ -31,13 +31,17 @@ pub(crate) enum TypeVarOwnerContext<'db> {
}
impl<'db> TypeVarOwnerContext<'db> {
fn typevar(self, db: &'db dyn Db) -> TypeVarInstance<'db> {
fn bound_typevar(self) -> BoundTypeVarInstance<'db> {
match self {
TypeVarOwnerContext::Bare(bound_typevar)
| TypeVarOwnerContext::SubclassOf(bound_typevar) => bound_typevar.typevar(db),
| TypeVarOwnerContext::SubclassOf(bound_typevar) => bound_typevar,
}
}
fn typevar(self, db: &'db dyn Db) -> TypeVarInstance<'db> {
self.bound_typevar().typevar(db)
}
fn has_implicit_upper_bound(self, db: &'db dyn Db, env: &ProgramEnvironment<'db>) -> bool {
self.typevar(db).bound_or_constraints(db, env).is_none()
}
@@ -52,14 +56,12 @@ impl<'db> TypeVarOwnerContext<'db> {
) -> Type<'db> {
match self {
TypeVarOwnerContext::Bare(typevar) => typevar
.typevar(db)
.require_bound_or_constraints(db, env)
.as_type(db, env),
TypeVarOwnerContext::SubclassOf(typevar) => SubclassOfType::try_from_instance(
db,
env,
typevar
.typevar(db)
.require_bound_or_constraints(db, env)
.as_type(db, env),
)
@@ -208,11 +210,13 @@ impl<'db> BoundSuperError<'db> {
let type_var = type_var_context.typevar(db);
match type_var_context.typevar(db).bound_or_constraints(db, env) {
None => {
let top = type_var_context.bound_typevar().domain(db).top(db);
diagnostic.info(format_args!(
"Type variable `{}` has `object` as its implicit upper bound",
"Type variable `{}` has `{}` as its implicit upper bound",
type_var.name(db),
top.display(db, env),
));
Type::object()
top
}
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
diagnostic.info(format_args!(
@@ -669,9 +673,8 @@ impl<'db> BoundSuperType<'db> {
SubclassOfInner::Protocol(_) => SuperOwnerKind::Dynamic(DynamicType::Unknown),
SubclassOfInner::Dynamic(dynamic) => SuperOwnerKind::Dynamic(dynamic),
SubclassOfInner::TypeVar(bound_typevar) => {
let typevar = bound_typevar.typevar(db);
match typevar.bound_or_constraints(db, env) {
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
match bound_typevar.require_bound_or_constraints(db, env) {
TypeVarBoundOrConstraints::UpperBound(bound) => {
let class = match bound {
Type::NominalInstance(instance) => Some(instance.class(db, env)),
Type::ProtocolInstance(protocol) => {
@@ -698,24 +701,12 @@ impl<'db> BoundSuperType<'db> {
);
}
}
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
TypeVarBoundOrConstraints::Constraints(constraints) => {
return build_constrained_union(
constraints,
TypeVarOwnerContext::SubclassOf(bound_typevar),
);
}
None => {
// No bound means the implicit upper bound is `object`.
SuperOwnerKind::Resolved(Self::resolve_class_super_owner(
db,
pivot_class,
pivot_class_type,
owner_type,
owner_type,
ClassType::object(db, env),
Some(TypeVarOwnerContext::SubclassOf(bound_typevar)),
)?)
}
}
}
},
@@ -788,9 +779,8 @@ impl<'db> BoundSuperType<'db> {
return delegate_to(alias.value_type(db));
}
Type::TypeVar(bound_typevar) => {
let typevar = bound_typevar.typevar(db);
match typevar.bound_or_constraints(db, env) {
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
match bound_typevar.require_bound_or_constraints(db, env) {
TypeVarBoundOrConstraints::UpperBound(bound) => {
let class = match bound {
Type::NominalInstance(instance) => Some(instance.class(db, env)),
Type::ProtocolInstance(protocol) => {
@@ -814,23 +804,12 @@ impl<'db> BoundSuperType<'db> {
);
}
}
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
TypeVarBoundOrConstraints::Constraints(constraints) => {
return build_constrained_union(
constraints,
TypeVarOwnerContext::Bare(bound_typevar),
);
}
None => {
// No bound means the implicit upper bound is `object`.
SuperOwnerKind::Resolved(Self::resolve_instance_super_owner(
db,
pivot_class,
pivot_class_type,
owner_type,
ClassType::object(db, env),
Some(TypeVarOwnerContext::Bare(bound_typevar)),
)?)
}
}
}
Type::TypeIs(_) | Type::TypeGuard(_) => {
@@ -175,7 +175,7 @@ impl<'db> Type<'db> {
}
}),
SubclassOfInner::TypeVar(tvar) => {
match tvar.typevar(db).require_bound_or_constraints(db, env) {
match tvar.require_bound_or_constraints(db, env) {
TypeVarBoundOrConstraints::UpperBound(bound) => {
let upcast_callables = bound
.constructor_for_typevar_bound(db, env)
@@ -4463,10 +4463,7 @@ impl<'db> PathBounds<'db> {
let bound_typevar = path_bound.bound_typevar;
let lower = path_bound.effective_lower(db, env);
match bound_typevar
.typevar(db)
.require_bound_or_constraints(db, env)
{
match bound_typevar.require_bound_or_constraints(db, env) {
TypeVarBoundOrConstraints::UpperBound(bound) => {
let declared_upper = bound.top_materialization(db, env);
+16 -23
View File
@@ -1982,16 +1982,13 @@ fn is_instance_truthiness<'db>(
if is_instance_truthiness(db, env, positive, class).is_always_true() {
return Truthiness::AlwaysTrue;
} else if let Type::TypeVar(tvar) = positive {
match tvar.typevar(db).bound_or_constraints(db, env) {
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
match tvar.require_bound_or_constraints(db, env) {
TypeVarBoundOrConstraints::UpperBound(bound) => {
effective.add_positive_in_place(bound);
}
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
TypeVarBoundOrConstraints::Constraints(constraints) => {
effective.add_positive_in_place(constraints.as_type(db, env));
}
// A typevar without bounds/constraints has `object` as its implicit upper bound,
// and adding `object` to an intersection is a no-op
None => {}
}
found_tvars_or_newtypes = true;
} else if let Type::NewTypeInstance(newtype) = positive {
@@ -2046,20 +2043,17 @@ fn is_instance_truthiness<'db>(
Type::TypeAlias(alias) => is_instance_truthiness(db, env, alias.value_type(db), class),
Type::TypeVar(bound_typevar) => {
match bound_typevar.typevar(db).bound_or_constraints(db, env) {
None => is_instance_truthiness(db, env, Type::object(), class),
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
is_instance_truthiness(db, env, bound, class)
}
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => always_true_if(
constraints
.elements(db)
.iter()
.all(|c| is_instance_truthiness(db, env, *c, class).is_always_true()),
),
Type::TypeVar(bound_typevar) => match bound_typevar.require_bound_or_constraints(db, env) {
TypeVarBoundOrConstraints::UpperBound(bound) => {
is_instance_truthiness(db, env, bound, class)
}
}
TypeVarBoundOrConstraints::Constraints(constraints) => always_true_if(
constraints
.elements(db)
.iter()
.all(|c| is_instance_truthiness(db, env, *c, class).is_always_true()),
),
},
Type::BoundMethod(..)
| Type::KnownBoundMethod(..)
@@ -2137,16 +2131,15 @@ fn is_instance_tuple_covers<'db>(
.positive(db)
.iter()
.any(|element| is_instance_tuple_covers(db, env, tuple, *element, recursion_guard)),
Type::TypeVar(typevar) => match typevar.typevar(db).bound_or_constraints(db, env) {
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
Type::TypeVar(typevar) => match typevar.require_bound_or_constraints(db, env) {
TypeVarBoundOrConstraints::UpperBound(bound) => {
is_instance_tuple_covers(db, env, tuple, bound, recursion_guard)
}
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
TypeVarBoundOrConstraints::Constraints(constraints) => {
constraints.elements(db).iter().all(|constraint| {
is_instance_tuple_covers(db, env, tuple, *constraint, recursion_guard)
})
}
None => is_instance_tuple_covers(db, env, tuple, Type::object(), recursion_guard),
},
ty => tuple.fixed_elements().any(|element| {
let Type::ClassLiteral(class) = element else {
@@ -70,12 +70,8 @@ impl<'db> Type<'db> {
Type::FunctionLiteral(FunctionType::new(db, function.literal(db), None))
}
Type::TypeVar(typevar) => visitor.visit_type(db, ty, || {
match typevar.typevar(db).bound_or_constraints(db, env) {
Some(bound_or_constraints) => {
upcast(db, env, bound_or_constraints.as_type(db, env), visitor)
}
None => KnownClass::Object.to_instance(db, env),
}
let bound_or_constraints = typevar.require_bound_or_constraints(db, env);
upcast(db, env, bound_or_constraints.as_type(db, env), visitor)
}),
Type::Union(union) => {
union.map(db, env, |element| upcast(db, env, *element, visitor))
@@ -335,14 +335,11 @@ impl<'db> AllMembers<'db> {
}
Type::TypeVar(bound_typevar) => {
match bound_typevar.typevar(db).bound_or_constraints(db, env) {
None => {
self.extend_with_type(db, env, Type::object());
}
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
match bound_typevar.require_bound_or_constraints(db, env) {
TypeVarBoundOrConstraints::UpperBound(bound) => {
self.extend_with_type(db, env, bound);
}
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
TypeVarBoundOrConstraints::Constraints(constraints) => {
self.members.extend(
constraints
.elements(db)
@@ -1267,19 +1267,14 @@ fn expand_intersection_typevars_and_newtypes<'db>(
let mut builder = IntersectionBuilder::new(db, env);
for &element in positive {
match element {
Type::TypeVar(tvar) => {
match tvar.typevar(db).bound_or_constraints(db, env) {
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
builder.add_positive_in_place(bound);
}
Some(TypeVarBoundOrConstraints::Constraints(constraints)) => {
builder.add_positive_in_place(constraints.as_type(db, env));
}
// Type variables without bounds or constraints implicitly have `object`
// as their upper bound, and adding `object` to an intersection is always a no-op
None => {}
Type::TypeVar(tvar) => match tvar.require_bound_or_constraints(db, env) {
TypeVarBoundOrConstraints::UpperBound(bound) => {
builder.add_positive_in_place(bound);
}
}
TypeVarBoundOrConstraints::Constraints(constraints) => {
builder.add_positive_in_place(constraints.as_type(db, env));
}
},
Type::NewTypeInstance(newtype) => {
builder.add_positive_in_place(newtype.concrete_base_type(db));
}
@@ -2934,6 +2934,12 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> {
}
};
// The top signature is supertype of (and assignable from) all other signatures. It is a
// subtype of no signature except itself, and assignable only to the gradual signature.
if target_parameters.is_top() {
return result;
}
if self.typevar_evaluation == TypeVarEvaluation::Lazy {
let source_paramspec = source_parameters.as_paramspec_with_prefix();
let target_paramspec = target_parameters.as_paramspec_with_prefix();
@@ -3521,11 +3527,7 @@ impl<'c, 'db> TypeRelationChecker<'_, 'c, 'db> {
return result;
}
// The top signature is supertype of (and assignable from) all other signatures. It is a
// subtype of no signature except itself, and assignable only to the gradual signature.
if target_parameters.is_top() {
return result;
} else if source_parameters.is_top() && !target_parameters.is_gradual() {
if source_parameters.is_top() && !target_parameters.is_gradual() {
if let Some(context) = self.report_context() {
context.push(ErrorContext::TopCallableAssignedToNonTop {
return_type: source.return_ty,
@@ -539,16 +539,13 @@ impl<'db> SubclassOfInner<'db> {
Self::Dynamic(_) | Self::Protocol(_) => None,
Self::Class(class) => Some(class),
Self::TypeVar(bound_typevar) => {
match bound_typevar.typevar(db).bound_or_constraints(db, env) {
None => Some(ClassType::object(db, env)),
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
match bound_typevar.require_bound_or_constraints(db, env) {
TypeVarBoundOrConstraints::UpperBound(bound) => {
Self::try_from_instance(db, env, bound)
.and_then(|subclass_of| subclass_of.into_class(db, env))
}
// TODO this is quite imprecise
Some(TypeVarBoundOrConstraints::Constraints(_)) => {
Some(ClassType::object(db, env))
}
TypeVarBoundOrConstraints::Constraints(_) => Some(ClassType::object(db, env)),
}
}
}
@@ -625,7 +622,7 @@ impl<'db> SubclassOfInner<'db> {
let bound_typevar = bound_typevar.map_bound_or_constraints(db, |bound_or_constraints| {
Some(match bound_or_constraints {
None => TypeVarBoundOrConstraints::UpperBound(
SubclassOfType::try_from_instance(db, env, Type::object())
SubclassOfType::try_from_instance(db, env, bound_typevar.domain(db).top(db))
.unwrap_or(SubclassOfType::subclass_of_unknown()),
),
Some(TypeVarBoundOrConstraints::UpperBound(bound)) => {
+49 -11
View File
@@ -355,17 +355,6 @@ impl<'db> TypeVarInstance<'db> {
})
}
/// Returns the bounds or constraints of this typevar. If the typevar is unbounded, returns
/// `object` as its upper bound.
pub(crate) fn require_bound_or_constraints(
self,
db: &'db dyn Db,
env: &ProgramEnvironment<'db>,
) -> TypeVarBoundOrConstraints<'db> {
self.bound_or_constraints(db, env)
.unwrap_or_else(|| TypeVarBoundOrConstraints::UpperBound(Type::object()))
}
pub(crate) fn default_type(
self,
db: &'db dyn Db,
@@ -1069,6 +1058,18 @@ impl<'db> BoundTypeVarInstance<'db> {
self.identity(db).kind(db)
}
pub(crate) fn domain(self, db: &'db dyn Db) -> TypeVarDomain {
let identity = self.identity(db);
let kind = identity.kind(db);
if kind.is_paramspec() && identity.paramspec_attr.is_none() {
TypeVarDomain::ParameterSignature
} else if kind.is_typevartuple() {
TypeVarDomain::TypeTuple
} else {
TypeVarDomain::Type
}
}
pub(crate) fn is_paramspec(self, db: &'db dyn Db) -> bool {
self.kind(db).is_paramspec()
}
@@ -1077,6 +1078,18 @@ impl<'db> BoundTypeVarInstance<'db> {
self.kind(db).is_typevartuple()
}
/// Returns the bounds or constraints of this typevar. If the typevar is unbounded, returns
/// `object` as its upper bound.
pub(crate) fn require_bound_or_constraints(
self,
db: &'db dyn Db,
env: &ProgramEnvironment<'db>,
) -> TypeVarBoundOrConstraints<'db> {
self.typevar(db)
.bound_or_constraints(db, env)
.unwrap_or_else(|| TypeVarBoundOrConstraints::UpperBound(self.domain(db).top(db)))
}
/// Returns a new bound typevar instance with the given `ParamSpec` attribute set.
///
/// This method will also set an appropriate upper bound on the typevar, based on the
@@ -1555,6 +1568,31 @@ impl<'db> BoundTypeVarInstance<'db> {
}
}
/// The kind of element that can be assigned to a typevar.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, get_size2::GetSize)]
pub(crate) enum TypeVarDomain {
/// "Plain" typevars and `ParamSpec` components (e.g. `P.args` and `P.kwargs`) are mapped to
/// types
Type,
/// `ParamSpec`s are mapped to parameter signatures
ParameterSignature,
/// `TypeVarTuple`s are mapped to type tuples
TypeTuple,
}
impl TypeVarDomain {
pub(crate) fn top(self, db: &dyn Db) -> Type<'_> {
match self {
TypeVarDomain::Type => Type::object(),
TypeVarDomain::ParameterSignature => {
Type::paramspec_value_callable(db, Parameters::top())
}
// TODO: Choose the correct top type once we support TypeVarTuple in constraint sets
TypeVarDomain::TypeTuple => Type::object(),
}
}
}
/// Whether this typevar was created via the legacy `TypeVar` constructor, using PEP 695 syntax,
/// or an implicit typevar like `Self` was used.
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, get_size2::GetSize)]