[ty] Change to BDD representation for constraint sets (#20533)
Some checks are pending
CI / Determine changes (push) Waiting to run
CI / cargo fmt (push) Waiting to run
CI / cargo clippy (push) Blocked by required conditions
CI / cargo test (linux) (push) Blocked by required conditions
CI / cargo test (linux, release) (push) Blocked by required conditions
CI / cargo test (windows) (push) Blocked by required conditions
CI / cargo test (wasm) (push) Blocked by required conditions
CI / cargo build (release) (push) Waiting to run
CI / cargo build (msrv) (push) Blocked by required conditions
CI / cargo fuzz build (push) Blocked by required conditions
CI / fuzz parser (push) Blocked by required conditions
CI / test scripts (push) Blocked by required conditions
CI / ecosystem (push) Blocked by required conditions
CI / Fuzz for new ty panics (push) Blocked by required conditions
CI / cargo shear (push) Blocked by required conditions
CI / python package (push) Waiting to run
CI / pre-commit (push) Waiting to run
CI / mkdocs (push) Waiting to run
CI / formatter instabilities and black similarity (push) Blocked by required conditions
CI / test ruff-lsp (push) Blocked by required conditions
CI / check playground (push) Blocked by required conditions
CI / benchmarks instrumented (ruff) (push) Blocked by required conditions
CI / benchmarks instrumented (ty) (push) Blocked by required conditions
CI / benchmarks-walltime (push) Blocked by required conditions
[ty Playground] Release / publish (push) Waiting to run

While working on #20093, I kept running into test failures due to
constraint sets not simplifying as much as they could, and therefore not
being easily testable against "always true" and "always false".

This PR updates our constraint set representation to use BDDs. Because
BDDs are reduced and ordered, they are canonical — equivalent boolean
formulas are represented by the same interned BDD node.

That said, there is a wrinkle, in that the "variables" that we use in
these BDDs — the individual constraints like `Lower ≤ T ≤ Upper` are not
always independent of each other.

As an example, given types `A ≤ B ≤ C ≤ D` and a typevar `T`, the
constraints `A ≤ T ≤ C` and `B ≤ T ≤ D` "overlap" — their intersection
is non-empty. So we should be able to simplify

```
(A ≤ T ≤ C) ∧ (B ≤ T ≤ D) == (B ≤ T ≤ C)
```

That's not a simplification that the BDD structure can perform itself,
since those three constraints are modeled as separate BDD variables, and
are therefore "opaque" to the BDD algorithms.

That means we need to perform this kind of simplification ourselves. We
look at pairs of constraints that appear in a BDD and see if they can be
simplified relative to each other, and if so, replace the pair with the
simplification. A large part of the toil of getting this PR to work was
identifying all of those patterns and getting that substitution logic
correct.

With this new representation, all existing tests pass, as well as some
new ones that represent test failures that were occuring on #20093.

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
This commit is contained in:
Douglas Creager 2025-09-25 21:55:35 -04:00 committed by GitHub
parent e66a872c14
commit 02ebb2ee61
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 1100 additions and 1220 deletions

View file

@ -305,9 +305,9 @@ range.
```py ```py
def _[T]() -> None: def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[((SubSub ≤ T@_ ≤ Base) ∧ ¬(Sub ≤ T@_ ≤ Base))] # revealed: ty_extensions.ConstraintSet[(¬(Sub ≤ T@_ ≤ Base) ∧ (SubSub ≤ T@_ ≤ Base))]
reveal_type(range_constraint(SubSub, T, Base) & negated_range_constraint(Sub, T, Super)) reveal_type(range_constraint(SubSub, T, Base) & negated_range_constraint(Sub, T, Super))
# revealed: ty_extensions.ConstraintSet[((SubSub ≤ T@_ ≤ Super) ∧ ¬(Sub ≤ T@_ ≤ Base))] # revealed: ty_extensions.ConstraintSet[(¬(Sub ≤ T@_ ≤ Base) ∧ (SubSub ≤ T@_ ≤ Super))]
reveal_type(range_constraint(SubSub, T, Super) & negated_range_constraint(Sub, T, Base)) reveal_type(range_constraint(SubSub, T, Super) & negated_range_constraint(Sub, T, Base))
``` ```
@ -339,9 +339,9 @@ Otherwise, the union cannot be simplified.
```py ```py
def _[T]() -> None: def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[(¬(Sub ≤ T@_ ≤ Base) ∧ ¬(Base ≤ T@_ ≤ Super))] # revealed: ty_extensions.ConstraintSet[(¬(Base ≤ T@_ ≤ Super) ∧ ¬(Sub ≤ T@_ ≤ Base))]
reveal_type(negated_range_constraint(Sub, T, Base) & negated_range_constraint(Base, T, Super)) reveal_type(negated_range_constraint(Sub, T, Base) & negated_range_constraint(Base, T, Super))
# revealed: ty_extensions.ConstraintSet[(¬(SubSub ≤ T@_ ≤ Sub) ∧ ¬(Base ≤ T@_ ≤ Super))] # revealed: ty_extensions.ConstraintSet[(¬(Base ≤ T@_ ≤ Super) ∧ ¬(SubSub ≤ T@_ ≤ Sub))]
reveal_type(negated_range_constraint(SubSub, T, Sub) & negated_range_constraint(Base, T, Super)) reveal_type(negated_range_constraint(SubSub, T, Sub) & negated_range_constraint(Base, T, Super))
# revealed: ty_extensions.ConstraintSet[(¬(SubSub ≤ T@_ ≤ Sub) ∧ ¬(Unrelated ≤ T@_))] # revealed: ty_extensions.ConstraintSet[(¬(SubSub ≤ T@_ ≤ Sub) ∧ ¬(Unrelated ≤ T@_))]
reveal_type(negated_range_constraint(SubSub, T, Sub) & negated_range_constraint(Unrelated, T, object)) reveal_type(negated_range_constraint(SubSub, T, Sub) & negated_range_constraint(Unrelated, T, object))
@ -385,7 +385,7 @@ We cannot simplify the union of constraints that refer to different typevars.
def _[T, U]() -> None: def _[T, U]() -> None:
# revealed: ty_extensions.ConstraintSet[(Sub ≤ T@_ ≤ Base) (Sub ≤ U@_ ≤ Base)] # revealed: ty_extensions.ConstraintSet[(Sub ≤ T@_ ≤ Base) (Sub ≤ U@_ ≤ Base)]
reveal_type(range_constraint(Sub, T, Base) | range_constraint(Sub, U, Base)) reveal_type(range_constraint(Sub, T, Base) | range_constraint(Sub, U, Base))
# revealed: ty_extensions.ConstraintSet[¬(Sub ≤ T@_ ≤ Base) ¬(Sub ≤ U@_ ≤ Base)] # revealed: ty_extensions.ConstraintSet[¬(Sub ≤ U@_ ≤ Base) ¬(Sub ≤ T@_ ≤ Base)]
reveal_type(negated_range_constraint(Sub, T, Base) | negated_range_constraint(Sub, U, Base)) reveal_type(negated_range_constraint(Sub, T, Base) | negated_range_constraint(Sub, U, Base))
``` ```
@ -417,9 +417,9 @@ Otherwise, the union cannot be simplified.
```py ```py
def _[T]() -> None: def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[(Sub ≤ T@_ ≤ Base) (Base ≤ T@_ ≤ Super)] # revealed: ty_extensions.ConstraintSet[(Base ≤ T@_ ≤ Super) (Sub ≤ T@_ ≤ Base)]
reveal_type(range_constraint(Sub, T, Base) | range_constraint(Base, T, Super)) reveal_type(range_constraint(Sub, T, Base) | range_constraint(Base, T, Super))
# revealed: ty_extensions.ConstraintSet[(SubSub ≤ T@_ ≤ Sub) (Base ≤ T@_ ≤ Super)] # revealed: ty_extensions.ConstraintSet[(Base ≤ T@_ ≤ Super) (SubSub ≤ T@_ ≤ Sub)]
reveal_type(range_constraint(SubSub, T, Sub) | range_constraint(Base, T, Super)) reveal_type(range_constraint(SubSub, T, Sub) | range_constraint(Base, T, Super))
# revealed: ty_extensions.ConstraintSet[(SubSub ≤ T@_ ≤ Sub) (Unrelated ≤ T@_)] # revealed: ty_extensions.ConstraintSet[(SubSub ≤ T@_ ≤ Sub) (Unrelated ≤ T@_)]
reveal_type(range_constraint(SubSub, T, Sub) | range_constraint(Unrelated, T, object)) reveal_type(range_constraint(SubSub, T, Sub) | range_constraint(Unrelated, T, object))
@ -488,9 +488,9 @@ range.
```py ```py
def _[T]() -> None: def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[¬(SubSub ≤ T@_ ≤ Base) (Sub ≤ T@_ ≤ Base)] # revealed: ty_extensions.ConstraintSet[(Sub ≤ T@_ ≤ Base) ¬(SubSub ≤ T@_ ≤ Base)]
reveal_type(negated_range_constraint(SubSub, T, Base) | range_constraint(Sub, T, Super)) reveal_type(negated_range_constraint(SubSub, T, Base) | range_constraint(Sub, T, Super))
# revealed: ty_extensions.ConstraintSet[¬(SubSub ≤ T@_ ≤ Super) (Sub ≤ T@_ ≤ Base)] # revealed: ty_extensions.ConstraintSet[(Sub ≤ T@_ ≤ Base) ¬(SubSub ≤ T@_ ≤ Super)]
reveal_type(negated_range_constraint(SubSub, T, Super) | range_constraint(Sub, T, Base)) reveal_type(negated_range_constraint(SubSub, T, Super) | range_constraint(Sub, T, Base))
``` ```
@ -562,3 +562,42 @@ def _[T]() -> None:
# revealed: ty_extensions.ConstraintSet[always] # revealed: ty_extensions.ConstraintSet[always]
reveal_type(constraint | ~constraint) reveal_type(constraint | ~constraint)
``` ```
### Negation of constraints involving two variables
```py
from typing import final, Never
from ty_extensions import range_constraint
class Base: ...
@final
class Unrelated: ...
def _[T, U]() -> None:
# revealed: ty_extensions.ConstraintSet[¬(U@_ ≤ Base) ¬(T@_ ≤ Base)]
reveal_type(~(range_constraint(Never, T, Base) & range_constraint(Never, U, Base)))
```
The union of a constraint and its negation should always be satisfiable.
```py
def _[T, U]() -> None:
c1 = range_constraint(Never, T, Base) & range_constraint(Never, U, Base)
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(c1 | ~c1)
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(~c1 | c1)
c2 = range_constraint(Unrelated, T, object) & range_constraint(Unrelated, U, object)
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(c2 | ~c2)
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(~c2 | c2)
union = c1 | c2
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(union | ~union)
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(~union | union)
```

File diff suppressed because it is too large Load diff

View file

@ -877,7 +877,7 @@ impl<'db> Specialization<'db> {
} }
TypeVarVariance::Bivariant => ConstraintSet::from(true), TypeVarVariance::Bivariant => ConstraintSet::from(true),
}; };
if result.intersect(db, &compatible).is_never_satisfied() { if result.intersect(db, compatible).is_never_satisfied() {
return result; return result;
} }
} }
@ -918,7 +918,7 @@ impl<'db> Specialization<'db> {
} }
TypeVarVariance::Bivariant => ConstraintSet::from(true), TypeVarVariance::Bivariant => ConstraintSet::from(true),
}; };
if result.intersect(db, &compatible).is_never_satisfied() { if result.intersect(db, compatible).is_never_satisfied() {
return result; return result;
} }
} }
@ -928,7 +928,7 @@ impl<'db> Specialization<'db> {
(None, None) => {} (None, None) => {}
(Some(self_tuple), Some(other_tuple)) => { (Some(self_tuple), Some(other_tuple)) => {
let compatible = self_tuple.is_equivalent_to_impl(db, other_tuple, visitor); let compatible = self_tuple.is_equivalent_to_impl(db, other_tuple, visitor);
if result.intersect(db, &compatible).is_never_satisfied() { if result.intersect(db, compatible).is_never_satisfied() {
return result; return result;
} }
} }

View file

@ -6947,7 +6947,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
ast::UnaryOp::Invert, ast::UnaryOp::Invert,
Type::KnownInstance(KnownInstanceType::ConstraintSet(constraints)), Type::KnownInstance(KnownInstanceType::ConstraintSet(constraints)),
) => { ) => {
let constraints = constraints.constraints(self.db()).clone(); let constraints = constraints.constraints(self.db());
let result = constraints.negate(self.db()); let result = constraints.negate(self.db());
Type::KnownInstance(KnownInstanceType::ConstraintSet(TrackedConstraintSet::new( Type::KnownInstance(KnownInstanceType::ConstraintSet(TrackedConstraintSet::new(
self.db(), self.db(),
@ -7311,9 +7311,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
Type::KnownInstance(KnownInstanceType::ConstraintSet(right)), Type::KnownInstance(KnownInstanceType::ConstraintSet(right)),
ast::Operator::BitAnd, ast::Operator::BitAnd,
) => { ) => {
let left = left.constraints(self.db()).clone(); let left = left.constraints(self.db());
let right = right.constraints(self.db()).clone(); let right = right.constraints(self.db());
let result = left.and(self.db(), || right); let result = left.and(self.db(), || *right);
Some(Type::KnownInstance(KnownInstanceType::ConstraintSet( Some(Type::KnownInstance(KnownInstanceType::ConstraintSet(
TrackedConstraintSet::new(self.db(), result), TrackedConstraintSet::new(self.db(), result),
))) )))
@ -7324,9 +7324,9 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
Type::KnownInstance(KnownInstanceType::ConstraintSet(right)), Type::KnownInstance(KnownInstanceType::ConstraintSet(right)),
ast::Operator::BitOr, ast::Operator::BitOr,
) => { ) => {
let left = left.constraints(self.db()).clone(); let left = left.constraints(self.db());
let right = right.constraints(self.db()).clone(); let right = right.constraints(self.db());
let result = left.or(self.db(), || right); let result = left.or(self.db(), || *right);
Some(Type::KnownInstance(KnownInstanceType::ConstraintSet( Some(Type::KnownInstance(KnownInstanceType::ConstraintSet(
TrackedConstraintSet::new(self.db(), result), TrackedConstraintSet::new(self.db(), result),
))) )))

View file

@ -551,10 +551,7 @@ impl<'db> Signature<'db> {
let self_type = self_type.unwrap_or(Type::unknown()); let self_type = self_type.unwrap_or(Type::unknown());
let other_type = other_type.unwrap_or(Type::unknown()); let other_type = other_type.unwrap_or(Type::unknown());
!result !result
.intersect( .intersect(db, self_type.is_equivalent_to_impl(db, other_type, visitor))
db,
&self_type.is_equivalent_to_impl(db, other_type, visitor),
)
.is_never_satisfied() .is_never_satisfied()
}; };
@ -699,10 +696,7 @@ impl<'db> Signature<'db> {
let type1 = type1.unwrap_or(Type::unknown()); let type1 = type1.unwrap_or(Type::unknown());
let type2 = type2.unwrap_or(Type::unknown()); let type2 = type2.unwrap_or(Type::unknown());
!result !result
.intersect( .intersect(db, type1.has_relation_to_impl(db, type2, relation, visitor))
db,
&type1.has_relation_to_impl(db, type2, relation, visitor),
)
.is_never_satisfied() .is_never_satisfied()
}; };

View file

@ -439,7 +439,7 @@ impl<'db> FixedLengthTuple<Type<'db>> {
let element_constraints = let element_constraints =
self_ty.has_relation_to_impl(db, *other_ty, relation, visitor); self_ty.has_relation_to_impl(db, *other_ty, relation, visitor);
if result if result
.intersect(db, &element_constraints) .intersect(db, element_constraints)
.is_never_satisfied() .is_never_satisfied()
{ {
return result; return result;
@ -452,7 +452,7 @@ impl<'db> FixedLengthTuple<Type<'db>> {
let element_constraints = let element_constraints =
self_ty.has_relation_to_impl(db, *other_ty, relation, visitor); self_ty.has_relation_to_impl(db, *other_ty, relation, visitor);
if result if result
.intersect(db, &element_constraints) .intersect(db, element_constraints)
.is_never_satisfied() .is_never_satisfied()
{ {
return result; return result;
@ -774,7 +774,7 @@ impl<'db> VariableLengthTuple<Type<'db>> {
let element_constraints = let element_constraints =
self_ty.has_relation_to_impl(db, other_ty, relation, visitor); self_ty.has_relation_to_impl(db, other_ty, relation, visitor);
if result if result
.intersect(db, &element_constraints) .intersect(db, element_constraints)
.is_never_satisfied() .is_never_satisfied()
{ {
return result; return result;
@ -788,7 +788,7 @@ impl<'db> VariableLengthTuple<Type<'db>> {
let element_constraints = let element_constraints =
self_ty.has_relation_to_impl(db, other_ty, relation, visitor); self_ty.has_relation_to_impl(db, other_ty, relation, visitor);
if result if result
.intersect(db, &element_constraints) .intersect(db, element_constraints)
.is_never_satisfied() .is_never_satisfied()
{ {
return result; return result;
@ -832,7 +832,7 @@ impl<'db> VariableLengthTuple<Type<'db>> {
return ConstraintSet::from(false); return ConstraintSet::from(false);
} }
}; };
if result.intersect(db, &pair_constraints).is_never_satisfied() { if result.intersect(db, pair_constraints).is_never_satisfied() {
return result; return result;
} }
} }
@ -858,7 +858,7 @@ impl<'db> VariableLengthTuple<Type<'db>> {
return ConstraintSet::from(false); return ConstraintSet::from(false);
} }
}; };
if result.intersect(db, &pair_constraints).is_never_satisfied() { if result.intersect(db, pair_constraints).is_never_satisfied() {
return result; return result;
} }
} }