Skip to content

Conversation

@dcreager
Copy link
Member

@dcreager dcreager commented Aug 8, 2025

"Why would you do this? This looks like you just replaced bool with an overly complex trait"

Yes that's correct!

This should be a no-op refactoring. It replaces all of the logic in our assignability, subtyping, equivalence, and disjointness methods to work over an arbitrary Constraints trait instead of only working on bool.

The methods that Constraints provides looks very much like what we get from bool. But soon we will add a new impl of this trait, and some new methods, that let us express "fuzzy" constraints that aren't always true or false. (In particular, a constraint will express the upper and lower bounds of the allowed specializations of a typevar.)

Even once we have that, most of the operations that we perform on constraint sets will be the usual boolean operations, just on sets. (false becomes empty/never; true becomes universe/always; or becomes union; and becomes intersection; not becomes negation.) So it's helpful to have this separate PR to refactor how we invoke those operations without introducing the new functionality yet.

Note that we also have translations of Option::is_some_and and is_none_or, and of Iterator::any and all, and that the and, or, when_any, and when_all methods are meant to short-circuit, just like the corresponding boolean operations. For constraint sets, that depends on being able to implement the is_always and is_never trait methods.

@github-actions
Copy link
Contributor

github-actions bot commented Aug 8, 2025

Diagnostic diff on typing conformance tests

No changes detected when running ty on typing conformance tests ✅

@github-actions
Copy link
Contributor

github-actions bot commented Aug 8, 2025

mypy_primer results

No ecosystem changes detected ✅
No memory usage changes detected ✅

@codspeed-hq
Copy link

codspeed-hq bot commented Aug 8, 2025

CodSpeed WallTime Performance Report

Merging #19838 will not alter performance

Comparing dcreager/relation-with-constraints (f10babf) with main (045cba3)

Summary

✅ 8 untouched benchmarks

@AlexWaygood AlexWaygood added the ty Multi-file analysis & type inference label Aug 8, 2025
Base automatically changed from dcreager/inferrable to main August 16, 2025 22:25
@dcreager dcreager force-pushed the dcreager/relation-with-constraints branch 2 times, most recently from 53077f3 to efabd55 Compare August 18, 2025 15:10
@dcreager dcreager force-pushed the dcreager/relation-with-constraints branch from efabd55 to 737c8e9 Compare August 19, 2025 15:14
@dcreager dcreager force-pushed the dcreager/relation-with-constraints branch from 737c8e9 to 1e5c45d Compare August 19, 2025 16:07
@dcreager dcreager marked this pull request as ready for review August 19, 2025 18:38
@dcreager dcreager changed the title [ty] WIP: Perform assignability etc checks using new Constraints trait [ty] Perform assignability etc checks using new Constraints trait Aug 19, 2025
Copy link
Contributor

@carljm carljm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is quite cool! Very impressed with how you wrangled the Rust API to make the translation fairly mechanical in many cases, even though you're adding significant capability.

Comment on lines +1458 to +1459
// not false). Once we're using real constraint sets instead of bool, we should be
// able to simplify the typevar logic.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm curious how you think the logic can be simplified "once we're using real constraint sets"? To me it appears kind of the opposite -- this duplication is here precisely because we are trying to be correct in a future with real constraint sets, where we need to return the actual accumulated constraints from the inner has_relation_to_impl checks. In this PR, with just bool, we can pass all tests without this duplication, by just changing the sense of the match arm if clause back to a positive .is_always(db) instead of a negated .is_never(db), and making the body of the match arm be C::always(db).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Considering this case is actually making me wonder about a more fundamental thing: what about a case where this match arm returns some constraints (neither always nor never), but some later match arm would also have returned some constraints, had we reached it? In an ideal world, wouldn't we OR those constraint sets, if we're trying to express the conditions under which the relation would be true?

I guess in the real world, we just pick one of them and don't find all solutions?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

My thinking is not that the stuff inside the match arm would change all that much. The part that I find too complex in its current state is that so much depends on the order of the match arms. My hunch is that the fall-through here won't be needed, and the ordering will no longer matter as much, because we'll be able to better combine the results of each recursive call with the other surrounding constraints.

