Skip to content

Commit 943a7b3

Browse files
committed
constraint implication
1 parent 33816d6 commit 943a7b3

10 files changed

Lines changed: 170 additions & 75 deletions

File tree

crates/ty_python_semantic/src/types.rs

Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ pub(crate) type ApplyTypeMappingVisitor<'db> = TypeTransformer<'db, TypeMapping<
198198

199199
/// A [`PairVisitor`] that is used in `has_relation_to` methods.
200200
pub(crate) type HasRelationToVisitor<'db> =
201-
CycleDetector<TypeRelation, (Type<'db>, Type<'db>, TypeRelation), ConstraintSet<'db>>;
201+
CycleDetector<TypeRelation<'db>, (Type<'db>, Type<'db>, TypeRelation<'db>), ConstraintSet<'db>>;
202202

203203
impl Default for HasRelationToVisitor<'_> {
204204
fn default() -> Self {
@@ -1589,7 +1589,7 @@ impl<'db> Type<'db> {
15891589
db: &'db dyn Db,
15901590
target: Type<'db>,
15911591
inferable: InferableTypeVars<'_, 'db>,
1592-
relation: TypeRelation,
1592+
relation: TypeRelation<'db>,
15931593
) -> ConstraintSet<'db> {
15941594
self.has_relation_to_impl(
15951595
db,
@@ -1606,7 +1606,7 @@ impl<'db> Type<'db> {
16061606
db: &'db dyn Db,
16071607
target: Type<'db>,
16081608
inferable: InferableTypeVars<'_, 'db>,
1609-
relation: TypeRelation,
1609+
relation: TypeRelation<'db>,
16101610
relation_visitor: &HasRelationToVisitor<'db>,
16111611
disjointness_visitor: &IsDisjointVisitor<'db>,
16121612
) -> ConstraintSet<'db> {
@@ -1699,6 +1699,9 @@ impl<'db> Type<'db> {
16991699
Type::Union(union) => union.elements(db).iter().any(Type::is_dynamic),
17001700
_ => false,
17011701
},
1702+
TypeRelation::ConstraintImplication(_) => {
1703+
panic!("constraints should not contain dynamic types")
1704+
}
17021705
}),
17031706
(_, Type::Dynamic(_)) => ConstraintSet::from(match relation {
17041707
TypeRelation::Subtyping => false,
@@ -1710,6 +1713,9 @@ impl<'db> Type<'db> {
17101713
}
17111714
_ => false,
17121715
},
1716+
TypeRelation::ConstraintImplication(_) => {
1717+
panic!("constraints should not contain dynamic types")
1718+
}
17131719
}),
17141720

