[ty] Some more simplifications when rendering constraint sets (#21009)

This PR adds another useful simplification when rendering constraint
sets: `T = int` instead of `T = int ∧ T ≠ str`. (The "smaller"
constraint `T = int` implies the "larger" constraint `T ≠ str`.
Constraint set clauses are intersections, and if one constraint in a
clause implies another, we can throw away the "larger" constraint.)

While we're here, we also normalize the bounds of a constraint, so that
we equate e.g. `T ≤ int | str` with `T ≤ str | int`, and change the
ordering of BDD variables so that all constraints with the same typevar
are ordered adjacent to each other.

Lastly, we also add a new `display_graph` helper method that prints out
the full graph structure of a BDD.

---------

Co-authored-by: Alex Waygood <Alex.Waygood@Gmail.com>
This commit is contained in:
Douglas Creager 2025-10-22 13:38:44 -04:00 committed by GitHub
parent 81c1d36088
commit 766ed5b5f3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 314 additions and 23 deletions

View file

@ -601,3 +601,40 @@ def _[T, U]() -> None:
# revealed: ty_extensions.ConstraintSet[always]
reveal_type(~union | union)
```
## Other simplifications
When displaying a constraint set, we transform the internal BDD representation into a DNF formula
(i.e., the logical OR of several clauses, each of which is the logical AND of several constraints).
This section contains several examples that show that we simplify the DNF formula as much as we can
before displaying it.
```py
from ty_extensions import range_constraint
def f[T, U]():
t1 = range_constraint(str, T, str)
t2 = range_constraint(bool, T, bool)
u1 = range_constraint(str, U, str)
u2 = range_constraint(bool, U, bool)
# revealed: ty_extensions.ConstraintSet[(T@f = bool) (T@f = str)]
reveal_type(t1 | t2)
# revealed: ty_extensions.ConstraintSet[(U@f = bool) (U@f = str)]
reveal_type(u1 | u2)
# revealed: ty_extensions.ConstraintSet[((T@f = bool) ∧ (U@f = bool)) ((T@f = bool) ∧ (U@f = str)) ((T@f = str) ∧ (U@f = bool)) ((T@f = str) ∧ (U@f = str))]
reveal_type((t1 | t2) & (u1 | u2))
```
The lower and upper bounds of a constraint are normalized, so that we equate unions and
intersections whose elements appear in different orders.
```py
from typing import Never
def f[T]():
# revealed: ty_extensions.ConstraintSet[(T@f ≤ int | str)]
reveal_type(range_constraint(Never, T, str | int))
# revealed: ty_extensions.ConstraintSet[(T@f ≤ int | str)]
reveal_type(range_constraint(Never, T, int | str))
```