/// Encodes the constraints under which a type property (e.g. assignability) holds.
pub(crate) trait Constraints<'db>: Clone + Sized {
/// Returns a constraint set that never holds
fn never(db: &'db dyn Db) -> Self;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's the obvious name tension with Type::Never here, but I'm not sure I have a better name to offer.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unsatisfiable or never_satisfiable, but that's longer to type 😄

Copy link
Member

@AlexWaygood AlexWaygood Aug 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

unsatisfiable or never_satisfiable, but that's longer to type 😄

I sort-of prefer those, actually! unsatisfiable doesn't seem too verbose to me. Not a big deal though.

Copy link
Member Author

@dcreager dcreager Aug 21, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No problem, it's just a couple of sed commands to make the change!

For the predicates, I've gone with is_{always,never}_satisfied. For the constructors, I want them to be consistent, so I went with unsatisfied/always_satisfied.

* main: (29 commits)
  [ty] add docstrings to completions based on type (#20008)
  [`pyupgrade`] Avoid reporting `__future__` features as unnecessary when they are used (`UP010`) (#19769)
  [`flake8-use-pathlib`] Add fixes for `PTH102` and `PTH103` (#19514)
  [ty] correctly ignore field specifiers when not specified (#20002)
  `Option::unwrap` is now const (#20007)
  [ty] Re-arrange "list modules" implementation for Salsa caching
  [ty] Test "list modules" versus "resolve module" in every mdtest
  [ty] Wire up "list modules" API to make module completions work
  [ty] Tweak some completion tests
  [ty] Add "list modules" implementation
  [ty] Lightly expose `FileModule` and `NamespacePackage` fields
  [ty] Add some more helper routines to `ModulePath`
  [ty] Fix a bug when converting `ModulePath` to `ModuleName`
  [ty] Split out another constructor for `ModuleName`
  [ty] Add stub-file tests to existing module resolver
  [ty] Expose some routines in the module resolver
  [ty] Add more path helper functions
  [`flake8-annotations`] Remove unused import in example (`ANN401`) (#20000)
  [ty] distinguish base conda from child conda (#19990)
  [ty] Fix server hang (#19991)
  ...
Copy link
Contributor

@carljm carljm left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great!

dcreager and others added 2 commits August 21, 2025 07:39
* main:
  [ty] Use `dedent` in cursor tests (#20019)
  Fix rust feature activation (#20012)
  [ty] Avoid unnecessary argument type expansion (#19999)
  [ty] Add link for namespaces being partial (#20015)
@dcreager dcreager merged commit 14fe122 into main Aug 21, 2025
38 checks passed
@dcreager dcreager deleted the dcreager/relation-with-constraints branch August 21, 2025 13:30
dcreager added a commit that referenced this pull request Aug 29, 2025
This PR adds an implementation of constraint sets.

An individual constraint restricts the specialization of a single
typevar to be within a particular lower and upper bound: the typevar can
only specialize to types that are a supertype of the lower bound, and a
subtype of the upper bound. (Note that lower and upper bounds are fully
static; we take the bottom and top materializations of the bounds to
remove any gradual forms if needed.) Either bound can be “closed” (where
the bound is a valid specialization), or “open” (where it is not).

You can then build up more complex constraint sets using union,
intersection, and negation operations. We use a disjunctive normal form
(DNF) representation, just like we do for types: a _constraint set_ is
the union of zero or more _clauses_, each of which is the intersection
of zero or more individual constraints. Note that the constraint set
that contains no clauses is never satisfiable (`⋃ {} = 0`); and the
constraint set that contains a single clause, which contains no
constraints, is always satisfiable (`⋃ {⋂ {}} = 1`).

One thing to note is that this PR does not change the logic of the
actual assignability checks, and in particular, we still aren't ever
trying to create an "individual constraint" that constrains a typevar.
Technically we're still operating only on `bool`s, since we only ever
instantiate `C::always_satisfiable` (i.e., `true`) and
`C::unsatisfiable` (i.e., `false`) in the `has_relation_to` methods. So
if you thought that #19838 introduced an unnecessarily complex stand-in
for `bool`, well here you go, this one is worse! (But still seemingly
not yielding a performance regression!) The next PR in this series,
#20093, is where we will actually create some non-trivial constraint
sets and use them in anger.

That said, the PR does go ahead and update the assignability checks to
use the new `ConstraintSet` type instead of `bool`. That part is fairly
straightforward since we had already updated the assignability checks to
use the `Constraints` trait; we just have to actively choose a different
impl type. (For the `is_whatever` variants, which still return a `bool`,
we have to convert the constraint set, but the explicit
`is_always_satisfiable` calls serve as nice documentation of our
intent.)

---------

Co-authored-by: Alex Waygood <[email protected]>
Co-authored-by: Carl Meyer <[email protected]>
second-ed pushed a commit to second-ed/ruff that referenced this pull request Sep 9, 2025
…stral-sh#19838)

"Why would you do this? This looks like you just replaced `bool` with an
overly complex trait"

Yes that's correct!

This should be a no-op refactoring. It replaces all of the logic in our
assignability, subtyping, equivalence, and disjointness methods to work
over an arbitrary `Constraints` trait instead of only working on `bool`.

The methods that `Constraints` provides looks very much like what we get
from `bool`. But soon we will add a new impl of this trait, and some new
methods, that let us express "fuzzy" constraints that aren't always true
or false. (In particular, a constraint will express the upper and lower
bounds of the allowed specializations of a typevar.)

Even once we have that, most of the operations that we perform on
constraint sets will be the usual boolean operations, just on sets.
(`false` becomes empty/never; `true` becomes universe/always; `or`
becomes union; `and` becomes intersection; `not` becomes negation.) So
it's helpful to have this separate PR to refactor how we invoke those
operations without introducing the new functionality yet.

Note that we also have translations of `Option::is_some_and` and
`is_none_or`, and of `Iterator::any` and `all`, and that the `and`,
`or`, `when_any`, and `when_all` methods are meant to short-circuit,
just like the corresponding boolean operations. For constraint sets,
that depends on being able to implement the `is_always` and `is_never`
trait methods.

---------

Co-authored-by: Carl Meyer <[email protected]>
Co-authored-by: Alex Waygood <[email protected]>
second-ed pushed a commit to second-ed/ruff that referenced this pull request Sep 9, 2025
This PR adds an implementation of constraint sets.

An individual constraint restricts the specialization of a single
typevar to be within a particular lower and upper bound: the typevar can
only specialize to types that are a supertype of the lower bound, and a
subtype of the upper bound. (Note that lower and upper bounds are fully
static; we take the bottom and top materializations of the bounds to
remove any gradual forms if needed.) Either bound can be “closed” (where
the bound is a valid specialization), or “open” (where it is not).

You can then build up more complex constraint sets using union,
intersection, and negation operations. We use a disjunctive normal form
(DNF) representation, just like we do for types: a _constraint set_ is
the union of zero or more _clauses_, each of which is the intersection
of zero or more individual constraints. Note that the constraint set
that contains no clauses is never satisfiable (`⋃ {} = 0`); and the
constraint set that contains a single clause, which contains no
constraints, is always satisfiable (`⋃ {⋂ {}} = 1`).

One thing to note is that this PR does not change the logic of the
actual assignability checks, and in particular, we still aren't ever
trying to create an "individual constraint" that constrains a typevar.
Technically we're still operating only on `bool`s, since we only ever
instantiate `C::always_satisfiable` (i.e., `true`) and
`C::unsatisfiable` (i.e., `false`) in the `has_relation_to` methods. So
if you thought that astral-sh#19838 introduced an unnecessarily complex stand-in
for `bool`, well here you go, this one is worse! (But still seemingly
not yielding a performance regression!) The next PR in this series,
astral-sh#20093, is where we will actually create some non-trivial constraint
sets and use them in anger.

That said, the PR does go ahead and update the assignability checks to
use the new `ConstraintSet` type instead of `bool`. That part is fairly
straightforward since we had already updated the assignability checks to
use the `Constraints` trait; we just have to actively choose a different
impl type. (For the `is_whatever` variants, which still return a `bool`,
we have to convert the constraint set, but the explicit
`is_always_satisfiable` calls serve as nice documentation of our
intent.)

---------

Co-authored-by: Alex Waygood <[email protected]>
Co-authored-by: Carl Meyer <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ty Multi-file analysis & type inference

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants