diff --git a/crates/red_knot_python_semantic/src/types.rs b/crates/red_knot_python_semantic/src/types.rs index 3204501846..a1568f79ac 100644 --- a/crates/red_knot_python_semantic/src/types.rs +++ b/crates/red_knot_python_semantic/src/types.rs @@ -1,3 +1,4 @@ +use instance::{Protocol, ProtocolInstanceType}; use itertools::Either; use std::slice::Iter; @@ -476,6 +477,8 @@ pub enum Type<'db> { /// The set of Python objects with the given class in their __class__'s method resolution order. /// Construct this variant using the `Type::instance` constructor function. NominalInstance(NominalInstanceType<'db>), + /// The set of Python objects that conform to the interface described by a given protocol. + ProtocolInstance(ProtocolInstanceType<'db>), /// A single Python object that requires special treatment in the type system KnownInstance(KnownInstanceType<'db>), /// An instance of `builtins.property` @@ -543,17 +546,17 @@ impl<'db> Type<'db> { } fn is_none(&self, db: &'db dyn Db) -> bool { - self.into_instance() + self.into_nominal_instance() .is_some_and(|instance| instance.class().is_known(db, KnownClass::NoneType)) } fn is_bool(&self, db: &'db dyn Db) -> bool { - self.into_instance() + self.into_nominal_instance() .is_some_and(|instance| instance.class().is_known(db, KnownClass::Bool)) } pub fn is_notimplemented(&self, db: &'db dyn Db) -> bool { - self.into_instance().is_some_and(|instance| { + self.into_nominal_instance().is_some_and(|instance| { instance .class() .is_known(db, KnownClass::NotImplementedType) @@ -561,7 +564,7 @@ impl<'db> Type<'db> { } pub fn is_object(&self, db: &'db dyn Db) -> bool { - self.into_instance() + self.into_nominal_instance() .is_some_and(|instance| instance.class().is_object(db)) } @@ -667,6 +670,8 @@ impl<'db> Type<'db> { .iter() .any(|ty| ty.contains_todo(db)) } + + Self::ProtocolInstance(protocol) => protocol.contains_todo(), } } @@ -887,6 +892,7 @@ impl<'db> Type<'db> { Type::Intersection(intersection) => Type::Intersection(intersection.normalized(db)), Type::Tuple(tuple) => Type::Tuple(tuple.normalized(db)), Type::Callable(callable) => Type::Callable(callable.normalized(db)), + Type::ProtocolInstance(protocol) => protocol.normalized(db), Type::LiteralString | Type::NominalInstance(_) | Type::PropertyInstance(_) @@ -1150,6 +1156,14 @@ impl<'db> Type<'db> { false } + (Type::ProtocolInstance(left), Type::ProtocolInstance(right)) => { + left.is_subtype_of(db, right) + } + // A protocol instance can never be a subtype of a nominal type, with the *sole* exception of `object`. + // TODO: `Callable` types are also structural types. + (Type::ProtocolInstance(_), _) => false, + (_, Type::ProtocolInstance(protocol)) => self.satisfies_protocol(db, protocol), + // A fully static heterogeneous tuple type `A` is a subtype of a fully static heterogeneous tuple type `B` // iff the two tuple types have the same number of elements and each element-type in `A` is a subtype // of the element-type at the same index in `B`. (Now say that 5 times fast.) @@ -1499,6 +1513,16 @@ impl<'db> Type<'db> { .into_callable_type(db) .is_assignable_to(db, target), + (Type::ProtocolInstance(left), Type::ProtocolInstance(right)) => { + left.is_assignable_to(db, right) + } + // Other than the dynamic types such as `Any`/`Unknown`/`Todo` handled above, + // a protocol instance can never be assignable to a nominal type, + // with the *sole* exception of `object`. + // TODO: `Callable` types are also structural types. + (Type::ProtocolInstance(_), _) => false, + (_, Type::ProtocolInstance(protocol)) => self.satisfies_protocol(db, protocol), + // TODO other types containing gradual forms _ => self.is_subtype_of(db, target), } @@ -1522,6 +1546,13 @@ impl<'db> Type<'db> { (Type::NominalInstance(left), Type::NominalInstance(right)) => { left.is_equivalent_to(db, right) } + (Type::ProtocolInstance(first), Type::ProtocolInstance(right)) => { + first.is_equivalent_to(db, right) + } + (Type::ProtocolInstance(protocol), nominal @ Type::NominalInstance(n)) + | (nominal @ Type::NominalInstance(n), Type::ProtocolInstance(protocol)) => { + n.class().is_object(db) && protocol.normalized(db) == nominal + } _ => self == other && self.is_fully_static(db) && other.is_fully_static(db), } } @@ -1572,10 +1603,24 @@ impl<'db> Type<'db> { first.is_gradual_equivalent_to(db, second) } + (Type::ProtocolInstance(first), Type::ProtocolInstance(right)) => { + first.is_gradual_equivalent_to(db, right) + } + (Type::ProtocolInstance(protocol), nominal @ Type::NominalInstance(n)) + | (nominal @ Type::NominalInstance(n), Type::ProtocolInstance(protocol)) => { + n.class().is_object(db) && protocol.normalized(db) == nominal + } _ => false, } } + fn satisfies_protocol(self, db: &'db dyn Db, protocol: ProtocolInstanceType<'db>) -> bool { + protocol + .protocol_members(db) + .iter() + .all(|member| !self.member(db, member).symbol.is_unbound()) + } + /// Return true if this type and `other` have no common elements. /// /// Note: This function aims to have no false positives, but might return @@ -1772,6 +1817,68 @@ impl<'db> Type<'db> { ty.bool(db).is_always_true() } + (Type::ProtocolInstance(left), Type::ProtocolInstance(right)) => { + left.is_disjoint_from(db, right) + } + + // TODO: we could also consider `protocol` to be disjoint from `nominal` if `nominal` + // has the right member but the type of its member is disjoint from the type of the + // member on `protocol`. + (Type::ProtocolInstance(protocol), nominal @ Type::NominalInstance(n)) + | (nominal @ Type::NominalInstance(n), Type::ProtocolInstance(protocol)) => { + n.class().is_final(db) && !nominal.satisfies_protocol(db, protocol) + } + + ( + ty @ (Type::LiteralString + | Type::StringLiteral(..) + | Type::BytesLiteral(..) + | Type::BooleanLiteral(..) + | Type::SliceLiteral(..) + | Type::ClassLiteral(..) + | Type::FunctionLiteral(..) + | Type::ModuleLiteral(..) + | Type::GenericAlias(..) + | Type::IntLiteral(..)), + Type::ProtocolInstance(protocol), + ) + | ( + Type::ProtocolInstance(protocol), + ty @ (Type::LiteralString + | Type::StringLiteral(..) + | Type::BytesLiteral(..) + | Type::BooleanLiteral(..) + | Type::SliceLiteral(..) + | Type::ClassLiteral(..) + | Type::FunctionLiteral(..) + | Type::ModuleLiteral(..) + | Type::GenericAlias(..) + | Type::IntLiteral(..)), + ) => !ty.satisfies_protocol(db, protocol), + + (Type::ProtocolInstance(protocol), Type::KnownInstance(known_instance)) + | (Type::KnownInstance(known_instance), Type::ProtocolInstance(protocol)) => { + !known_instance + .instance_fallback(db) + .satisfies_protocol(db, protocol) + } + + (Type::Callable(_), Type::ProtocolInstance(_)) + | (Type::ProtocolInstance(_), Type::Callable(_)) => { + // TODO disjointness between `Callable` and `ProtocolInstance` + false + } + + (Type::Tuple(..), Type::ProtocolInstance(..)) + | (Type::ProtocolInstance(..), Type::Tuple(..)) => { + // Currently we do not make any general assumptions about the disjointness of a `Tuple` type + // and a `ProtocolInstance` type because a `Tuple` type can be an instance of a tuple + // subclass. + // + // TODO when we capture the types of the protocol members, we can improve on this. + false + } + // for `type[Any]`/`type[Unknown]`/`type[Todo]`, we know the type cannot be any larger than `type`, // so although the type is dynamic we can still determine disjointedness in some situations (Type::SubclassOf(subclass_of_ty), other) @@ -1977,6 +2084,8 @@ impl<'db> Type<'db> { | Type::AlwaysTruthy | Type::PropertyInstance(_) => true, + Type::ProtocolInstance(protocol) => protocol.is_fully_static(), + Type::TypeVar(typevar) => match typevar.bound_or_constraints(db) { None => true, Some(TypeVarBoundOrConstraints::UpperBound(bound)) => bound.is_fully_static(db), @@ -2042,6 +2151,26 @@ impl<'db> Type<'db> { false } + Type::ProtocolInstance(..) => { + // It *might* be possible to have a singleton protocol-instance type...? + // + // E.g.: + // + // ```py + // from typing import Protocol, Callable + // + // class WeirdAndWacky(Protocol): + // @property + // def __class__(self) -> Callable[[], None]: ... + // ``` + // + // `WeirdAndWacky` only has a single possible inhabitant: `None`! + // It is thus a singleton type. + // However, going out of our way to recognise it as such is probably not worth it. + // Such cases should anyway be exceedingly rare and/or contrived. + false + } + // An unbounded, unconstrained typevar is not a singleton, because it can be // specialized to a non-singleton type. A bounded typevar is not a singleton, even if // the bound is a final singleton class, since it can still be specialized to `Never`. @@ -2144,6 +2273,11 @@ impl<'db> Type<'db> { | Type::SliceLiteral(..) | Type::KnownInstance(..) => true, + Type::ProtocolInstance(..) => { + // See comment in the `Type::ProtocolInstance` branch for `Type::is_singleton`. + false + } + // An unbounded, unconstrained typevar is not single-valued, because it can be // specialized to a multiple-valued type. A bounded typevar is not single-valued, even // if the bound is a final single-valued class, since it can still be specialized to @@ -2337,6 +2471,7 @@ impl<'db> Type<'db> { | Type::Tuple(_) | Type::TypeVar(_) | Type::NominalInstance(_) + | Type::ProtocolInstance(_) | Type::PropertyInstance(_) => None, } } @@ -2403,6 +2538,17 @@ impl<'db> Type<'db> { Type::NominalInstance(instance) => instance.class().instance_member(db, name), + Type::ProtocolInstance(protocol) => match protocol.inner() { + Protocol::FromClass(class) => class.instance_member(db, name), + Protocol::Synthesized(synthesized) => { + if synthesized.members(db).contains(name) { + SymbolAndQualifiers::todo("Capture type of synthesized protocol members") + } else { + Symbol::Unbound.into() + } + } + }, + Type::FunctionLiteral(_) => KnownClass::FunctionType .to_instance(db) .instance_member(db, name), @@ -2885,6 +3031,7 @@ impl<'db> Type<'db> { ), Type::NominalInstance(..) + | Type::ProtocolInstance(..) | Type::BooleanLiteral(..) | Type::IntLiteral(..) | Type::StringLiteral(..) @@ -2915,7 +3062,7 @@ impl<'db> Type<'db> { // It will need a special handling, so it remember the origin type to properly // resolve the attribute. if matches!( - self.into_instance() + self.into_nominal_instance() .and_then(|instance| instance.class().known(db)), Some(KnownClass::ModuleType | KnownClass::GenericAlias) ) { @@ -3181,6 +3328,8 @@ impl<'db> Type<'db> { None => try_dunder_bool()?, }, + Type::ProtocolInstance(_) => try_dunder_bool()?, + Type::KnownInstance(known_instance) => known_instance.bool(), Type::PropertyInstance(_) => Truthiness::AlwaysTrue, @@ -4315,11 +4464,14 @@ impl<'db> Type<'db> { }; let specialized = specialization .map(|specialization| { - Type::instance(ClassType::Generic(GenericAlias::new( + Type::instance( db, - generic_origin, - specialization, - ))) + ClassType::Generic(GenericAlias::new( + db, + generic_origin, + specialization, + )), + ) }) .unwrap_or(instance_ty); Ok(specialized) @@ -4350,9 +4502,9 @@ impl<'db> Type<'db> { pub fn to_instance(&self, db: &'db dyn Db) -> Option> { match self { Type::Dynamic(_) | Type::Never => Some(*self), - Type::ClassLiteral(class) => Some(Type::instance(class.default_specialization(db))), - Type::GenericAlias(alias) => Some(Type::instance(ClassType::from(*alias))), - Type::SubclassOf(subclass_of_ty) => Some(subclass_of_ty.to_instance()), + 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) { @@ -4371,6 +4523,7 @@ impl<'db> Type<'db> { | Type::DataclassDecorator(_) | Type::DataclassTransformer(_) | Type::NominalInstance(_) + | Type::ProtocolInstance(_) | Type::KnownInstance(_) | Type::PropertyInstance(_) | Type::ModuleLiteral(_) @@ -4417,11 +4570,11 @@ impl<'db> Type<'db> { KnownClass::Float.to_instance(db), ], ), - _ => Type::instance(class.default_specialization(db)), + _ => Type::instance(db, class.default_specialization(db)), }; Ok(ty) } - Type::GenericAlias(alias) => Ok(Type::instance(ClassType::from(*alias))), + Type::GenericAlias(alias) => Ok(Type::instance(db, ClassType::from(*alias))), Type::SubclassOf(_) | Type::BooleanLiteral(_) @@ -4444,6 +4597,7 @@ impl<'db> Type<'db> { | Type::Never | Type::FunctionLiteral(_) | Type::BoundSuper(_) + | Type::ProtocolInstance(_) | Type::PropertyInstance(_) => Err(InvalidTypeExpressionError { invalid_expressions: smallvec::smallvec![InvalidTypeExpression::InvalidType(*self)], fallback_type: Type::unknown(), @@ -4691,6 +4845,7 @@ impl<'db> Type<'db> { ), Type::AlwaysTruthy | Type::AlwaysFalsy => KnownClass::Type.to_instance(db), Type::BoundSuper(_) => KnownClass::Super.to_class_literal(db), + Type::ProtocolInstance(protocol) => protocol.to_meta_type(db), } } @@ -4812,9 +4967,11 @@ impl<'db> Type<'db> { | Type::BytesLiteral(_) | Type::SliceLiteral(_) | Type::BoundSuper(_) - // Instance contains a ClassType, which has already been specialized if needed, like - // above with BoundMethod's self_instance. + // `NominalInstance` contains a ClassType, which has already been specialized if needed, + // like above with BoundMethod's self_instance. | Type::NominalInstance(_) + // Same for `ProtocolInstance` + | Type::ProtocolInstance(_) | Type::KnownInstance(_) => self, } } @@ -4912,6 +5069,11 @@ impl<'db> Type<'db> { Self::TypeVar(var) => Some(TypeDefinition::TypeVar(var.definition(db))), + Self::ProtocolInstance(protocol) => match protocol.inner() { + Protocol::FromClass(class) => Some(TypeDefinition::Class(class.definition(db))), + Protocol::Synthesized(_) => None, + }, + Self::Union(_) | Self::Intersection(_) => None, // These types have no definition diff --git a/crates/red_knot_python_semantic/src/types/builder.rs b/crates/red_knot_python_semantic/src/types/builder.rs index 005441f15a..3efbd2db0a 100644 --- a/crates/red_knot_python_semantic/src/types/builder.rs +++ b/crates/red_knot_python_semantic/src/types/builder.rs @@ -591,7 +591,7 @@ impl<'db> InnerIntersectionBuilder<'db> { } _ => { let known_instance = new_positive - .into_instance() + .into_nominal_instance() .and_then(|instance| instance.class().known(db)); if known_instance == Some(KnownClass::Object) { @@ -705,7 +705,7 @@ impl<'db> InnerIntersectionBuilder<'db> { let contains_bool = || { self.positive .iter() - .filter_map(|ty| ty.into_instance()) + .filter_map(|ty| ty.into_nominal_instance()) .filter_map(|instance| instance.class().known(db)) .any(KnownClass::is_bool) }; diff --git a/crates/red_knot_python_semantic/src/types/class.rs b/crates/red_knot_python_semantic/src/types/class.rs index e158851bb9..b823ed03c3 100644 --- a/crates/red_knot_python_semantic/src/types/class.rs +++ b/crates/red_knot_python_semantic/src/types/class.rs @@ -144,6 +144,10 @@ impl<'db> ClassType<'db> { } } + pub(super) fn is_protocol(self, db: &'db dyn Db) -> bool { + self.class_literal(db).0.is_protocol(db) + } + pub(crate) fn name(self, db: &'db dyn Db) -> &'db ast::name::Name { let (class_literal, _) = self.class_literal(db); class_literal.name(db) @@ -1076,6 +1080,7 @@ impl<'db> ClassLiteral<'db> { Parameters::new([Parameter::positional_or_keyword(Name::new_static("other")) // TODO: could be `Self`. .with_annotated_type(Type::instance( + db, self.apply_optional_specialization(db, specialization), ))]), Some(KnownClass::Bool.to_instance(db)), @@ -2084,7 +2089,7 @@ impl<'db> KnownClass { pub(crate) fn to_instance(self, db: &'db dyn Db) -> Type<'db> { self.to_class_literal(db) .to_class_type(db) - .map(Type::instance) + .map(|class| Type::instance(db, class)) .unwrap_or_else(Type::unknown) } diff --git a/crates/red_knot_python_semantic/src/types/class_base.rs b/crates/red_knot_python_semantic/src/types/class_base.rs index c801ce0076..5e9fb2dcfb 100644 --- a/crates/red_knot_python_semantic/src/types/class_base.rs +++ b/crates/red_knot_python_semantic/src/types/class_base.rs @@ -106,6 +106,7 @@ impl<'db> ClassBase<'db> { | Type::SubclassOf(_) | Type::TypeVar(_) | Type::BoundSuper(_) + | Type::ProtocolInstance(_) | Type::AlwaysFalsy | Type::AlwaysTruthy => None, Type::KnownInstance(known_instance) => match known_instance { diff --git a/crates/red_knot_python_semantic/src/types/display.rs b/crates/red_knot_python_semantic/src/types/display.rs index 17c380a601..283d7c923f 100644 --- a/crates/red_knot_python_semantic/src/types/display.rs +++ b/crates/red_knot_python_semantic/src/types/display.rs @@ -17,6 +17,7 @@ use crate::types::{ use crate::Db; use rustc_hash::FxHashMap; +use super::instance::Protocol; use super::CallableType; impl<'db> Type<'db> { @@ -81,6 +82,25 @@ impl Display for DisplayRepresentation<'_> { (ClassType::Generic(alias), _) => write!(f, "{}", alias.display(self.db)), } } + Type::ProtocolInstance(protocol) => match protocol.inner() { + Protocol::FromClass(ClassType::NonGeneric(class)) => { + f.write_str(class.name(self.db)) + } + Protocol::FromClass(ClassType::Generic(alias)) => alias.display(self.db).fmt(f), + Protocol::Synthesized(synthetic) => { + f.write_str("') + } + }, Type::PropertyInstance(_) => f.write_str("property"), Type::ModuleLiteral(module) => { write!(f, "", module.module(self.db).name()) diff --git a/crates/red_knot_python_semantic/src/types/infer.rs b/crates/red_knot_python_semantic/src/types/infer.rs index 4af0a8ccff..e6891baf1a 100644 --- a/crates/red_knot_python_semantic/src/types/infer.rs +++ b/crates/red_knot_python_semantic/src/types/infer.rs @@ -2541,6 +2541,7 @@ impl<'db> TypeInferenceBuilder<'db> { Type::Dynamic(..) | Type::Never => true, Type::NominalInstance(..) + | Type::ProtocolInstance(_) | Type::BooleanLiteral(..) | Type::IntLiteral(..) | Type::StringLiteral(..) @@ -5008,6 +5009,7 @@ impl<'db> TypeInferenceBuilder<'db> { | Type::GenericAlias(_) | Type::SubclassOf(_) | Type::NominalInstance(_) + | Type::ProtocolInstance(_) | Type::KnownInstance(_) | Type::PropertyInstance(_) | Type::Union(_) @@ -5288,6 +5290,7 @@ impl<'db> TypeInferenceBuilder<'db> { | Type::GenericAlias(_) | Type::SubclassOf(_) | Type::NominalInstance(_) + | Type::ProtocolInstance(_) | Type::KnownInstance(_) | Type::PropertyInstance(_) | Type::Intersection(_) @@ -5313,6 +5316,7 @@ impl<'db> TypeInferenceBuilder<'db> { | Type::GenericAlias(_) | Type::SubclassOf(_) | Type::NominalInstance(_) + | Type::ProtocolInstance(_) | Type::KnownInstance(_) | Type::PropertyInstance(_) | Type::Intersection(_) diff --git a/crates/red_knot_python_semantic/src/types/instance.rs b/crates/red_knot_python_semantic/src/types/instance.rs index da8eeb78ee..a192f5b663 100644 --- a/crates/red_knot_python_semantic/src/types/instance.rs +++ b/crates/red_knot_python_semantic/src/types/instance.rs @@ -6,11 +6,15 @@ use super::{ClassType, KnownClass, SubclassOfType, Type}; use crate::{Db, FxOrderSet}; impl<'db> Type<'db> { - pub(crate) const fn instance(class: ClassType<'db>) -> Self { - Self::NominalInstance(NominalInstanceType { class }) + pub(crate) fn instance(db: &'db dyn Db, class: ClassType<'db>) -> Self { + if class.is_protocol(db) { + Self::ProtocolInstance(ProtocolInstanceType(Protocol::FromClass(class))) + } else { + Self::NominalInstance(NominalInstanceType { class }) + } } - pub(crate) const fn into_instance(self) -> Option> { + pub(crate) const fn into_nominal_instance(self) -> Option> { match self { Type::NominalInstance(instance_type) => Some(instance_type), _ => None, @@ -95,30 +99,26 @@ impl<'db> From> for Type<'db> { } } -#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, salsa::Update, salsa::Supertype)] -pub enum ProtocolInstanceType<'db> { - FromClass(ClassType<'db>), - Synthesized(SynthesizedProtocolType<'db>), -} +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord, salsa::Update)] +pub struct ProtocolInstanceType<'db>( + // Keep the inner field here private, + // so that the only way of constructing `ProtocolInstanceType` instances + // is through the `Type::instance` constructor function. + Protocol<'db>, +); -#[salsa::tracked] impl<'db> ProtocolInstanceType<'db> { - #[salsa::tracked(return_ref)] - fn protocol_members(self, db: &'db dyn Db) -> FxOrderSet { - match self { - Self::FromClass(class) => class - .class_literal(db) - .0 - .into_protocol_class(db) - .expect("Protocol class literal should be a protocol class") - .protocol_members(db), - Self::Synthesized(synthesized) => synthesized.members(db), - } + pub(super) fn protocol_members(self, db: &'db dyn Db) -> &'db FxOrderSet { + self.0.protocol_members(db) + } + + pub(super) fn inner(self) -> Protocol<'db> { + self.0 } pub(super) fn to_meta_type(self, db: &'db dyn Db) -> Type<'db> { - match self { - Self::FromClass(class) => SubclassOfType::from(db, class), + match self.0 { + Protocol::FromClass(class) => SubclassOfType::from(db, class), // TODO: we can and should do better here. // @@ -133,17 +133,32 @@ impl<'db> ProtocolInstanceType<'db> { // reveal_type(type(x)) # mypy: "type[def (builtins.int) -> builtins.str]" // reveal_type(type(x).__call__) # mypy: "def (*args: Any, **kwds: Any) -> Any" // ``` - Self::Synthesized(_) => KnownClass::Type.to_instance(db), + Protocol::Synthesized(_) => KnownClass::Type.to_instance(db), } } - pub(super) fn normalized(self, db: &'db dyn Db) -> Self { - match self { - Self::FromClass(_) => { - Self::Synthesized(SynthesizedProtocolType::new(db, self.protocol_members(db))) - } - Self::Synthesized(_) => self, + pub(super) fn normalized(self, db: &'db dyn Db) -> Type<'db> { + let members = self.protocol_members(db); + let object = KnownClass::Object.to_instance(db); + if members + .iter() + .all(|member| !object.member(db, member).symbol.is_unbound()) + { + return object; } + match self.0 { + Protocol::FromClass(_) => Type::ProtocolInstance(Self(Protocol::Synthesized( + SynthesizedProtocolType::new(db, self.protocol_members(db)), + ))), + Protocol::Synthesized(_) => Type::ProtocolInstance(self), + } + } + + /// TODO: should iterate over the types of the members + /// and check if any of them contain `Todo` types + #[expect(clippy::unused_self)] + pub(super) fn contains_todo(self) -> bool { + false } /// TODO: should not be considered fully static if any members do not have fully static types @@ -181,7 +196,35 @@ impl<'db> ProtocolInstanceType<'db> { } } -#[salsa::interned(debug)] -pub struct SynthesizedProtocolType<'db> { - members: FxOrderSet, +/// Private inner enum to represent the two kinds of protocol types. +/// This is not exposed publicly, so that the only way of constructing `Protocol` instances +/// is through the [`Type::instance`] constructor function. +#[derive( + Copy, Clone, Debug, Eq, PartialEq, Hash, salsa::Update, salsa::Supertype, PartialOrd, Ord, +)] +pub(super) enum Protocol<'db> { + FromClass(ClassType<'db>), + Synthesized(SynthesizedProtocolType<'db>), +} + +#[salsa::tracked] +impl<'db> Protocol<'db> { + #[salsa::tracked(return_ref)] + fn protocol_members(self, db: &'db dyn Db) -> FxOrderSet { + match self { + Self::FromClass(class) => class + .class_literal(db) + .0 + .into_protocol_class(db) + .expect("Protocol class literal should be a protocol class") + .protocol_members(db), + Self::Synthesized(synthesized) => synthesized.members(db).clone(), + } + } +} + +#[salsa::interned(debug)] +pub(super) struct SynthesizedProtocolType<'db> { + #[return_ref] + pub(super) members: FxOrderSet, } diff --git a/crates/red_knot_python_semantic/src/types/narrow.rs b/crates/red_knot_python_semantic/src/types/narrow.rs index 902a0042e2..815d6d8e3a 100644 --- a/crates/red_knot_python_semantic/src/types/narrow.rs +++ b/crates/red_knot_python_semantic/src/types/narrow.rs @@ -159,7 +159,7 @@ impl KnownConstraintFunction { /// union types are not yet supported. Returns `None` if the `classinfo` argument has a wrong type. fn generate_constraint<'db>(self, db: &'db dyn Db, classinfo: Type<'db>) -> Option> { let constraint_fn = |class| match self { - KnownConstraintFunction::IsInstance => Type::instance(class), + KnownConstraintFunction::IsInstance => Type::instance(db, class), KnownConstraintFunction::IsSubclass => SubclassOfType::from(db, class), }; @@ -682,7 +682,7 @@ impl<'db> NarrowingConstraintsBuilder<'db> { let symbol = self.expect_expr_name_symbol(id); constraints.insert( symbol, - Type::instance(rhs_class.unknown_specialization(self.db)), + Type::instance(self.db, rhs_class.unknown_specialization(self.db)), ); } } diff --git a/crates/red_knot_python_semantic/src/types/subclass_of.rs b/crates/red_knot_python_semantic/src/types/subclass_of.rs index c3ae9a5c02..56bccc8b68 100644 --- a/crates/red_knot_python_semantic/src/types/subclass_of.rs +++ b/crates/red_knot_python_semantic/src/types/subclass_of.rs @@ -94,9 +94,9 @@ impl<'db> SubclassOfType<'db> { } } - pub(crate) fn to_instance(self) -> Type<'db> { + pub(crate) fn to_instance(self, db: &'db dyn Db) -> Type<'db> { match self.subclass_of { - SubclassOfInner::Class(class) => Type::instance(class), + SubclassOfInner::Class(class) => Type::instance(db, class), SubclassOfInner::Dynamic(dynamic_type) => Type::Dynamic(dynamic_type), } } diff --git a/crates/red_knot_python_semantic/src/types/type_ordering.rs b/crates/red_knot_python_semantic/src/types/type_ordering.rs index b1c5d46510..29630505fe 100644 --- a/crates/red_knot_python_semantic/src/types/type_ordering.rs +++ b/crates/red_knot_python_semantic/src/types/type_ordering.rs @@ -126,13 +126,21 @@ pub(super) fn union_or_intersection_elements_ordering<'db>( (Type::SubclassOf(_), _) => Ordering::Less, (_, Type::SubclassOf(_)) => Ordering::Greater, + (Type::NominalInstance(left), Type::NominalInstance(right)) => { left.class().cmp(&right.class()) } - (Type::NominalInstance(_), _) => Ordering::Less, (_, Type::NominalInstance(_)) => Ordering::Greater, + (Type::ProtocolInstance(left_proto), Type::ProtocolInstance(right_proto)) => { + debug_assert_eq!(*left, left_proto.normalized(db)); + debug_assert_eq!(*right, right_proto.normalized(db)); + left_proto.cmp(right_proto) + } + (Type::ProtocolInstance(_), _) => Ordering::Less, + (_, Type::ProtocolInstance(_)) => Ordering::Greater, + (Type::TypeVar(left), Type::TypeVar(right)) => left.cmp(right), (Type::TypeVar(_), _) => Ordering::Less, (_, Type::TypeVar(_)) => Ordering::Greater,