Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ strict subtype of the lower bound, a strict supertype of the upper bound, or inc

```py
from typing import Any, final, Never, Sequence
from ty_extensions import ConstraintSet, static_assert
from ty_extensions import ConstraintSet, Not, static_assert

class Super: ...
class Base(Super): ...
Expand Down Expand Up @@ -207,6 +207,15 @@ def _[T]() -> None:
static_assert(constraints == expected)
```

A negated _type_ is not the same thing as a negated _range_.

```py
def _[T]() -> None:
negated_type = ConstraintSet.range(Never, T, Not[int])
negated_constraint = ~ConstraintSet.range(Never, T, int)
static_assert(negated_type != negated_constraint)
Comment on lines +214 to +216
Copy link
Contributor

Choose a reason for hiding this comment

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

This DSL is so cool

```

## Intersection

The intersection of two constraint sets requires that the constraints in both sets hold. In many
Expand Down
14 changes: 12 additions & 2 deletions crates/ty_python_semantic/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1268,8 +1268,14 @@ impl<'db> Type<'db> {
self.as_union().expect("Expected a Type::Union variant")
}

pub(crate) const fn is_intersection(self) -> bool {
matches!(self, Type::Intersection(_))
/// Returns whether this is a "real" intersection type. (Negated types are represented by an
/// intersection containing a single negative branch, which this method does _not_ consider a
/// "real" intersection.)
pub(crate) fn is_nontrivial_intersection(self, db: &'db dyn Db) -> bool {
match self {
Type::Intersection(intersection) => !intersection.is_simple_negation(db),
_ => false,
}
}

pub(crate) const fn as_function_literal(self) -> Option<FunctionType<'db>> {
Expand Down Expand Up @@ -14123,6 +14129,10 @@ impl<'db> IntersectionType<'db> {
(self.positive(db).len() + self.negative(db).len()) == 1
}

pub(crate) fn is_simple_negation(self, db: &'db dyn Db) -> bool {
self.positive(db).is_empty() && self.negative(db).len() == 1
}

fn heap_size((positive, negative): &(FxOrderSet<Type<'db>>, FxOrderSet<Type<'db>>)) -> usize {
ruff_memory_usage::order_set_heap_size(positive)
+ ruff_memory_usage::order_set_heap_size(negative)
Expand Down
27 changes: 19 additions & 8 deletions crates/ty_python_semantic/src/types/constraints.rs
Original file line number Diff line number Diff line change
Expand Up @@ -435,6 +435,11 @@ impl<'db> ConstraintSet<'db> {
pub(crate) fn display(self, db: &'db dyn Db) -> impl Display {
self.node.simplify_for_display(db).display(db)
}

#[expect(dead_code)] // Keep this around for debugging purposes
pub(crate) fn display_graph(self, db: &'db dyn Db, prefix: &dyn Display) -> impl Display {
self.node.display_graph(db, prefix)
}
}

impl From<bool> for ConstraintSet<'_> {
Expand Down Expand Up @@ -498,11 +503,13 @@ impl<'db> ConstrainedTypeVar<'db> {
debug_assert_eq!(upper, upper.top_materialization(db));

// It's not useful for an upper bound to be an intersection type, or for a lower bound to
// be a union type. Both of those can be rewritten as simpler BDDs:
// be a union type. Because the following equivalences hold, we can break these bounds
// apart and create an equivalent BDD with more nodes but simpler constraints. (Fewer,
// simpler constraints mean that our sequent maps won't grow pathologically large.)
//
// T ≤ α & β (T ≤ α) ∧ (T ≤ β)
// T ≤ α & ¬β (T ≤ α) ∧ ¬(T ≤ β)
// α | β ≤ T (α ≤ T) ∧ (β ≤ T)
// T ≤ (α & β) ⇔ (T ≤ α) ∧ (T ≤ β)
// T ≤ α & ¬β) ⇔ (T ≤ ¬α) ∧ (T ≤ ¬β)
// (α | β) ≤ T (α ≤ T) ∧ (β ≤ T)
if let Type::Union(lower_union) = lower {
let mut result = Node::AlwaysTrue;
for lower_element in lower_union.elements(db) {
Expand All @@ -513,7 +520,12 @@ impl<'db> ConstrainedTypeVar<'db> {
}
return result;
}
if let Type::Intersection(upper_intersection) = upper {
// A negated type ¬α is represented as an intersection with no positive elements, and a
// single negative element. We _don't_ want to treat that an "intersection" for the
// purposes of simplifying upper bounds.
if let Type::Intersection(upper_intersection) = upper
&& !upper_intersection.is_simple_negation(db)
{
let mut result = Node::AlwaysTrue;
for upper_element in upper_intersection.iter_positive(db) {
result = result.and(
Expand All @@ -524,7 +536,7 @@ impl<'db> ConstrainedTypeVar<'db> {
for upper_element in upper_intersection.iter_negative(db) {
result = result.and(
db,
ConstrainedTypeVar::new_node(db, typevar, lower, upper_element).negate(db),
ConstrainedTypeVar::new_node(db, typevar, lower, upper_element.negate(db)),
Copy link
Contributor

Choose a reason for hiding this comment

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

So subtle 😃

);
}
return result;
Expand Down Expand Up @@ -716,7 +728,7 @@ impl<'db> ConstrainedTypeVar<'db> {
return IntersectionResult::Disjoint;
}

if lower.is_union() || upper.is_intersection() {
if lower.is_union() || upper.is_nontrivial_intersection(db) {
return IntersectionResult::CannotSimplify;
}

Expand Down Expand Up @@ -1579,7 +1591,6 @@ impl<'db> Node<'db> {
/// │ └─₀ never
/// └─₀ never
/// ```
#[cfg_attr(not(test), expect(dead_code))] // Keep this around for debugging purposes
fn display_graph(self, db: &'db dyn Db, prefix: &dyn Display) -> impl Display {
struct DisplayNode<'a, 'db> {
db: &'db dyn Db,
Expand Down
21 changes: 8 additions & 13 deletions crates/ty_python_semantic/src/types/infer/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10844,19 +10844,14 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
(
Type::KnownInstance(KnownInstanceType::ConstraintSet(left)),
Type::KnownInstance(KnownInstanceType::ConstraintSet(right)),
) => {
let result = match op {
ast::CmpOp::Eq => Some(
left.constraints(self.db()).iff(self.db(), right.constraints(self.db()))
),
ast::CmpOp::NotEq => Some(
left.constraints(self.db()).iff(self.db(), right.constraints(self.db())).negate(self.db())
),
_ => None,
};
result.map(|constraints| Ok(Type::KnownInstance(KnownInstanceType::ConstraintSet(
TrackedConstraintSet::new(self.db(), constraints)
))))
) => match op {
ast::CmpOp::Eq => Some(Ok(Type::BooleanLiteral(
left.constraints(self.db()).iff(self.db(), right.constraints(self.db())).is_always_satisfied(self.db()),
))),
ast::CmpOp::NotEq => Some(Ok(Type::BooleanLiteral(
!left.constraints(self.db()).iff(self.db(), right.constraints(self.db())).is_always_satisfied(self.db()),
))),
_ => None,
}

(
Expand Down
Loading