17151721
// In general, a TypeVar `T` is not a subtype of a type `S` unless one of the two conditions is satisfied:
@@ -1894,12 +1900,16 @@ impl<'db> Type<'db> {
18941900
// to non-transitivity (highly undesirable); and pragmatically, a full implementation
18951901
// of redundancy may not generally lead to simpler types in many situations.
18961902
let self_ty = match relation {
1897-
TypeRelation::Subtyping | TypeRelation::Redundancy => self,
1903+
TypeRelation::Subtyping
1904+
| TypeRelation::Redundancy
1905+
| TypeRelation::ConstraintImplication(_) => self,
18981906
TypeRelation::Assignability => self.bottom_materialization(db),
18991907
};
19001908
intersection.negative(db).iter().when_all(db, |&neg_ty| {
19011909
let neg_ty = match relation {
1902-
TypeRelation::Subtyping | TypeRelation::Redundancy => neg_ty,
1910+
TypeRelation::Subtyping
1911+
| TypeRelation::Redundancy
1912+
| TypeRelation::ConstraintImplication(_) => neg_ty,
19031913
TypeRelation::Assignability => neg_ty.bottom_materialization(db),
19041914
};
19051915
self_ty.is_disjoint_from_impl(
@@ -9715,7 +9725,7 @@ impl<'db> ConstructorCallError<'db> {
97159725

97169726
/// A non-exhaustive enumeration of relations that can exist between types.
97179727
#[derive(Debug, Copy, Clone, Hash, PartialEq, Eq)]
9718-
pub(crate) enum TypeRelation {
9728+
pub(crate) enum TypeRelation<'db> {
97199729
/// The "subtyping" relation.
97209730
///
97219731
/// A [fully static] type `B` is a subtype of a fully static type `A` if and only if
@@ -9835,9 +9845,45 @@ pub(crate) enum TypeRelation {
98359845
/// [fully static]: https://typing.python.org/en/latest/spec/glossary.html#term-fully-static-type
98369846
/// [materializations]: https://typing.python.org/en/latest/spec/glossary.html#term-materialize
98379847
Redundancy,
9848+
9849+
/// The "constraint implication" relation.
9850+
///
9851+
/// This is used when solving a [`ConstraintSet`], which expresses the conditions under which
9852+
/// one of the other relationships hold. There are three cases to consider: dynamic types,
9853+
/// concrete types, and typevars.
9854+
///
9855+
/// The first case is easy; we can ignore it! The upper and lower bounds of a constraint are
9856+
/// guaranteed to be fully static, so we don't have to define constraint implication for
9857+
/// dynamic types.
9858+
///
9859+
/// For concrete types, all of the typing relationships (including this one) produce the same
9860+
/// result. (A concrete type is any fully static type that is not a typevar. It can _contain_ a
9861+
/// typevar, though — `list[T]` is considered concrete.)
9862+
///
9863+
/// The interesting case is typevars. The other typing relationships all "punt" on the question
9864+
/// when considering a typevar, by translating the desired relationship into a constraint set.
9865+
/// That is, we say that `T` is assignable to `int` when `T ≤ int`. (There are interesting
9866+
/// nuances when comparing a typevar with a dynamic type, but they all end up translating the
9867+
/// types into a constraint in some way.)
9868+
///
9869+
/// At some point, though, we need to resolve a constraint set; at that point, we can no longer
9870+
/// punt on the question, and have to determine whether a runtime instance being a member of
9871+
/// `T` implies that it is also a member of `int`. This will depend on the final constraint set
9872+
/// that we produced, which might incorporate tighter constraints from other parts of a larger
9873+
/// type. For instance, `S` implies `int` under each of the following constraint sets:
9874+
///
9875+
/// - `S ≤ bool`
9876+
/// - `S ≤ int`
9877+
/// - `S ≤ T ∧ T ≤ int`
9878+
///
9879+
/// Whereas `S` does not imply `int` under the following:
9880+
///
9881+
/// - `S ≤ str`
9882+
/// - `S ≤ T`
9883+
ConstraintImplication(ConstraintSet<'db>),
98389884
}
98399885

9840-
impl TypeRelation {
9886+
impl TypeRelation<'_> {
98419887
pub(crate) const fn is_assignability(self) -> bool {
98429888
matches!(self, TypeRelation::Assignability)
98439889
}
@@ -10005,7 +10051,7 @@ impl<'db> BoundMethodType<'db> {
1000510051
db: &'db dyn Db,
1000610052
other: Self,
1000710053
inferable: InferableTypeVars<'_, 'db>,
10008-
relation: TypeRelation,
10054+
relation: TypeRelation<'db>,
1000910055
relation_visitor: &HasRelationToVisitor<'db>,
1001010056
disjointness_visitor: &IsDisjointVisitor<'db>,
1001110057
) -> ConstraintSet<'db> {
@@ -10172,7 +10218,7 @@ impl<'db> CallableType<'db> {
1017210218
db: &'db dyn Db,
1017310219
other: Self,
1017410220
inferable: InferableTypeVars<'_, 'db>,
10175-
relation: TypeRelation,
10221+
relation: TypeRelation<'db>,
1017610222
relation_visitor: &HasRelationToVisitor<'db>,
1017710223
disjointness_visitor: &IsDisjointVisitor<'db>,
1017810224
) -> ConstraintSet<'db> {
@@ -10267,7 +10313,7 @@ impl<'db> KnownBoundMethodType<'db> {
1026710313
db: &'db dyn Db,
1026810314
other: Self,
1026910315
inferable: InferableTypeVars<'_, 'db>,
10270-
relation: TypeRelation,
10316+
relation: TypeRelation<'db>,
1027110317
relation_visitor: &HasRelationToVisitor<'db>,
1027210318
disjointness_visitor: &IsDisjointVisitor<'db>,
1027310319
) -> ConstraintSet<'db> {

crates/ty_python_semantic/src/types/class.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -596,14 +596,16 @@ impl<'db> ClassType<'db> {
596596
db: &'db dyn Db,
597597
other: Self,
598598
inferable: InferableTypeVars<'_, 'db>,
599-
relation: TypeRelation,
599+
relation: TypeRelation<'db>,
600600
relation_visitor: &HasRelationToVisitor<'db>,
601601
disjointness_visitor: &IsDisjointVisitor<'db>,
602602
) -> ConstraintSet<'db> {
603603
self.iter_mro(db).when_any(db, |base| {
604604
match base {
605605
ClassBase::Dynamic(_) => match relation {
606-
TypeRelation::Subtyping | TypeRelation::Redundancy => {
606+
TypeRelation::Subtyping
607+
| TypeRelation::Redundancy
608+
| TypeRelation::ConstraintImplication(_) => {
607609
ConstraintSet::from(other.is_object(db))
608610
}
609611
TypeRelation::Assignability => ConstraintSet::from(!other.is_final(db)),

crates/ty_python_semantic/src/types/constraints.rs

Lines changed: 57 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,8 @@ use rustc_hash::FxHashSet;
6464
use salsa::plumbing::AsId;
6565

6666
use crate::Db;
67-
use crate::types::{BoundTypeVarIdentity, IntersectionType, Type, UnionType};
67+
use crate::types::generics::InferableTypeVars;
68+
use crate::types::{BoundTypeVarIdentity, IntersectionType, Type, TypeRelation, UnionType};
6869

6970
/// An extension trait for building constraint sets from [`Option`] values.
7071
pub(crate) trait OptionConstraintsExtension<T> {
@@ -185,6 +186,22 @@ impl<'db> ConstraintSet<'db> {
185186
self.node.is_always_satisfied()
186187
}
187188

189+
fn type_implies(
190+
self,
191+
db: &'db dyn Db,
192+
lhs: Type<'db>,
193+
rhs: Type<'db>,
194+
inferable: InferableTypeVars<'_, 'db>,
195+
) -> bool {
196+
lhs.has_relation_to(
197+
db,
198+
rhs,
199+
inferable,
200+
TypeRelation::ConstraintImplication(self),
201+
)
202+
.is_always_satisfied()
203+
}
204+
188205
/// Updates this constraint set to hold the union of itself and another constraint set.
189206
pub(crate) fn union(&mut self, db: &'db dyn Db, other: Self) -> Self {
190207
self.node = self.node.or(db, other.node);
@@ -247,7 +264,7 @@ impl<'db> ConstraintSet<'db> {
247264
}
248265

249266
pub(crate) fn display(self, db: &'db dyn Db) -> impl Display {
250-
self.node.display(db)
267+
self.node.display(db, self)
251268
}
252269
}
253270

@@ -321,20 +338,34 @@ impl<'db> ConstrainedTypeVar<'db> {
321338
///
322339
/// This is used (among other places) to simplify how we display constraint sets, by removing
323340
/// redundant constraints from a clause.
324-
fn implies(self, db: &'db dyn Db, other: Self) -> bool {
325-
other.contains(db, self)
341+
fn implies(self, db: &'db dyn Db, other: Self, constraints: ConstraintSet<'db>) -> bool {
342+
if self.typevar(db) != other.typevar(db) {
343+
return false;
344+
}
345+
constraints.type_implies(db, other.lower(db), self.lower(db), InferableTypeVars::None)
346+
&& constraints.type_implies(
347+
db,
348+
self.upper(db),
349+
other.upper(db),
350+
InferableTypeVars::None,
351+
)
326352
}
327353

328354
/// Returns the intersection of two range constraints, or `None` if the intersection is empty.
329-
fn intersect(self, db: &'db dyn Db, other: Self) -> Option<Self> {
355+
fn intersect(
356+
self,
357+
db: &'db dyn Db,
358+
other: Self,
359+
constraints: ConstraintSet<'db>,
360+
) -> Option<Self> {
330361
// (s₁ ≤ α ≤ t₁) ∧ (s₂ ≤ α ≤ t₂) = (s₁ ∪ s₂) ≤ α ≤ (t₁ ∩ t₂))
331362
let lower = UnionType::from_elements(db, [self.lower(db), other.lower(db)]).normalized(db);
332363
let upper =
333364
IntersectionType::from_elements(db, [self.upper(db), other.upper(db)]).normalized(db);
334365

335366
// If `lower ≰ upper`, then the intersection is empty, since there is no type that is both
336367
// greater than `lower`, and less than `upper`.
337-
if !lower.is_subtype_of(db, upper) {
368+
if !constraints.type_implies(db, lower, upper, InferableTypeVars::None) {
338369
return None;
339370
}
340371

@@ -755,10 +786,10 @@ impl<'db> Node<'db> {
755786
}
756787

757788
/// Simplifies a BDD, replacing constraints with simpler or smaller constraints where possible.
758-
fn simplify(self, db: &'db dyn Db) -> Self {
789+
fn simplify(self, db: &'db dyn Db, constraints: ConstraintSet<'db>) -> Self {
759790
match self {
760791
Node::AlwaysTrue | Node::AlwaysFalse => self,
761-
Node::Interior(interior) => interior.simplify(db),
792+
Node::Interior(interior) => interior.simplify(db, constraints),
762793
}
763794
}
764795

@@ -796,14 +827,15 @@ impl<'db> Node<'db> {
796827
searcher.clauses
797828
}
798829

799-
fn display(self, db: &'db dyn Db) -> impl Display {
830+
fn display(self, db: &'db dyn Db, constraints: ConstraintSet<'db>) -> impl Display {
800831
// To render a BDD in DNF form, you perform a depth-first search of the BDD tree, looking
801832
// for any path that leads to the AlwaysTrue terminal. Each such path represents one of the
802833
// intersection clauses in the DNF form. The path traverses zero or more interior nodes,
803834
// and takes either the true or false edge from each one. That gives you the positive or
804835
// negative individual constraints in the path's clause.
805836
struct DisplayNode<'db> {
806837
node: Node<'db>,
838+
constraints: ConstraintSet<'db>,
807839
db: &'db dyn Db,
808840
}
809841

@@ -814,15 +846,16 @@ impl<'db> Node<'db> {
814846
Node::AlwaysFalse => f.write_str("never"),
815847
Node::Interior(_) => {
816848
let mut clauses = self.node.satisfied_clauses(self.db);
817-
clauses.simplify(self.db);
849+
clauses.simplify(self.db, self.constraints);
818850
clauses.display(self.db).fmt(f)
819851
}
820852
}
821853
}
822854
}
823855

824856
DisplayNode {
825-
node: self.simplify(db),
857+
node: self.simplify(db, constraints),
858+
constraints,
826859
db,
827860
}
828861
}
@@ -1033,7 +1066,7 @@ impl<'db> InteriorNode<'db> {
10331066
}
10341067

10351068
#[salsa::tracked(heap_size=ruff_memory_usage::heap_size)]
1036-
fn simplify(self, db: &'db dyn Db) -> Node<'db> {
1069+
fn simplify(self, db: &'db dyn Db, constraints: ConstraintSet<'db>) -> Node<'db> {
10371070
// To simplify a non-terminal BDD, we find all pairs of constraints that are mentioned in
10381071
// the BDD. If any of those pairs can be simplified to some other BDD, we perform a
10391072
// substitution to replace the pair with the simplification.
@@ -1117,7 +1150,7 @@ impl<'db> InteriorNode<'db> {
11171150
// There are some simplifications we can make when the intersection of the two
11181151
// constraints is empty, and others that we can make when the intersection is
11191152
// non-empty.
1120-
match left_constraint.intersect(db, right_constraint) {
1153+
match left_constraint.intersect(db, right_constraint, constraints) {
11211154
Some(intersection_constraint) => {
11221155
// If the intersection is non-empty, we need to create a new constraint to
11231156
// represent that intersection. We also need to add the new constraint to our
@@ -1274,7 +1307,7 @@ impl<'db> ConstraintAssignment<'db> {
12741307
///
12751308
/// This is used (among other places) to simplify how we display constraint sets, by removing
12761309
/// redundant constraints from a clause.
1277-
fn implies(self, db: &'db dyn Db, other: Self) -> bool {
1310+
fn implies(self, db: &'db dyn Db, other: Self, constraints: ConstraintSet<'db>) -> bool {
12781311
match (self, other) {
12791312
// For two positive constraints, one range has to fully contain the other; the smaller
12801313
// constraint implies the larger.
@@ -1284,7 +1317,7 @@ impl<'db> ConstraintAssignment<'db> {
12841317
(
12851318
ConstraintAssignment::Positive(self_constraint),
12861319
ConstraintAssignment::Positive(other_constraint),
1287-
) => self_constraint.implies(db, other_constraint),
1320+
) => self_constraint.implies(db, other_constraint, constraints),
12881321

12891322
// For two negative constraints, one range has to fully contain the other; the ranges
12901323
// represent "holes", though, so the constraint with the larger range implies the one
@@ -1295,7 +1328,7 @@ impl<'db> ConstraintAssignment<'db> {
12951328
(
12961329
ConstraintAssignment::Negative(self_constraint),
12971330
ConstraintAssignment::Negative(other_constraint),
1298-
) => other_constraint.implies(db, self_constraint),
1331+
) => other_constraint.implies(db, self_constraint, constraints),
12991332

13001333
// For a positive and negative constraint, the ranges have to be disjoint, and the
13011334
// positive range implies the negative range.
@@ -1305,7 +1338,9 @@ impl<'db> ConstraintAssignment<'db> {
13051338
(
13061339
ConstraintAssignment::Positive(self_constraint),
13071340
ConstraintAssignment::Negative(other_constraint),
1308-
) => self_constraint.intersect(db, other_constraint).is_none(),
1341+
) => self_constraint
1342+
.intersect(db, other_constraint, constraints)
1343+
.is_none(),
13091344

13101345
// It's theoretically possible for a negative constraint to imply a positive constraint
13111346
// if the positive constraint is always satisfied (`Never ≤ T ≤ object`). But we never
@@ -1391,15 +1426,15 @@ impl<'db> SatisfiedClause<'db> {
13911426
/// want to remove the larger one and keep the smaller one.)
13921427
///
13931428
/// Returns a boolean that indicates whether any simplifications were made.
1394-
fn simplify(&mut self, db: &'db dyn Db) -> bool {
1429+
fn simplify(&mut self, db: &'db dyn Db, constraints: ConstraintSet<'db>) -> bool {
13951430
let mut changes_made = false;
13961431
let mut i = 0;
13971432
// Loop through each constraint, comparing it with any constraints that appear later in the
13981433
// list.
13991434
'outer: while i < self.constraints.len() {
14001435
let mut j = i + 1;
14011436
while j < self.constraints.len() {
1402-
if self.constraints[j].implies(db, self.constraints[i]) {
1437+
if self.constraints[j].implies(db, self.constraints[i], constraints) {
14031438
// If constraint `i` is removed, then we don't need to compare it with any
14041439
// later constraints in the list. Note that we continue the outer loop, instead
14051440
// of breaking from the inner loop, so that we don't bump index `i` below.
@@ -1408,7 +1443,7 @@ impl<'db> SatisfiedClause<'db> {
14081443
self.constraints.swap_remove(i);
14091444
changes_made = true;
14101445
continue 'outer;
1411-
} else if self.constraints[i].implies(db, self.constraints[j]) {
1446+
} else if self.constraints[i].implies(db, self.constraints[j], constraints) {
14121447
// If constraint `j` is removed, then we can continue the inner loop. We will
14131448
// swap a new element into place at index `j`, and will continue comparing the
14141449
// constraint at index `i` with later constraints.
@@ -1472,11 +1507,11 @@ impl<'db> SatisfiedClauses<'db> {
14721507
/// Simplifies the DNF representation, removing redundancies that do not change the underlying
14731508
/// function. (This is used when displaying a BDD, to make sure that the representation that we
14741509
/// show is as simple as possible while still producing the same results.)
1475-
fn simplify(&mut self, db: &'db dyn Db) {
1510+
fn simplify(&mut self, db: &'db dyn Db, constraints: ConstraintSet<'db>) {
14761511
// First simplify each clause individually, by removing constraints that are implied by
14771512
// other constraints in the clause.
14781513
for clause in &mut self.clauses {
1479-
clause.simplify(db);
1514+
clause.simplify(db, constraints);
14801515
}
14811516

14821517
while self.simplify_one_round() {

0 commit comments

Comments
 (0)