mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 21:34:57 +00:00
[ty] Add mdtests that exercise constraint sets (#20319)
This PR adds a new `ty_extensions.ConstraintSet` class, which is used to expose constraint sets to our mdtest framework. This lets us write a large collection of unit tests that exercise the invariants and rewrite rules of our constraint set implementation. As part of this, `is_assignable_to` and friends are updated to return a `ConstraintSet` instead of a `bool`, and we implement `ConstraintSet.__bool__` to return when a constraint set is always satisfied. That lets us still use `static_assert(is_assignable_to(...))`, since the assertion will coerce the constraint set to a bool, and also lets us `reveal_type(is_assignable_to(...))` to see more detail about whether/when the two types are assignable. That lets us get rid of `reveal_when_assignable_to` and friends, since they are now redundant with the expanded capabilities of `is_assignable_to`.
This commit is contained in:
parent
ffead90410
commit
2ac4147435
12 changed files with 1231 additions and 223 deletions
|
@ -3838,6 +3838,11 @@ impl<'db> Type<'db> {
|
|||
Truthiness::Ambiguous
|
||||
}
|
||||
|
||||
Type::KnownInstance(KnownInstanceType::ConstraintSet(tracked_set)) => {
|
||||
let constraints = tracked_set.constraints(db);
|
||||
Truthiness::from(constraints.is_always_satisfied(db))
|
||||
}
|
||||
|
||||
Type::FunctionLiteral(_)
|
||||
| Type::BoundMethod(_)
|
||||
| Type::WrapperDescriptor(_)
|
||||
|
@ -4186,8 +4191,8 @@ impl<'db> Type<'db> {
|
|||
Type::FunctionLiteral(function_type) => match function_type.known(db) {
|
||||
Some(
|
||||
KnownFunction::IsEquivalentTo
|
||||
| KnownFunction::IsSubtypeOf
|
||||
| KnownFunction::IsAssignableTo
|
||||
| KnownFunction::IsSubtypeOf
|
||||
| KnownFunction::IsDisjointFrom,
|
||||
) => Binding::single(
|
||||
self,
|
||||
|
@ -4200,25 +4205,58 @@ impl<'db> Type<'db> {
|
|||
.type_form()
|
||||
.with_annotated_type(Type::any()),
|
||||
]),
|
||||
Some(KnownClass::Bool.to_instance(db)),
|
||||
Some(KnownClass::ConstraintSet.to_instance(db)),
|
||||
),
|
||||
)
|
||||
.into(),
|
||||
|
||||
Some(
|
||||
KnownFunction::RevealWhenAssignableTo | KnownFunction::RevealWhenSubtypeOf,
|
||||
) => Binding::single(
|
||||
Some(KnownFunction::RangeConstraint) => Binding::single(
|
||||
self,
|
||||
Signature::new(
|
||||
Parameters::new([
|
||||
Parameter::positional_only(Some(Name::new_static("a")))
|
||||
Parameter::positional_only(Some(Name::new_static("lower_bound")))
|
||||
.type_form()
|
||||
.with_annotated_type(Type::any()),
|
||||
Parameter::positional_only(Some(Name::new_static("b")))
|
||||
Parameter::positional_only(Some(Name::new_static("typevar")))
|
||||
.type_form()
|
||||
.with_annotated_type(Type::any()),
|
||||
Parameter::positional_only(Some(Name::new_static("upper_bound")))
|
||||
.type_form()
|
||||
.with_annotated_type(Type::any()),
|
||||
]),
|
||||
Some(KnownClass::NoneType.to_instance(db)),
|
||||
Some(KnownClass::ConstraintSet.to_instance(db)),
|
||||
),
|
||||
)
|
||||
.into(),
|
||||
|
||||
Some(KnownFunction::NotEquivalentConstraint) => Binding::single(
|
||||
self,
|
||||
Signature::new(
|
||||
Parameters::new([
|
||||
Parameter::positional_only(Some(Name::new_static("typevar")))
|
||||
.type_form()
|
||||
.with_annotated_type(Type::any()),
|
||||
Parameter::positional_only(Some(Name::new_static("hole")))
|
||||
.type_form()
|
||||
.with_annotated_type(Type::any()),
|
||||
]),
|
||||
Some(KnownClass::ConstraintSet.to_instance(db)),
|
||||
),
|
||||
)
|
||||
.into(),
|
||||
|
||||
Some(KnownFunction::IncomparableConstraint) => Binding::single(
|
||||
self,
|
||||
Signature::new(
|
||||
Parameters::new([
|
||||
Parameter::positional_only(Some(Name::new_static("typevar")))
|
||||
.type_form()
|
||||
.with_annotated_type(Type::any()),
|
||||
Parameter::positional_only(Some(Name::new_static("pivot")))
|
||||
.type_form()
|
||||
.with_annotated_type(Type::any()),
|
||||
]),
|
||||
Some(KnownClass::ConstraintSet.to_instance(db)),
|
||||
),
|
||||
)
|
||||
.into(),
|
||||
|
@ -5702,6 +5740,10 @@ impl<'db> Type<'db> {
|
|||
invalid_expressions: smallvec::smallvec![InvalidTypeExpression::Field],
|
||||
fallback_type: Type::unknown(),
|
||||
}),
|
||||
KnownInstanceType::ConstraintSet(__call__) => Err(InvalidTypeExpressionError {
|
||||
invalid_expressions: smallvec::smallvec![InvalidTypeExpression::ConstraintSet],
|
||||
fallback_type: Type::unknown(),
|
||||
}),
|
||||
KnownInstanceType::SubscriptedProtocol(_) => Err(InvalidTypeExpressionError {
|
||||
invalid_expressions: smallvec::smallvec_inline![
|
||||
InvalidTypeExpression::Protocol
|
||||
|
@ -6851,6 +6893,20 @@ impl<'db> TypeMapping<'_, 'db> {
|
|||
}
|
||||
}
|
||||
|
||||
/// A Salsa-tracked constraint set. This is only needed to have something appropriately small to
|
||||
/// put in a [`KnownInstance::ConstraintSet`]. We don't actually manipulate these as part of using
|
||||
/// constraint sets to check things like assignability; they're only used as a debugging aid in
|
||||
/// mdtests. That means there's no need for this to be interned; being tracked is sufficient.
|
||||
#[salsa::tracked(debug, heap_size=ruff_memory_usage::heap_size)]
|
||||
#[derive(PartialOrd, Ord)]
|
||||
pub struct TrackedConstraintSet<'db> {
|
||||
#[returns(ref)]
|
||||
constraints: ConstraintSet<'db>,
|
||||
}
|
||||
|
||||
// The Salsa heap is tracked separately.
|
||||
impl get_size2::GetSize for TrackedConstraintSet<'_> {}
|
||||
|
||||
/// Singleton types that are heavily special-cased by ty. Despite its name,
|
||||
/// quite a different type to [`NominalInstanceType`].
|
||||
///
|
||||
|
@ -6893,6 +6949,10 @@ pub enum KnownInstanceType<'db> {
|
|||
|
||||
/// A single instance of `dataclasses.Field`
|
||||
Field(FieldInstance<'db>),
|
||||
|
||||
/// A constraint set, which is exposed in mdtests as an instance of
|
||||
/// `ty_extensions.ConstraintSet`.
|
||||
ConstraintSet(TrackedConstraintSet<'db>),
|
||||
}
|
||||
|
||||
fn walk_known_instance_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>(
|
||||
|
@ -6911,7 +6971,7 @@ fn walk_known_instance_type<'db, V: visitor::TypeVisitor<'db> + ?Sized>(
|
|||
KnownInstanceType::TypeAliasType(type_alias) => {
|
||||
visitor.visit_type_alias_type(db, type_alias);
|
||||
}
|
||||
KnownInstanceType::Deprecated(_) => {
|
||||
KnownInstanceType::Deprecated(_) | KnownInstanceType::ConstraintSet(_) => {
|
||||
// Nothing to visit
|
||||
}
|
||||
KnownInstanceType::Field(field) => {
|
||||
|
@ -6938,6 +6998,10 @@ impl<'db> KnownInstanceType<'db> {
|
|||
Self::Deprecated(deprecated)
|
||||
}
|
||||
Self::Field(field) => Self::Field(field.normalized_impl(db, visitor)),
|
||||
Self::ConstraintSet(set) => {
|
||||
// Nothing to normalize
|
||||
Self::ConstraintSet(set)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6951,6 +7015,7 @@ impl<'db> KnownInstanceType<'db> {
|
|||
Self::TypeAliasType(_) => KnownClass::TypeAliasType,
|
||||
Self::Deprecated(_) => KnownClass::Deprecated,
|
||||
Self::Field(_) => KnownClass::Field,
|
||||
Self::ConstraintSet(_) => KnownClass::ConstraintSet,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -7010,6 +7075,20 @@ impl<'db> KnownInstanceType<'db> {
|
|||
field.default_type(self.db).display(self.db).fmt(f)?;
|
||||
f.write_str("]")
|
||||
}
|
||||
KnownInstanceType::ConstraintSet(tracked_set) => {
|
||||
let constraints = tracked_set.constraints(self.db);
|
||||
if constraints.is_always_satisfied(self.db) {
|
||||
f.write_str("ty_extensions.ConstraintSet[always]")
|
||||
} else if constraints.is_never_satisfied(self.db) {
|
||||
f.write_str("ty_extensions.ConstraintSet[never]")
|
||||
} else {
|
||||
write!(
|
||||
f,
|
||||
"ty_extensions.ConstraintSet[{}]",
|
||||
constraints.display(self.db)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -7249,6 +7328,8 @@ enum InvalidTypeExpression<'db> {
|
|||
Deprecated,
|
||||
/// Same for `dataclasses.Field`
|
||||
Field,
|
||||
/// Same for `ty_extensions.ConstraintSet`
|
||||
ConstraintSet,
|
||||
/// Same for `typing.TypedDict`
|
||||
TypedDict,
|
||||
/// Type qualifiers are always invalid in *type expressions*,
|
||||
|
@ -7298,6 +7379,9 @@ impl<'db> InvalidTypeExpression<'db> {
|
|||
InvalidTypeExpression::Field => {
|
||||
f.write_str("`dataclasses.Field` is not allowed in type expressions")
|
||||
}
|
||||
InvalidTypeExpression::ConstraintSet => {
|
||||
f.write_str("`ty_extensions.ConstraintSet` is not allowed in type expressions")
|
||||
}
|
||||
InvalidTypeExpression::TypedDict => {
|
||||
f.write_str(
|
||||
"The special form `typing.TypedDict` is not allowed in type expressions. \
|
||||
|
|
|
@ -32,8 +32,8 @@ use crate::types::signatures::{Parameter, ParameterForm, Parameters};
|
|||
use crate::types::tuple::{Tuple, TupleLength, TupleType};
|
||||
use crate::types::{
|
||||
BoundMethodType, ClassLiteral, DataclassParams, FieldInstance, KnownBoundMethodType,
|
||||
KnownClass, KnownInstanceType, PropertyInstanceType, SpecialFormType, TypeAliasType,
|
||||
TypeMapping, UnionType, WrapperDescriptorKind, enums, ide_support, todo_type,
|
||||
KnownClass, KnownInstanceType, PropertyInstanceType, SpecialFormType, TrackedConstraintSet,
|
||||
TypeAliasType, TypeMapping, UnionType, WrapperDescriptorKind, enums, ide_support, todo_type,
|
||||
};
|
||||
use ruff_db::diagnostic::{Annotation, Diagnostic, SubDiagnostic, SubDiagnosticSeverity};
|
||||
use ruff_python_ast::{self as ast, PythonVersion};
|
||||
|
@ -586,32 +586,43 @@ impl<'db> Bindings<'db> {
|
|||
Type::FunctionLiteral(function_type) => match function_type.known(db) {
|
||||
Some(KnownFunction::IsEquivalentTo) => {
|
||||
if let [Some(ty_a), Some(ty_b)] = overload.parameter_types() {
|
||||
overload.set_return_type(Type::BooleanLiteral(
|
||||
ty_a.is_equivalent_to(db, *ty_b),
|
||||
let constraints =
|
||||
ty_a.when_equivalent_to::<ConstraintSet>(db, *ty_b);
|
||||
let tracked = TrackedConstraintSet::new(db, constraints);
|
||||
overload.set_return_type(Type::KnownInstance(
|
||||
KnownInstanceType::ConstraintSet(tracked),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Some(KnownFunction::IsSubtypeOf) => {
|
||||
if let [Some(ty_a), Some(ty_b)] = overload.parameter_types() {
|
||||
overload.set_return_type(Type::BooleanLiteral(
|
||||
ty_a.is_subtype_of(db, *ty_b),
|
||||
let constraints = ty_a.when_subtype_of::<ConstraintSet>(db, *ty_b);
|
||||
let tracked = TrackedConstraintSet::new(db, constraints);
|
||||
overload.set_return_type(Type::KnownInstance(
|
||||
KnownInstanceType::ConstraintSet(tracked),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Some(KnownFunction::IsAssignableTo) => {
|
||||
if let [Some(ty_a), Some(ty_b)] = overload.parameter_types() {
|
||||
overload.set_return_type(Type::BooleanLiteral(
|
||||
ty_a.is_assignable_to(db, *ty_b),
|
||||
let constraints =
|
||||
ty_a.when_assignable_to::<ConstraintSet>(db, *ty_b);
|
||||
let tracked = TrackedConstraintSet::new(db, constraints);
|
||||
overload.set_return_type(Type::KnownInstance(
|
||||
KnownInstanceType::ConstraintSet(tracked),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Some(KnownFunction::IsDisjointFrom) => {
|
||||
if let [Some(ty_a), Some(ty_b)] = overload.parameter_types() {
|
||||
overload.set_return_type(Type::BooleanLiteral(
|
||||
ty_a.is_disjoint_from(db, *ty_b),
|
||||
let constraints =
|
||||
ty_a.when_disjoint_from::<ConstraintSet>(db, *ty_b);
|
||||
let tracked = TrackedConstraintSet::new(db, constraints);
|
||||
overload.set_return_type(Type::KnownInstance(
|
||||
KnownInstanceType::ConstraintSet(tracked),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3652,6 +3652,8 @@ pub enum KnownClass {
|
|||
TypedDictFallback,
|
||||
// string.templatelib
|
||||
Template,
|
||||
// ty_extensions
|
||||
ConstraintSet,
|
||||
}
|
||||
|
||||
impl KnownClass {
|
||||
|
@ -3751,6 +3753,7 @@ impl KnownClass {
|
|||
| Self::InitVar
|
||||
| Self::NamedTupleFallback
|
||||
| Self::NamedTupleLike
|
||||
| Self::ConstraintSet
|
||||
| Self::TypedDictFallback => Some(Truthiness::Ambiguous),
|
||||
|
||||
Self::Tuple => None,
|
||||
|
@ -3830,6 +3833,7 @@ impl KnownClass {
|
|||
| KnownClass::InitVar
|
||||
| KnownClass::NamedTupleFallback
|
||||
| KnownClass::NamedTupleLike
|
||||
| KnownClass::ConstraintSet
|
||||
| KnownClass::TypedDictFallback
|
||||
| KnownClass::BuiltinFunctionType
|
||||
| KnownClass::Template => false,
|
||||
|
@ -3908,6 +3912,7 @@ impl KnownClass {
|
|||
| KnownClass::InitVar
|
||||
| KnownClass::NamedTupleFallback
|
||||
| KnownClass::NamedTupleLike
|
||||
| KnownClass::ConstraintSet
|
||||
| KnownClass::TypedDictFallback
|
||||
| KnownClass::BuiltinFunctionType
|
||||
| KnownClass::Template => false,
|
||||
|
@ -3986,6 +3991,7 @@ impl KnownClass {
|
|||
| KnownClass::TypedDictFallback
|
||||
| KnownClass::NamedTupleLike
|
||||
| KnownClass::NamedTupleFallback
|
||||
| KnownClass::ConstraintSet
|
||||
| KnownClass::BuiltinFunctionType
|
||||
| KnownClass::Template => false,
|
||||
}
|
||||
|
@ -4075,6 +4081,7 @@ impl KnownClass {
|
|||
| Self::KwOnly
|
||||
| Self::InitVar
|
||||
| Self::NamedTupleFallback
|
||||
| Self::ConstraintSet
|
||||
| Self::TypedDictFallback
|
||||
| Self::BuiltinFunctionType
|
||||
| Self::Template => false,
|
||||
|
@ -4173,6 +4180,7 @@ impl KnownClass {
|
|||
Self::InitVar => "InitVar",
|
||||
Self::NamedTupleFallback => "NamedTupleFallback",
|
||||
Self::NamedTupleLike => "NamedTupleLike",
|
||||
Self::ConstraintSet => "ConstraintSet",
|
||||
Self::TypedDictFallback => "TypedDictFallback",
|
||||
Self::Template => "Template",
|
||||
}
|
||||
|
@ -4439,7 +4447,7 @@ impl KnownClass {
|
|||
| Self::OrderedDict => KnownModule::Collections,
|
||||
Self::Field | Self::KwOnly | Self::InitVar => KnownModule::Dataclasses,
|
||||
Self::NamedTupleFallback | Self::TypedDictFallback => KnownModule::TypeCheckerInternals,
|
||||
Self::NamedTupleLike => KnownModule::TyExtensions,
|
||||
Self::NamedTupleLike | Self::ConstraintSet => KnownModule::TyExtensions,
|
||||
Self::Template => KnownModule::Templatelib,
|
||||
}
|
||||
}
|
||||
|
@ -4518,6 +4526,7 @@ impl KnownClass {
|
|||
| Self::Iterator
|
||||
| Self::NamedTupleFallback
|
||||
| Self::NamedTupleLike
|
||||
| Self::ConstraintSet
|
||||
| Self::TypedDictFallback
|
||||
| Self::BuiltinFunctionType
|
||||
| Self::Template => Some(false),
|
||||
|
@ -4601,6 +4610,7 @@ impl KnownClass {
|
|||
| Self::Iterator
|
||||
| Self::NamedTupleFallback
|
||||
| Self::NamedTupleLike
|
||||
| Self::ConstraintSet
|
||||
| Self::TypedDictFallback
|
||||
| Self::BuiltinFunctionType
|
||||
| Self::Template => false,
|
||||
|
@ -4693,6 +4703,7 @@ impl KnownClass {
|
|||
"InitVar" => Self::InitVar,
|
||||
"NamedTupleFallback" => Self::NamedTupleFallback,
|
||||
"NamedTupleLike" => Self::NamedTupleLike,
|
||||
"ConstraintSet" => Self::ConstraintSet,
|
||||
"TypedDictFallback" => Self::TypedDictFallback,
|
||||
"Template" => Self::Template,
|
||||
_ => return None,
|
||||
|
@ -4761,6 +4772,7 @@ impl KnownClass {
|
|||
| Self::NamedTupleFallback
|
||||
| Self::TypedDictFallback
|
||||
| Self::NamedTupleLike
|
||||
| Self::ConstraintSet
|
||||
| Self::Awaitable
|
||||
| Self::Generator
|
||||
| Self::Template => module == self.canonical_module(db),
|
||||
|
|
|
@ -166,7 +166,8 @@ impl<'db> ClassBase<'db> {
|
|||
KnownInstanceType::TypeAliasType(_)
|
||||
| KnownInstanceType::TypeVar(_)
|
||||
| KnownInstanceType::Deprecated(_)
|
||||
| KnownInstanceType::Field(_) => None,
|
||||
| KnownInstanceType::Field(_)
|
||||
| KnownInstanceType::ConstraintSet(_) => None,
|
||||
},
|
||||
|
||||
Type::SpecialForm(special_form) => match special_form {
|
||||
|
|
|
@ -102,8 +102,12 @@ use smallvec::{SmallVec, smallvec};
|
|||
use crate::Db;
|
||||
use crate::types::{BoundTypeVarInstance, IntersectionType, Type, UnionType};
|
||||
|
||||
fn comparable<'db>(db: &'db dyn Db, left: Type<'db>, right: Type<'db>) -> bool {
|
||||
left.is_subtype_of(db, right) || right.is_subtype_of(db, left)
|
||||
}
|
||||
|
||||
fn incomparable<'db>(db: &'db dyn Db, left: Type<'db>, right: Type<'db>) -> bool {
|
||||
!left.is_subtype_of(db, right) && !right.is_subtype_of(db, left)
|
||||
!comparable(db, left, right)
|
||||
}
|
||||
|
||||
/// Encodes the constraints under which a type property (e.g. assignability) holds.
|
||||
|
@ -246,8 +250,8 @@ where
|
|||
/// be simplified into a single clause.
|
||||
///
|
||||
/// [POPL2015]: https://doi.org/10.1145/2676726.2676991
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct ConstraintSet<'db> {
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq, get_size2::GetSize, salsa::Update)]
|
||||
pub struct ConstraintSet<'db> {
|
||||
// NOTE: We use 2 here because there are a couple of places where we create unions of 2 clauses
|
||||
// as temporary values — in particular when negating a constraint — and this lets us avoid
|
||||
// spilling the temporary value to the heap.
|
||||
|
@ -269,6 +273,42 @@ impl<'db> ConstraintSet<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn range(
|
||||
db: &'db dyn Db,
|
||||
lower: Type<'db>,
|
||||
typevar: BoundTypeVarInstance<'db>,
|
||||
upper: Type<'db>,
|
||||
) -> Self {
|
||||
let lower = lower.bottom_materialization(db);
|
||||
let upper = upper.top_materialization(db);
|
||||
let constraint = Constraint::range(db, lower, upper).constrain(typevar);
|
||||
let mut result = Self::never();
|
||||
result.union_constraint(db, constraint);
|
||||
result
|
||||
}
|
||||
|
||||
pub(crate) fn not_equivalent(
|
||||
db: &'db dyn Db,
|
||||
typevar: BoundTypeVarInstance<'db>,
|
||||
hole: Type<'db>,
|
||||
) -> Self {
|
||||
let constraint = Constraint::not_equivalent(db, hole).constrain(typevar);
|
||||
let mut result = Self::never();
|
||||
result.union_constraint(db, constraint);
|
||||
result
|
||||
}
|
||||
|
||||
pub(crate) fn incomparable(
|
||||
db: &'db dyn Db,
|
||||
typevar: BoundTypeVarInstance<'db>,
|
||||
pivot: Type<'db>,
|
||||
) -> Self {
|
||||
let constraint = Constraint::incomparable(db, pivot).constrain(typevar);
|
||||
let mut result = Self::never();
|
||||
result.union_constraint(db, constraint);
|
||||
result
|
||||
}
|
||||
|
||||
/// Updates this set to be the union of itself and a constraint.
|
||||
fn union_constraint(
|
||||
&mut self,
|
||||
|
@ -429,7 +469,7 @@ impl<'db> Constraints<'db> for ConstraintSet<'db> {
|
|||
/// This is called a "constraint set", and denoted _C_, in [[POPL2015][]].
|
||||
///
|
||||
/// [POPL2015]: https://doi.org/10.1145/2676726.2676991
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq, get_size2::GetSize, salsa::Update)]
|
||||
pub(crate) struct ConstraintClause<'db> {
|
||||
// NOTE: We use 1 here because most clauses only mention a single typevar.
|
||||
constraints: SmallVec<[ConstrainedTypeVar<'db>; 1]>,
|
||||
|
@ -810,7 +850,7 @@ impl<'db> ConstraintClause<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq, get_size2::GetSize, salsa::Update)]
|
||||
pub(crate) struct ConstrainedTypeVar<'db> {
|
||||
typevar: BoundTypeVarInstance<'db>,
|
||||
constraint: Constraint<'db>,
|
||||
|
@ -857,7 +897,7 @@ impl<'db> ConstrainedTypeVar<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq, get_size2::GetSize, salsa::Update)]
|
||||
pub(crate) enum Constraint<'db> {
|
||||
Range(RangeConstraint<'db>),
|
||||
NotEquivalent(NotEquivalentConstraint<'db>),
|
||||
|
@ -980,7 +1020,7 @@ impl<'db> Satisfiable<Constraint<'db>> {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq, get_size2::GetSize, salsa::Update)]
|
||||
pub(crate) struct RangeConstraint<'db> {
|
||||
lower: Type<'db>,
|
||||
upper: Type<'db>,
|
||||
|
@ -1054,7 +1094,7 @@ impl<'db> RangeConstraint<'db> {
|
|||
db: &'db dyn Db,
|
||||
other: &IncomparableConstraint<'db>,
|
||||
) -> Simplifiable<Constraint<'db>> {
|
||||
if other.ty.is_subtype_of(db, other.ty) || self.upper.is_subtype_of(db, other.ty) {
|
||||
if other.ty.is_subtype_of(db, self.lower) || self.upper.is_subtype_of(db, other.ty) {
|
||||
return Simplifiable::NeverSatisfiable;
|
||||
}
|
||||
|
||||
|
@ -1189,7 +1229,7 @@ impl<'db> RangeConstraint<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq, get_size2::GetSize, salsa::Update)]
|
||||
pub(crate) struct NotEquivalentConstraint<'db> {
|
||||
ty: Type<'db>,
|
||||
}
|
||||
|
@ -1224,8 +1264,9 @@ impl<'db> NotEquivalentConstraint<'db> {
|
|||
db: &'db dyn Db,
|
||||
other: &IncomparableConstraint<'db>,
|
||||
) -> Simplifiable<Constraint<'db>> {
|
||||
// (α ≠ t) ∧ (a ≁ t) = a ≁ t
|
||||
if self.ty.is_equivalent_to(db, other.ty) {
|
||||
// If the hole and pivot are comparable, then removing the hole from the incomparable
|
||||
// constraint doesn't do anything.
|
||||
if comparable(db, self.ty, other.ty) {
|
||||
return Simplifiable::Simplified(Constraint::Incomparable(other.clone()));
|
||||
}
|
||||
Simplifiable::NotSimplified(
|
||||
|
@ -1302,7 +1343,7 @@ impl<'db> NotEquivalentConstraint<'db> {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq, get_size2::GetSize, salsa::Update)]
|
||||
pub(crate) struct IncomparableConstraint<'db> {
|
||||
ty: Type<'db>,
|
||||
}
|
||||
|
|
|
@ -79,8 +79,9 @@ use crate::types::visitor::any_over_type;
|
|||
use crate::types::{
|
||||
BoundMethodType, BoundTypeVarInstance, CallableType, ClassBase, ClassLiteral, ClassType,
|
||||
DeprecatedInstance, DynamicType, FindLegacyTypeVarsVisitor, HasRelationToVisitor,
|
||||
IsEquivalentVisitor, KnownClass, NormalizedVisitor, SpecialFormType, Truthiness, Type,
|
||||
TypeMapping, TypeRelation, UnionBuilder, all_members, binding_type, walk_type_mapping,
|
||||
IsEquivalentVisitor, KnownClass, KnownInstanceType, NormalizedVisitor, SpecialFormType,
|
||||
TrackedConstraintSet, Truthiness, Type, TypeMapping, TypeRelation, UnionBuilder, all_members,
|
||||
binding_type, walk_type_mapping,
|
||||
};
|
||||
use crate::{Db, FxOrderSet, ModuleName, resolve_module};
|
||||
|
||||
|
@ -1255,10 +1256,12 @@ pub enum KnownFunction {
|
|||
HasMember,
|
||||
/// `ty_extensions.reveal_protocol_interface`
|
||||
RevealProtocolInterface,
|
||||
/// `ty_extensions.reveal_when_assignable_to`
|
||||
RevealWhenAssignableTo,
|
||||
/// `ty_extensions.reveal_when_subtype_of`
|
||||
RevealWhenSubtypeOf,
|
||||
/// `ty_extensions.range_constraint`
|
||||
RangeConstraint,
|
||||
/// `ty_extensions.not_equivalent_constraint`
|
||||
NotEquivalentConstraint,
|
||||
/// `ty_extensions.incomparable_constraint`
|
||||
IncomparableConstraint,
|
||||
}
|
||||
|
||||
impl KnownFunction {
|
||||
|
@ -1324,8 +1327,9 @@ impl KnownFunction {
|
|||
| Self::StaticAssert
|
||||
| Self::HasMember
|
||||
| Self::RevealProtocolInterface
|
||||
| Self::RevealWhenAssignableTo
|
||||
| Self::RevealWhenSubtypeOf
|
||||
| Self::RangeConstraint
|
||||
| Self::NotEquivalentConstraint
|
||||
| Self::IncomparableConstraint
|
||||
| Self::AllMembers => module.is_ty_extensions(),
|
||||
Self::ImportModule => module.is_importlib(),
|
||||
}
|
||||
|
@ -1621,52 +1625,82 @@ impl KnownFunction {
|
|||
overload.set_return_type(Type::module_literal(db, file, module));
|
||||
}
|
||||
|
||||
KnownFunction::RevealWhenAssignableTo => {
|
||||
let [Some(ty_a), Some(ty_b)] = overload.parameter_types() else {
|
||||
return;
|
||||
};
|
||||
let constraints = ty_a.when_assignable_to::<ConstraintSet>(db, *ty_b);
|
||||
let Some(builder) =
|
||||
context.report_diagnostic(DiagnosticId::RevealedType, Severity::Info)
|
||||
KnownFunction::RangeConstraint => {
|
||||
let [
|
||||
Some(lower),
|
||||
Some(Type::NonInferableTypeVar(typevar)),
|
||||
Some(upper),
|
||||
] = parameter_types
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let mut diag = builder.into_diagnostic("Assignability holds");
|
||||
let span = context.span(call_expression);
|
||||
if constraints.is_always_satisfied(db) {
|
||||
diag.annotate(Annotation::primary(span).message("always"));
|
||||
} else if constraints.is_never_satisfied(db) {
|
||||
diag.annotate(Annotation::primary(span).message("never"));
|
||||
} else {
|
||||
diag.annotate(
|
||||
Annotation::primary(span)
|
||||
.message(format_args!("when {}", constraints.display(db))),
|
||||
);
|
||||
}
|
||||
|
||||
let constraints = ConstraintSet::range(db, *lower, *typevar, *upper);
|
||||
let tracked = TrackedConstraintSet::new(db, constraints);
|
||||
overload.set_return_type(Type::KnownInstance(KnownInstanceType::ConstraintSet(
|
||||
tracked,
|
||||
)));
|
||||
}
|
||||
|
||||
KnownFunction::RevealWhenSubtypeOf => {
|
||||
let [Some(ty_a), Some(ty_b)] = overload.parameter_types() else {
|
||||
KnownFunction::NotEquivalentConstraint => {
|
||||
let [Some(Type::NonInferableTypeVar(typevar)), Some(hole)] = parameter_types else {
|
||||
return;
|
||||
};
|
||||
let constraints = ty_a.when_subtype_of::<ConstraintSet>(db, *ty_b);
|
||||
let Some(builder) =
|
||||
context.report_diagnostic(DiagnosticId::RevealedType, Severity::Info)
|
||||
|
||||
if !hole.is_equivalent_to(db, hole.top_materialization(db)) {
|
||||
if let Some(builder) =
|
||||
context.report_lint(&INVALID_ARGUMENT_TYPE, call_expression)
|
||||
{
|
||||
let mut diagnostic = builder.into_diagnostic(format_args!(
|
||||
"Not-equivalent constraint must have a fully static type"
|
||||
));
|
||||
diagnostic.annotate(
|
||||
Annotation::secondary(context.span(&call_expression.arguments.args[1]))
|
||||
.message(format_args!(
|
||||
"Type `{}` is not fully static",
|
||||
hole.display(db)
|
||||
)),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let constraints = ConstraintSet::not_equivalent(db, *typevar, *hole);
|
||||
let tracked = TrackedConstraintSet::new(db, constraints);
|
||||
overload.set_return_type(Type::KnownInstance(KnownInstanceType::ConstraintSet(
|
||||
tracked,
|
||||
)));
|
||||
}
|
||||
|
||||
KnownFunction::IncomparableConstraint => {
|
||||
let [Some(Type::NonInferableTypeVar(typevar)), Some(pivot)] = parameter_types
|
||||
else {
|
||||
return;
|
||||
};
|
||||
let mut diag = builder.into_diagnostic("Subtyping holds");
|
||||
let span = context.span(call_expression);
|
||||
if constraints.is_always_satisfied(db) {
|
||||
diag.annotate(Annotation::primary(span).message("always"));
|
||||
} else if constraints.is_never_satisfied(db) {
|
||||
diag.annotate(Annotation::primary(span).message("never"));
|
||||
} else {
|
||||
diag.annotate(
|
||||
Annotation::primary(span)
|
||||
.message(format_args!("when {}", constraints.display(db))),
|
||||
);
|
||||
|
||||
if !pivot.is_equivalent_to(db, pivot.top_materialization(db)) {
|
||||
if let Some(builder) =
|
||||
context.report_lint(&INVALID_ARGUMENT_TYPE, call_expression)
|
||||
{
|
||||
let mut diagnostic = builder.into_diagnostic(format_args!(
|
||||
"Incomparable constraint must have a fully static type"
|
||||
));
|
||||
diagnostic.annotate(
|
||||
Annotation::secondary(context.span(&call_expression.arguments.args[1]))
|
||||
.message(format_args!(
|
||||
"Type `{}` is not fully static",
|
||||
pivot.display(db)
|
||||
)),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let constraints = ConstraintSet::incomparable(db, *typevar, *pivot);
|
||||
let tracked = TrackedConstraintSet::new(db, constraints);
|
||||
overload.set_return_type(Type::KnownInstance(KnownInstanceType::ConstraintSet(
|
||||
tracked,
|
||||
)));
|
||||
}
|
||||
|
||||
_ => {}
|
||||
|
@ -1729,8 +1763,9 @@ pub(crate) mod tests {
|
|||
| KnownFunction::IsEquivalentTo
|
||||
| KnownFunction::HasMember
|
||||
| KnownFunction::RevealProtocolInterface
|
||||
| KnownFunction::RevealWhenAssignableTo
|
||||
| KnownFunction::RevealWhenSubtypeOf
|
||||
| KnownFunction::RangeConstraint
|
||||
| KnownFunction::NotEquivalentConstraint
|
||||
| KnownFunction::IncomparableConstraint
|
||||
| KnownFunction::AllMembers => KnownModule::TyExtensions,
|
||||
|
||||
KnownFunction::ImportModule => KnownModule::ImportLib,
|
||||
|
|
|
@ -93,6 +93,7 @@ use crate::semantic_index::{
|
|||
};
|
||||
use crate::types::call::{Binding, Bindings, CallArguments, CallError, CallErrorKind};
|
||||
use crate::types::class::{CodeGeneratorKind, FieldKind, MetaclassErrorKind, MethodDecorator};
|
||||
use crate::types::constraints::Constraints;
|
||||
use crate::types::diagnostic::{
|
||||
self, CALL_NON_CALLABLE, CONFLICTING_DECLARATIONS, CONFLICTING_METACLASS,
|
||||
CYCLIC_CLASS_DEFINITION, DIVISION_BY_ZERO, DUPLICATE_KW_ONLY, INCONSISTENT_MRO,
|
||||
|
@ -130,10 +131,10 @@ use crate::types::{
|
|||
CallDunderError, CallableType, ClassLiteral, ClassType, DataclassParams, DynamicType,
|
||||
IntersectionBuilder, IntersectionType, KnownClass, KnownInstanceType, LintDiagnosticGuard,
|
||||
MemberLookupPolicy, MetaclassCandidate, PEP695TypeAliasType, Parameter, ParameterForm,
|
||||
Parameters, SpecialFormType, SubclassOfType, Truthiness, Type, TypeAliasType,
|
||||
TypeAndQualifiers, TypeIsType, TypeQualifiers, TypeVarBoundOrConstraintsEvaluation,
|
||||
TypeVarDefaultEvaluation, TypeVarInstance, TypeVarKind, UnionBuilder, UnionType, binding_type,
|
||||
todo_type,
|
||||
Parameters, SpecialFormType, SubclassOfType, TrackedConstraintSet, Truthiness, Type,
|
||||
TypeAliasType, TypeAndQualifiers, TypeIsType, TypeQualifiers,
|
||||
TypeVarBoundOrConstraintsEvaluation, TypeVarDefaultEvaluation, TypeVarInstance, TypeVarKind,
|
||||
UnionBuilder, UnionType, binding_type, todo_type,
|
||||
};
|
||||
use crate::unpack::{EvaluationMode, Unpack, UnpackPosition};
|
||||
use crate::util::diagnostics::format_enumeration;
|
||||
|
@ -7375,6 +7376,18 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
Type::IntLiteral(!i64::from(bool))
|
||||
}
|
||||
|
||||
(
|
||||
ast::UnaryOp::Invert,
|
||||
Type::KnownInstance(KnownInstanceType::ConstraintSet(constraints)),
|
||||
) => {
|
||||
let constraints = constraints.constraints(self.db()).clone();
|
||||
let result = constraints.negate(self.db());
|
||||
Type::KnownInstance(KnownInstanceType::ConstraintSet(TrackedConstraintSet::new(
|
||||
self.db(),
|
||||
result,
|
||||
)))
|
||||
}
|
||||
|
||||
(ast::UnaryOp::Not, ty) => ty
|
||||
.try_bool(self.db())
|
||||
.unwrap_or_else(|err| {
|
||||
|
@ -7726,6 +7739,32 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
|
|||
op,
|
||||
),
|
||||
|
||||
(
|
||||
Type::KnownInstance(KnownInstanceType::ConstraintSet(left)),
|
||||
Type::KnownInstance(KnownInstanceType::ConstraintSet(right)),
|
||||
ast::Operator::BitAnd,
|
||||
) => {
|
||||
let left = left.constraints(self.db()).clone();
|
||||
let right = right.constraints(self.db()).clone();
|
||||
let result = left.and(self.db(), || right);
|
||||
Some(Type::KnownInstance(KnownInstanceType::ConstraintSet(
|
||||
TrackedConstraintSet::new(self.db(), result),
|
||||
)))
|
||||
}
|
||||
|
||||
(
|
||||
Type::KnownInstance(KnownInstanceType::ConstraintSet(left)),
|
||||
Type::KnownInstance(KnownInstanceType::ConstraintSet(right)),
|
||||
ast::Operator::BitOr,
|
||||
) => {
|
||||
let left = left.constraints(self.db()).clone();
|
||||
let right = right.constraints(self.db()).clone();
|
||||
let result = left.or(self.db(), || right);
|
||||
Some(Type::KnownInstance(KnownInstanceType::ConstraintSet(
|
||||
TrackedConstraintSet::new(self.db(), result),
|
||||
)))
|
||||
}
|
||||
|
||||
// We've handled all of the special cases that we support for literals, so we need to
|
||||
// fall back on looking for dunder methods on one of the operand types.
|
||||
(
|
||||
|
@ -10594,6 +10633,15 @@ impl<'db> TypeInferenceBuilder<'db, '_> {
|
|||
}
|
||||
Type::unknown()
|
||||
}
|
||||
KnownInstanceType::ConstraintSet(_) => {
|
||||
self.infer_type_expression(&subscript.slice);
|
||||
if let Some(builder) = self.context.report_lint(&INVALID_TYPE_FORM, subscript) {
|
||||
builder.into_diagnostic(format_args!(
|
||||
"`ty_extensions.ConstraintSet` is not allowed in type expressions",
|
||||
));
|
||||
}
|
||||
Type::unknown()
|
||||
}
|
||||
KnownInstanceType::TypeVar(_) => {
|
||||
self.infer_type_expression(&subscript.slice);
|
||||
todo_type!("TypeVar annotations")
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue