Skip to content

Commit c0b04d4

Browse files
authored
[ty] Update "constraint implication" relation to work on constraints between two typevars (#21068)
It's possible for a constraint to mention two typevars. For instance, in the body of ```py def f[S: int, T: S](): ... ``` the baseline constraint set would be `(T ≤ S) ∧ (S ≤ int)`. That is, `S` must specialize to some subtype of `int`, and `T` must specialize to a subtype of the type that `S` specializes to. This PR updates the new "constraint implication" relationship from #21010 to work on these kinds of constraint sets. For instance, in the example above, we should be able to see that `T ≤ int` must always hold: ```py def f[S, T](): constraints = ConstraintSet.range(Never, S, int) & ConstraintSet.range(Never, T, S) static_assert(constraints.implies_subtype_of(T, int)) # now succeeds! ``` This did not require major changes to the implementation of `implies_subtype_of`. That method already relies on how our `simplify` and `domain` methods expand a constraint set to include the transitive closure of the constraints that it mentions, and to mark certain combinations of constraints as impossible. Previously, that transitive closure logic only looked at pairs of constraints that constrain the same typevar. (For instance, to notice that `(T ≤ bool) ∧ ¬(T ≤ int)` is impossible.) Now we also look at pairs of constraints that constraint different typevars, if one of the constraints is bound by the other — that is, pairs of the form `T ≤ S` and `S ≤ something`, or `S ≤ T` and `something ≤ S`. In those cases, transitivity lets us add a new derived constraint that `T ≤ something` or `something ≤ T`, respectively. Having done that, our existing `implies_subtype_of` logic finds and takes into account that derived constraint.
1 parent 1c7ea69 commit c0b04d4

File tree

2 files changed

+89
-28
lines changed

2 files changed

+89
-28
lines changed

crates/ty_python_semantic/resources/mdtest/type_properties/implies_subtype_of.md

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -180,16 +180,12 @@ This might require propagating constraints from other typevars.
180180
def mutually_constrained[T, U]():
181181
# If [T = U ∧ U ≤ int], then [T ≤ int] must be true as well.
182182
given_int = ConstraintSet.range(U, T, U) & ConstraintSet.range(Never, U, int)
183-
# TODO: no static-assert-error
184-
# error: [static-assert-error]
185183
static_assert(given_int.implies_subtype_of(T, int))
186184
static_assert(not given_int.implies_subtype_of(T, bool))
187185
static_assert(not given_int.implies_subtype_of(T, str))
188186

189187
# If [T ≤ U ∧ U ≤ int], then [T ≤ int] must be true as well.
190188
given_int = ConstraintSet.range(Never, T, U) & ConstraintSet.range(Never, U, int)
191-
# TODO: no static-assert-error
192-
# error: [static-assert-error]
193189
static_assert(given_int.implies_subtype_of(T, int))
194190
static_assert(not given_int.implies_subtype_of(T, bool))
195191
static_assert(not given_int.implies_subtype_of(T, str))

crates/ty_python_semantic/src/types/constraints.rs

Lines changed: 89 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -753,30 +753,24 @@ impl<'db> Node<'db> {
753753
rhs: Type<'db>,
754754
inferable: InferableTypeVars<'_, 'db>,
755755
) -> Self {
756-
match (lhs, rhs) {
757-
// When checking subtyping involving a typevar, we project the BDD so that it only
758-
// contains that typevar, and any other typevars that could be its upper/lower bound.
759-
// (That is, other typevars that are "later" in our arbitrary ordering of typevars.)
760-
//
761-
// Having done that, we can turn the subtyping check into a constraint (i.e, "is `T` a
762-
// subtype of `int` becomes the constraint `T ≤ int`), and then check when the BDD
763-
// implies that constraint.
756+
// When checking subtyping involving a typevar, we can turn the subtyping check into a
757+
// constraint (i.e, "is `T` a subtype of `int` becomes the constraint `T ≤ int`), and then
758+
// check when the BDD implies that constraint.
759+
let constraint = match (lhs, rhs) {
764760
(Type::TypeVar(bound_typevar), _) => {
765-
let constraint = ConstrainedTypeVar::new_node(db, bound_typevar, Type::Never, rhs);
766-
let (simplified, domain) = self.implies(db, constraint).simplify_and_domain(db);
767-
simplified.and(db, domain)
761+
ConstrainedTypeVar::new_node(db, bound_typevar, Type::Never, rhs)
768762
}
769-
770763
(_, Type::TypeVar(bound_typevar)) => {
771-
let constraint =
772-
ConstrainedTypeVar::new_node(db, bound_typevar, lhs, Type::object());
773-
let (simplified, domain) = self.implies(db, constraint).simplify_and_domain(db);
774-
simplified.and(db, domain)
764+
ConstrainedTypeVar::new_node(db, bound_typevar, lhs, Type::object())
775765
}
776-
777766
// If neither type is a typevar, then we fall back on a normal subtyping check.
778-
_ => lhs.when_subtype_of(db, rhs, inferable).node,
779-
}
767+
_ => return lhs.when_subtype_of(db, rhs, inferable).node,
768+
};
769+
770+
let simplified_self = self.simplify(db);
771+
let implication = simplified_self.implies(db, constraint);
772+
let (simplified, domain) = implication.simplify_and_domain(db);
773+
simplified.and(db, domain)
780774
}
781775

782776
/// Returns a new BDD that returns the same results as `self`, but with some inputs fixed to
@@ -1258,14 +1252,85 @@ impl<'db> InteriorNode<'db> {
12581252
let mut simplified = Node::Interior(self);
12591253
let mut domain = Node::AlwaysTrue;
12601254
while let Some((left_constraint, right_constraint)) = to_visit.pop() {
1261-
// If the constraints refer to different typevars, they trivially cannot be compared.
1262-
// TODO: We might need to consider when one constraint's upper or lower bound refers to
1263-
// the other constraint's typevar.
1264-
let typevar = left_constraint.typevar(db);
1265-
if typevar != right_constraint.typevar(db) {
1255+
// If the constraints refer to different typevars, the only simplifications we can make
1256+
// are of the form `S ≤ T ∧ T ≤ int → S ≤ int`.
1257+
let left_typevar = left_constraint.typevar(db);
1258+
let right_typevar = right_constraint.typevar(db);
1259+
if !left_typevar.is_same_typevar_as(db, right_typevar) {
1260+
// We've structured our constraints so that a typevar's upper/lower bound can only
1261+
// be another typevar if the bound is "later" in our arbitrary ordering. That means
1262+
// we only have to check this pair of constraints in one direction — though we do
1263+
// have to figure out which of the two typevars is constrained, and which one is
1264+
// the upper/lower bound.
1265+
let (bound_typevar, bound_constraint, constrained_typevar, constrained_constraint) =
1266+
if left_typevar.can_be_bound_for(db, right_typevar) {
1267+
(
1268+
left_typevar,
1269+
left_constraint,
1270+
right_typevar,
1271+
right_constraint,
1272+
)
1273+
} else {
1274+
(
1275+
right_typevar,
1276+
right_constraint,
1277+
left_typevar,
1278+
left_constraint,
1279+
)
1280+
};
1281+
1282+
// We then look for cases where the "constrained" typevar's upper and/or lower
1283+
// bound matches the "bound" typevar. If so, we're going to add an implication to
1284+
// the constraint set that replaces the upper/lower bound that matched with the
1285+
// bound constraint's corresponding bound.
1286+
let (new_lower, new_upper) = match (
1287+
constrained_constraint.lower(db),
1288+
constrained_constraint.upper(db),
1289+
) {
1290+
// (B ≤ C ≤ B) ∧ (BL ≤ B ≤ BU) → (BL ≤ C ≤ BU)
1291+
(Type::TypeVar(constrained_lower), Type::TypeVar(constrained_upper))
1292+
if constrained_lower.is_same_typevar_as(db, bound_typevar)
1293+
&& constrained_upper.is_same_typevar_as(db, bound_typevar) =>
1294+
{
1295+
(bound_constraint.lower(db), bound_constraint.upper(db))
1296+
}
1297+
1298+
// (CL ≤ C ≤ B) ∧ (BL ≤ B ≤ BU) → (CL ≤ C ≤ BU)
1299+
(constrained_lower, Type::TypeVar(constrained_upper))
1300+
if constrained_upper.is_same_typevar_as(db, bound_typevar) =>
1301+
{
1302+
(constrained_lower, bound_constraint.upper(db))
1303+
}
1304+
1305+
// (B ≤ C ≤ CU) ∧ (BL ≤ B ≤ BU) → (BL ≤ C ≤ CU)
1306+
(Type::TypeVar(constrained_lower), constrained_upper)
1307+
if constrained_lower.is_same_typevar_as(db, bound_typevar) =>
1308+
{
1309+
(bound_constraint.lower(db), constrained_upper)
1310+
}
1311+
1312+
_ => continue,
1313+
};
1314+
1315+
let new_node = Node::new_constraint(
1316+
db,
1317+
ConstrainedTypeVar::new(db, constrained_typevar, new_lower, new_upper),
1318+
);
1319+
let positive_left_node =
1320+
Node::new_satisfied_constraint(db, left_constraint.when_true());
1321+
let positive_right_node =
1322+
Node::new_satisfied_constraint(db, right_constraint.when_true());
1323+
let lhs = positive_left_node.and(db, positive_right_node);
1324+
let implication = lhs.implies(db, new_node);
1325+
domain = domain.and(db, implication);
1326+
1327+
let intersection = new_node.ite(db, lhs, Node::AlwaysFalse);
1328+
simplified = simplified.and(db, intersection);
12661329
continue;
12671330
}
12681331

1332+
// From here on out we know that both constraints constrain the same typevar.
1333+
12691334
// Containment: The range of one constraint might completely contain the range of the
12701335
// other. If so, there are several potential simplifications.
12711336
let larger_smaller = if left_constraint.implies(db, right_constraint) {

0 commit comments

Comments
 (0)