[ty] Treat Hashable, and similar protocols, equivalently to object for subtyping/assignability (#20284)

This commit is contained in:
Alex Waygood 2025-09-10 11:38:58 +01:00 committed by GitHub
parent 9cb37db510
commit 4de7d653bd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 154 additions and 35 deletions

View file

@ -1397,6 +1397,9 @@ impl<'db> Type<'db> {
(_, Type::NominalInstance(instance)) if instance.is_object(db) => {
C::always_satisfiable(db)
}
(_, Type::ProtocolInstance(target)) if target.is_equivalent_to_object(db) => {
C::always_satisfiable(db)
}
// `Never` is the bottom type, the empty set.
// It is a subtype of all other types.
@ -1610,9 +1613,10 @@ impl<'db> Type<'db> {
callable.has_relation_to_impl(db, target, relation, visitor)
}),
(Type::ProtocolInstance(left), Type::ProtocolInstance(right)) => {
left.has_relation_to_impl(db, right, relation, visitor)
}
(Type::ProtocolInstance(left), Type::ProtocolInstance(right)) => left
.interface(db)
.extends_interface_of(db, right.interface(db), relation, visitor),
// A protocol instance can never be a subtype of a nominal type, with the *sole* exception of `object`.
(Type::ProtocolInstance(_), _) => C::unsatisfiable(db),
(_, Type::ProtocolInstance(protocol)) => {

View file

@ -482,6 +482,43 @@ impl<'db> ProtocolInstanceType<'db> {
}
}
/// Return `true` if this protocol is a supertype of `object`.
///
/// This indicates that the protocol represents the same set of possible runtime objects
/// as `object` (since `object` is the universal set of *all* possible runtime objects!).
/// Such a protocol is therefore an equivalent type to `object`, which would in fact be
/// normalised to `object`.
pub(super) fn is_equivalent_to_object(self, db: &'db dyn Db) -> bool {
#[salsa::tracked(cycle_fn=recover, cycle_initial=initial, heap_size=ruff_memory_usage::heap_size)]
fn inner<'db>(db: &'db dyn Db, protocol: ProtocolInstanceType<'db>, _: ()) -> bool {
Type::object(db)
.satisfies_protocol(
db,
protocol,
TypeRelation::Subtyping,
&HasRelationToVisitor::new(ConstraintSet::always_satisfiable(db)),
)
.is_always_satisfied(db)
}
#[expect(clippy::trivially_copy_pass_by_ref)]
fn recover<'db>(
_db: &'db dyn Db,
_result: &bool,
_count: u32,
_value: ProtocolInstanceType<'db>,
_: (),
) -> salsa::CycleRecoveryAction<bool> {
salsa::CycleRecoveryAction::Iterate
}
fn initial<'db>(_db: &'db dyn Db, _value: ProtocolInstanceType<'db>, _: ()) -> bool {
true
}
inner(db, self, ())
}
/// Return a "normalized" version of this `Protocol` type.
///
/// See [`Type::normalized`] for more details.
@ -497,17 +534,8 @@ impl<'db> ProtocolInstanceType<'db> {
db: &'db dyn Db,
visitor: &NormalizedVisitor<'db>,
) -> Type<'db> {
let object = Type::object(db);
if object
.satisfies_protocol(
db,
self,
TypeRelation::Subtyping,
&HasRelationToVisitor::new(ConstraintSet::always_satisfiable(db)),
)
.is_always_satisfied(db)
{
return object;
if self.is_equivalent_to_object(db) {
return Type::object(db);
}
match self.inner {
Protocol::FromClass(_) => Type::ProtocolInstance(Self::synthesized(
@ -517,22 +545,6 @@ impl<'db> ProtocolInstanceType<'db> {
}
}
/// Return `true` if this protocol type has the given type relation to the protocol `other`.
///
/// TODO: consider the types of the members as well as their existence
pub(super) fn has_relation_to_impl<C: Constraints<'db>>(
self,
db: &'db dyn Db,
other: Self,
_relation: TypeRelation,
visitor: &HasRelationToVisitor<'db, C>,
) -> C {
other
.inner
.interface(db)
.is_sub_interface_of(db, self.inner.interface(db), visitor)
}
/// Return `true` if this protocol type is equivalent to the protocol `other`.
///
/// TODO: consider the types of the members as well as their existence

View file

@ -230,19 +230,21 @@ impl<'db> ProtocolInterface<'db> {
.unwrap_or_else(|| Type::object(db).member(db, name))
}
/// Return `true` if if all members on `self` are also members of `other`.
/// Return `true` if `self` extends the interface of `other`, i.e.,
/// all members on `other` are also members of `self`.
///
/// TODO: this method should consider the types of the members as well as their names.
pub(super) fn is_sub_interface_of<C: Constraints<'db>>(
pub(super) fn extends_interface_of<C: Constraints<'db>>(
self,
db: &'db dyn Db,
other: Self,
_relation: TypeRelation,
_visitor: &HasRelationToVisitor<'db, C>,
) -> C {
// TODO: This could just return a bool as written, but this form is what will be needed to
// combine the constraints when we do assignability checks on each member.
self.inner(db).keys().when_all(db, |member_name| {
C::from_bool(db, other.inner(db).contains_key(member_name))
other.inner(db).keys().when_all(db, |member_name| {
C::from_bool(db, self.inner(db).contains_key(member_name))
})
}