[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:
Douglas Creager 2025-09-10 13:22:19 -04:00 committed by GitHub
parent ffead90410
commit 2ac4147435
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 1231 additions and 223 deletions

View file

@ -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. \

View file

@ -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),
));
}
}

View file

@ -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),

View file

@ -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 {

View file

@ -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>,
}

View file

@ -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,

View file

@ -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")