mirror of
https://github.com/astral-sh/ruff.git
synced 2025-10-29 11:07:54 +00:00
[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:
parent
81c1d36088
commit
766ed5b5f3
6 changed files with 314 additions and 23 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
|
@ -4434,10 +4434,12 @@ dependencies = [
|
||||||
"glob",
|
"glob",
|
||||||
"hashbrown 0.16.0",
|
"hashbrown 0.16.0",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
|
"indoc",
|
||||||
"insta",
|
"insta",
|
||||||
"itertools 0.14.0",
|
"itertools 0.14.0",
|
||||||
"memchr",
|
"memchr",
|
||||||
"ordermap",
|
"ordermap",
|
||||||
|
"pretty_assertions",
|
||||||
"quickcheck",
|
"quickcheck",
|
||||||
"quickcheck_macros",
|
"quickcheck_macros",
|
||||||
"ruff_annotate_snippets",
|
"ruff_annotate_snippets",
|
||||||
|
|
|
||||||
|
|
@ -63,7 +63,9 @@ ty_vendored = { workspace = true }
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
dir-test = { workspace = true }
|
dir-test = { workspace = true }
|
||||||
glob = { workspace = true }
|
glob = { workspace = true }
|
||||||
|
indoc = { workspace = true }
|
||||||
insta = { workspace = true }
|
insta = { workspace = true }
|
||||||
|
pretty_assertions = { workspace = true }
|
||||||
tempfile = { workspace = true }
|
tempfile = { workspace = true }
|
||||||
quickcheck = { version = "1.0.3", default-features = false }
|
quickcheck = { version = "1.0.3", default-features = false }
|
||||||
quickcheck_macros = { version = "1.0.0" }
|
quickcheck_macros = { version = "1.0.0" }
|
||||||
|
|
|
||||||
|
|
@ -601,3 +601,40 @@ def _[T, U]() -> None:
|
||||||
# revealed: ty_extensions.ConstraintSet[always]
|
# revealed: ty_extensions.ConstraintSet[always]
|
||||||
reveal_type(~union | union)
|
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))
|
||||||
|
```
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,12 @@ use crate::unpack::{Unpack, UnpackPosition};
|
||||||
/// because a new scope gets inserted before the `Definition` or a new place is inserted
|
/// because a new scope gets inserted before the `Definition` or a new place is inserted
|
||||||
/// before this `Definition`. However, the ID can be considered stable and it is okay to use
|
/// before this `Definition`. However, the ID can be considered stable and it is okay to use
|
||||||
/// `Definition` in cross-module` salsa queries or as a field on other salsa tracked structs.
|
/// `Definition` in cross-module` salsa queries or as a field on other salsa tracked structs.
|
||||||
|
///
|
||||||
|
/// # Ordering
|
||||||
|
/// Ordering is based on the definition's salsa-assigned id and not on its values.
|
||||||
|
/// The id may change between runs, or when the definition was garbage collected and recreated.
|
||||||
#[salsa::tracked(debug, heap_size=ruff_memory_usage::heap_size)]
|
#[salsa::tracked(debug, heap_size=ruff_memory_usage::heap_size)]
|
||||||
|
#[derive(Ord, PartialOrd)]
|
||||||
pub struct Definition<'db> {
|
pub struct Definition<'db> {
|
||||||
/// The file in which the definition occurs.
|
/// The file in which the definition occurs.
|
||||||
pub file: File,
|
pub file: File,
|
||||||
|
|
|
||||||
|
|
@ -8461,7 +8461,9 @@ fn lazy_bound_or_constraints_cycle_initial<'db>(
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Where a type variable is bound and usable.
|
/// Where a type variable is bound and usable.
|
||||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, salsa::Update, get_size2::GetSize)]
|
#[derive(
|
||||||
|
Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, salsa::Update, get_size2::GetSize,
|
||||||
|
)]
|
||||||
pub enum BindingContext<'db> {
|
pub enum BindingContext<'db> {
|
||||||
/// The definition of the generic class, function, or type alias that binds this typevar.
|
/// The definition of the generic class, function, or type alias that binds this typevar.
|
||||||
Definition(Definition<'db>),
|
Definition(Definition<'db>),
|
||||||
|
|
@ -8495,7 +8497,9 @@ impl<'db> BindingContext<'db> {
|
||||||
/// independent of the typevar's bounds or constraints. Two bound typevars have the same identity
|
/// independent of the typevar's bounds or constraints. Two bound typevars have the same identity
|
||||||
/// if they represent the same logical typevar bound in the same context, even if their bounds
|
/// if they represent the same logical typevar bound in the same context, even if their bounds
|
||||||
/// have been materialized differently.
|
/// have been materialized differently.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, get_size2::GetSize, salsa::Update)]
|
#[derive(
|
||||||
|
Debug, Clone, Copy, Eq, Hash, Ord, PartialEq, PartialOrd, get_size2::GetSize, salsa::Update,
|
||||||
|
)]
|
||||||
pub struct BoundTypeVarIdentity<'db> {
|
pub struct BoundTypeVarIdentity<'db> {
|
||||||
pub(crate) identity: TypeVarIdentity<'db>,
|
pub(crate) identity: TypeVarIdentity<'db>,
|
||||||
pub(crate) binding_context: BindingContext<'db>,
|
pub(crate) binding_context: BindingContext<'db>,
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,9 @@
|
||||||
//! Note that all lower and upper bounds in a constraint must be fully static. We take the bottom
|
//! Note that all lower and upper bounds in a constraint must be fully static. We take the bottom
|
||||||
//! and top materializations of the types to remove any gradual forms if needed.
|
//! and top materializations of the types to remove any gradual forms if needed.
|
||||||
//!
|
//!
|
||||||
|
//! Lower and upper bounds must also be normalized. This lets us identify, for instance,
|
||||||
|
//! two constraints with equivalent but differently ordered unions as their bounds.
|
||||||
|
//!
|
||||||
//! NOTE: This module is currently in a transitional state. We've added the BDD [`ConstraintSet`]
|
//! NOTE: This module is currently in a transitional state. We've added the BDD [`ConstraintSet`]
|
||||||
//! representation, and updated all of our property checks to build up a constraint set and then
|
//! representation, and updated all of our property checks to build up a constraint set and then
|
||||||
//! check whether it is ever or always satisfiable, as appropriate. We are not yet inferring
|
//! check whether it is ever or always satisfiable, as appropriate. We are not yet inferring
|
||||||
|
|
@ -58,6 +61,7 @@ use std::fmt::Display;
|
||||||
|
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
use rustc_hash::FxHashSet;
|
use rustc_hash::FxHashSet;
|
||||||
|
use salsa::plumbing::AsId;
|
||||||
|
|
||||||
use crate::Db;
|
use crate::Db;
|
||||||
use crate::types::{BoundTypeVarIdentity, IntersectionType, Type, UnionType};
|
use crate::types::{BoundTypeVarIdentity, IntersectionType, Type, UnionType};
|
||||||
|
|
@ -183,20 +187,20 @@ impl<'db> ConstraintSet<'db> {
|
||||||
|
|
||||||
/// Updates this constraint set to hold the union of itself and another constraint set.
|
/// Updates this constraint set to hold the union of itself and another constraint set.
|
||||||
pub(crate) fn union(&mut self, db: &'db dyn Db, other: Self) -> Self {
|
pub(crate) fn union(&mut self, db: &'db dyn Db, other: Self) -> Self {
|
||||||
self.node = self.node.or(db, other.node).simplify(db);
|
self.node = self.node.or(db, other.node);
|
||||||
*self
|
*self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Updates this constraint set to hold the intersection of itself and another constraint set.
|
/// Updates this constraint set to hold the intersection of itself and another constraint set.
|
||||||
pub(crate) fn intersect(&mut self, db: &'db dyn Db, other: Self) -> Self {
|
pub(crate) fn intersect(&mut self, db: &'db dyn Db, other: Self) -> Self {
|
||||||
self.node = self.node.and(db, other.node).simplify(db);
|
self.node = self.node.and(db, other.node);
|
||||||
*self
|
*self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the negation of this constraint set.
|
/// Returns the negation of this constraint set.
|
||||||
pub(crate) fn negate(self, db: &'db dyn Db) -> Self {
|
pub(crate) fn negate(self, db: &'db dyn Db) -> Self {
|
||||||
Self {
|
Self {
|
||||||
node: self.node.negate(db).simplify(db),
|
node: self.node.negate(db),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -256,7 +260,6 @@ impl From<bool> for ConstraintSet<'_> {
|
||||||
/// An individual constraint in a constraint set. This restricts a single typevar to be within a
|
/// An individual constraint in a constraint set. This restricts a single typevar to be within a
|
||||||
/// lower and upper bound.
|
/// lower and upper bound.
|
||||||
#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)]
|
#[salsa::interned(debug, heap_size=ruff_memory_usage::heap_size)]
|
||||||
#[derive(PartialOrd, Ord)]
|
|
||||||
pub(crate) struct ConstrainedTypeVar<'db> {
|
pub(crate) struct ConstrainedTypeVar<'db> {
|
||||||
typevar: BoundTypeVarIdentity<'db>,
|
typevar: BoundTypeVarIdentity<'db>,
|
||||||
lower: Type<'db>,
|
lower: Type<'db>,
|
||||||
|
|
@ -292,8 +295,11 @@ impl<'db> ConstrainedTypeVar<'db> {
|
||||||
return Node::AlwaysTrue;
|
return Node::AlwaysTrue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let lower = lower.normalized(db);
|
||||||
|
let upper = upper.normalized(db);
|
||||||
Node::new_constraint(db, ConstrainedTypeVar::new(db, typevar, lower, upper))
|
Node::new_constraint(db, ConstrainedTypeVar::new(db, typevar, lower, upper))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn when_true(self) -> ConstraintAssignment<'db> {
|
fn when_true(self) -> ConstraintAssignment<'db> {
|
||||||
ConstraintAssignment::Positive(self)
|
ConstraintAssignment::Positive(self)
|
||||||
}
|
}
|
||||||
|
|
@ -310,11 +316,37 @@ impl<'db> ConstrainedTypeVar<'db> {
|
||||||
&& other.upper(db).is_subtype_of(db, self.upper(db))
|
&& other.upper(db).is_subtype_of(db, self.upper(db))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Defines the ordering of the variables in a constraint set BDD.
|
||||||
|
///
|
||||||
|
/// If we only care about _correctness_, we can choose any ordering that we want, as long as
|
||||||
|
/// it's consistent. However, different orderings can have very different _performance_
|
||||||
|
/// characteristics. Many BDD libraries attempt to reorder variables on the fly while building
|
||||||
|
/// and working with BDDs. We don't do that, but we have tried to make some simple choices that
|
||||||
|
/// have clear wins.
|
||||||
|
///
|
||||||
|
/// In particular, we compare the _typevars_ of each constraint first, so that all constraints
|
||||||
|
/// for a single typevar are guaranteed to be adjacent in the BDD structure. There are several
|
||||||
|
/// simplifications that we perform that operate on constraints with the same typevar, and this
|
||||||
|
/// ensures that we can find all candidate simplifications more easily.
|
||||||
|
fn ordering(self, db: &'db dyn Db) -> impl Ord {
|
||||||
|
(self.typevar(db), self.as_id())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns whether this constraint implies another — i.e., whether every type that
|
||||||
|
/// satisfies this constraint also satisfies `other`.
|
||||||
|
///
|
||||||
|
/// This is used (among other places) to simplify how we display constraint sets, by removing
|
||||||
|
/// redundant constraints from a clause.
|
||||||
|
fn implies(self, db: &'db dyn Db, other: Self) -> bool {
|
||||||
|
other.contains(db, self)
|
||||||
|
}
|
||||||
|
|
||||||
/// Returns the intersection of two range constraints, or `None` if the intersection is empty.
|
/// Returns the intersection of two range constraints, or `None` if the intersection is empty.
|
||||||
fn intersect(self, db: &'db dyn Db, other: Self) -> Option<Self> {
|
fn intersect(self, db: &'db dyn Db, other: Self) -> Option<Self> {
|
||||||
// (s₁ ≤ α ≤ t₁) ∧ (s₂ ≤ α ≤ t₂) = (s₁ ∪ s₂) ≤ α ≤ (t₁ ∩ t₂))
|
// (s₁ ≤ α ≤ t₁) ∧ (s₂ ≤ α ≤ t₂) = (s₁ ∪ s₂) ≤ α ≤ (t₁ ∩ t₂))
|
||||||
let lower = UnionType::from_elements(db, [self.lower(db), other.lower(db)]);
|
let lower = UnionType::from_elements(db, [self.lower(db), other.lower(db)]).normalized(db);
|
||||||
let upper = IntersectionType::from_elements(db, [self.upper(db), other.upper(db)]);
|
let upper =
|
||||||
|
IntersectionType::from_elements(db, [self.upper(db), other.upper(db)]).normalized(db);
|
||||||
|
|
||||||
// If `lower ≰ upper`, then the intersection is empty, since there is no type that is both
|
// If `lower ≰ upper`, then the intersection is empty, since there is no type that is both
|
||||||
// greater than `lower`, and less than `upper`.
|
// greater than `lower`, and less than `upper`.
|
||||||
|
|
@ -390,8 +422,8 @@ impl<'db> ConstrainedTypeVar<'db> {
|
||||||
/// that point at the same node.
|
/// that point at the same node.
|
||||||
///
|
///
|
||||||
/// BDD nodes are also _ordered_, meaning that every path from the root of a BDD to a terminal node
|
/// BDD nodes are also _ordered_, meaning that every path from the root of a BDD to a terminal node
|
||||||
/// visits variables in the same order. [`ConstrainedTypeVar`]s are interned, so we can use the IDs
|
/// visits variables in the same order. [`ConstrainedTypeVar::ordering`] defines the variable
|
||||||
/// that salsa assigns to define this order.
|
/// ordering that we use for constraint set BDDs.
|
||||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, get_size2::GetSize, salsa::Update)]
|
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, get_size2::GetSize, salsa::Update)]
|
||||||
enum Node<'db> {
|
enum Node<'db> {
|
||||||
AlwaysFalse,
|
AlwaysFalse,
|
||||||
|
|
@ -407,13 +439,13 @@ impl<'db> Node<'db> {
|
||||||
if_true: Node<'db>,
|
if_true: Node<'db>,
|
||||||
if_false: Node<'db>,
|
if_false: Node<'db>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
debug_assert!((if_true.root_constraint(db)).is_none_or(|root_constraint| {
|
||||||
|
root_constraint.ordering(db) > constraint.ordering(db)
|
||||||
|
}));
|
||||||
debug_assert!(
|
debug_assert!(
|
||||||
(if_true.root_constraint(db))
|
(if_false.root_constraint(db)).is_none_or(|root_constraint| {
|
||||||
.is_none_or(|root_constraint| root_constraint > constraint)
|
root_constraint.ordering(db) > constraint.ordering(db)
|
||||||
);
|
})
|
||||||
debug_assert!(
|
|
||||||
(if_false.root_constraint(db))
|
|
||||||
.is_none_or(|root_constraint| root_constraint > constraint)
|
|
||||||
);
|
);
|
||||||
if if_true == if_false {
|
if if_true == if_false {
|
||||||
return if_true;
|
return if_true;
|
||||||
|
|
@ -762,14 +794,87 @@ impl<'db> Node<'db> {
|
||||||
Node::AlwaysFalse => f.write_str("never"),
|
Node::AlwaysFalse => f.write_str("never"),
|
||||||
Node::Interior(_) => {
|
Node::Interior(_) => {
|
||||||
let mut clauses = self.node.satisfied_clauses(self.db);
|
let mut clauses = self.node.satisfied_clauses(self.db);
|
||||||
clauses.simplify();
|
clauses.simplify(self.db);
|
||||||
clauses.display(self.db).fmt(f)
|
clauses.display(self.db).fmt(f)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DisplayNode { node: self, db }
|
DisplayNode {
|
||||||
|
node: self.simplify(db),
|
||||||
|
db,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Displays the full graph structure of this BDD. `prefix` will be output before each line
|
||||||
|
/// other than the first. Produces output like the following:
|
||||||
|
///
|
||||||
|
/// ```text
|
||||||
|
/// (T@_ = str)
|
||||||
|
/// ┡━₁ (U@_ = str)
|
||||||
|
/// │ ┡━₁ always
|
||||||
|
/// │ └─₀ (U@_ = bool)
|
||||||
|
/// │ ┡━₁ always
|
||||||
|
/// │ └─₀ never
|
||||||
|
/// └─₀ (T@_ = bool)
|
||||||
|
/// ┡━₁ (U@_ = str)
|
||||||
|
/// │ ┡━₁ always
|
||||||
|
/// │ └─₀ (U@_ = bool)
|
||||||
|
/// │ ┡━₁ always
|
||||||
|
/// │ └─₀ 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,
|
||||||
|
node: Node<'db>,
|
||||||
|
prefix: &'a dyn Display,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, 'db> DisplayNode<'a, 'db> {
|
||||||
|
fn new(db: &'db dyn Db, node: Node<'db>, prefix: &'a dyn Display) -> Self {
|
||||||
|
Self { db, node, prefix }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for DisplayNode<'_, '_> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match self.node {
|
||||||
|
Node::AlwaysTrue => write!(f, "always"),
|
||||||
|
Node::AlwaysFalse => write!(f, "never"),
|
||||||
|
Node::Interior(interior) => {
|
||||||
|
interior.constraint(self.db).display(self.db).fmt(f)?;
|
||||||
|
// Calling display_graph recursively here causes rustc to claim that the
|
||||||
|
// expect(unused) up above is unfulfilled!
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"\n{}┡━₁ {}",
|
||||||
|
self.prefix,
|
||||||
|
DisplayNode::new(
|
||||||
|
self.db,
|
||||||
|
interior.if_true(self.db),
|
||||||
|
&format_args!("{}│ ", self.prefix)
|
||||||
|
),
|
||||||
|
)?;
|
||||||
|
write!(
|
||||||
|
f,
|
||||||
|
"\n{}└─₀ {}",
|
||||||
|
self.prefix,
|
||||||
|
DisplayNode::new(
|
||||||
|
self.db,
|
||||||
|
interior.if_false(self.db),
|
||||||
|
&format_args!("{} ", self.prefix)
|
||||||
|
),
|
||||||
|
)?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DisplayNode::new(db, self, prefix)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -800,7 +905,7 @@ impl<'db> InteriorNode<'db> {
|
||||||
fn or(self, db: &'db dyn Db, other: Self) -> Node<'db> {
|
fn or(self, db: &'db dyn Db, other: Self) -> Node<'db> {
|
||||||
let self_constraint = self.constraint(db);
|
let self_constraint = self.constraint(db);
|
||||||
let other_constraint = other.constraint(db);
|
let other_constraint = other.constraint(db);
|
||||||
match self_constraint.cmp(&other_constraint) {
|
match (self_constraint.ordering(db)).cmp(&other_constraint.ordering(db)) {
|
||||||
Ordering::Equal => Node::new(
|
Ordering::Equal => Node::new(
|
||||||
db,
|
db,
|
||||||
self_constraint,
|
self_constraint,
|
||||||
|
|
@ -826,7 +931,7 @@ impl<'db> InteriorNode<'db> {
|
||||||
fn and(self, db: &'db dyn Db, other: Self) -> Node<'db> {
|
fn and(self, db: &'db dyn Db, other: Self) -> Node<'db> {
|
||||||
let self_constraint = self.constraint(db);
|
let self_constraint = self.constraint(db);
|
||||||
let other_constraint = other.constraint(db);
|
let other_constraint = other.constraint(db);
|
||||||
match self_constraint.cmp(&other_constraint) {
|
match (self_constraint.ordering(db)).cmp(&other_constraint.ordering(db)) {
|
||||||
Ordering::Equal => Node::new(
|
Ordering::Equal => Node::new(
|
||||||
db,
|
db,
|
||||||
self_constraint,
|
self_constraint,
|
||||||
|
|
@ -852,7 +957,7 @@ impl<'db> InteriorNode<'db> {
|
||||||
fn iff(self, db: &'db dyn Db, other: Self) -> Node<'db> {
|
fn iff(self, db: &'db dyn Db, other: Self) -> Node<'db> {
|
||||||
let self_constraint = self.constraint(db);
|
let self_constraint = self.constraint(db);
|
||||||
let other_constraint = other.constraint(db);
|
let other_constraint = other.constraint(db);
|
||||||
match self_constraint.cmp(&other_constraint) {
|
match (self_constraint.ordering(db)).cmp(&other_constraint.ordering(db)) {
|
||||||
Ordering::Equal => Node::new(
|
Ordering::Equal => Node::new(
|
||||||
db,
|
db,
|
||||||
self_constraint,
|
self_constraint,
|
||||||
|
|
@ -884,7 +989,7 @@ impl<'db> InteriorNode<'db> {
|
||||||
// point in the BDD where the assignment can no longer affect the result,
|
// point in the BDD where the assignment can no longer affect the result,
|
||||||
// and we can return early.
|
// and we can return early.
|
||||||
let self_constraint = self.constraint(db);
|
let self_constraint = self.constraint(db);
|
||||||
if assignment.constraint() < self_constraint {
|
if assignment.constraint().ordering(db) < self_constraint.ordering(db) {
|
||||||
return (Node::Interior(self), false);
|
return (Node::Interior(self), false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1141,6 +1246,55 @@ impl<'db> ConstraintAssignment<'db> {
|
||||||
*self = self.negated();
|
*self = self.negated();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns whether this constraint implies another — i.e., whether every type that
|
||||||
|
/// satisfies this constraint also satisfies `other`.
|
||||||
|
///
|
||||||
|
/// This is used (among other places) to simplify how we display constraint sets, by removing
|
||||||
|
/// redundant constraints from a clause.
|
||||||
|
fn implies(self, db: &'db dyn Db, other: Self) -> bool {
|
||||||
|
match (self, other) {
|
||||||
|
// For two positive constraints, one range has to fully contain the other; the smaller
|
||||||
|
// constraint implies the larger.
|
||||||
|
//
|
||||||
|
// ....|----other-----|....
|
||||||
|
// ......|---self---|......
|
||||||
|
(
|
||||||
|
ConstraintAssignment::Positive(self_constraint),
|
||||||
|
ConstraintAssignment::Positive(other_constraint),
|
||||||
|
) => self_constraint.implies(db, other_constraint),
|
||||||
|
|
||||||
|
// For two negative constraints, one range has to fully contain the other; the ranges
|
||||||
|
// represent "holes", though, so the constraint with the larger range implies the one
|
||||||
|
// with the smaller.
|
||||||
|
//
|
||||||
|
// |-----|...other...|-----|
|
||||||
|
// |---|.....self......|---|
|
||||||
|
(
|
||||||
|
ConstraintAssignment::Negative(self_constraint),
|
||||||
|
ConstraintAssignment::Negative(other_constraint),
|
||||||
|
) => other_constraint.implies(db, self_constraint),
|
||||||
|
|
||||||
|
// For a positive and negative constraint, the ranges have to be disjoint, and the
|
||||||
|
// positive range implies the negative range.
|
||||||
|
//
|
||||||
|
// |---------------|...self...|---|
|
||||||
|
// ..|---other---|................|
|
||||||
|
(
|
||||||
|
ConstraintAssignment::Positive(self_constraint),
|
||||||
|
ConstraintAssignment::Negative(other_constraint),
|
||||||
|
) => self_constraint.intersect(db, other_constraint).is_none(),
|
||||||
|
|
||||||
|
// It's theoretically possible for a negative constraint to imply a positive constraint
|
||||||
|
// if the positive constraint is always satisfied (`Never ≤ T ≤ object`). But we never
|
||||||
|
// create constraints of that form, so with our representation, a negative constraint
|
||||||
|
// can never imply a positive constraint.
|
||||||
|
//
|
||||||
|
// |------other-------|
|
||||||
|
// |---|...self...|---|
|
||||||
|
(ConstraintAssignment::Negative(_), ConstraintAssignment::Positive(_)) => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Keep this for future debugging needs, even though it's not currently used when rendering
|
// Keep this for future debugging needs, even though it's not currently used when rendering
|
||||||
// constraint sets.
|
// constraint sets.
|
||||||
#[expect(dead_code)]
|
#[expect(dead_code)]
|
||||||
|
|
@ -1209,6 +1363,43 @@ impl<'db> SatisfiedClause<'db> {
|
||||||
false
|
false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Simplifies this clause by removing constraints that are implied by other constraints in the
|
||||||
|
/// clause. (Clauses are the intersection of constraints, so if two clauses are redundant, we
|
||||||
|
/// want to remove the larger one and keep the smaller one.)
|
||||||
|
///
|
||||||
|
/// Returns a boolean that indicates whether any simplifications were made.
|
||||||
|
fn simplify(&mut self, db: &'db dyn Db) -> bool {
|
||||||
|
let mut changes_made = false;
|
||||||
|
let mut i = 0;
|
||||||
|
// Loop through each constraint, comparing it with any constraints that appear later in the
|
||||||
|
// list.
|
||||||
|
'outer: while i < self.constraints.len() {
|
||||||
|
let mut j = i + 1;
|
||||||
|
while j < self.constraints.len() {
|
||||||
|
if self.constraints[j].implies(db, self.constraints[i]) {
|
||||||
|
// If constraint `i` is removed, then we don't need to compare it with any
|
||||||
|
// later constraints in the list. Note that we continue the outer loop, instead
|
||||||
|
// of breaking from the inner loop, so that we don't bump index `i` below.
|
||||||
|
// (We'll have swapped another element into place at that index, and want to
|
||||||
|
// make sure that we process it.)
|
||||||
|
self.constraints.swap_remove(i);
|
||||||
|
changes_made = true;
|
||||||
|
continue 'outer;
|
||||||
|
} else if self.constraints[i].implies(db, self.constraints[j]) {
|
||||||
|
// If constraint `j` is removed, then we can continue the inner loop. We will
|
||||||
|
// swap a new element into place at index `j`, and will continue comparing the
|
||||||
|
// constraint at index `i` with later constraints.
|
||||||
|
self.constraints.swap_remove(j);
|
||||||
|
changes_made = true;
|
||||||
|
} else {
|
||||||
|
j += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
changes_made
|
||||||
|
}
|
||||||
|
|
||||||
fn display(&self, db: &'db dyn Db) -> String {
|
fn display(&self, db: &'db dyn Db) -> String {
|
||||||
// This is a bit heavy-handed, but we need to output the constraints in a consistent order
|
// This is a bit heavy-handed, but we need to output the constraints in a consistent order
|
||||||
// even though Salsa IDs are assigned non-deterministically. This Display output is only
|
// even though Salsa IDs are assigned non-deterministically. This Display output is only
|
||||||
|
|
@ -1258,7 +1449,13 @@ impl<'db> SatisfiedClauses<'db> {
|
||||||
/// Simplifies the DNF representation, removing redundancies that do not change the underlying
|
/// Simplifies the DNF representation, removing redundancies that do not change the underlying
|
||||||
/// function. (This is used when displaying a BDD, to make sure that the representation that we
|
/// function. (This is used when displaying a BDD, to make sure that the representation that we
|
||||||
/// show is as simple as possible while still producing the same results.)
|
/// show is as simple as possible while still producing the same results.)
|
||||||
fn simplify(&mut self) {
|
fn simplify(&mut self, db: &'db dyn Db) {
|
||||||
|
// First simplify each clause individually, by removing constraints that are implied by
|
||||||
|
// other constraints in the clause.
|
||||||
|
for clause in &mut self.clauses {
|
||||||
|
clause.simplify(db);
|
||||||
|
}
|
||||||
|
|
||||||
while self.simplify_one_round() {
|
while self.simplify_one_round() {
|
||||||
// Keep going
|
// Keep going
|
||||||
}
|
}
|
||||||
|
|
@ -1340,3 +1537,47 @@ impl<'db> SatisfiedClauses<'db> {
|
||||||
clauses.join(" ∨ ")
|
clauses.join(" ∨ ")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
use indoc::indoc;
|
||||||
|
use pretty_assertions::assert_eq;
|
||||||
|
|
||||||
|
use crate::db::tests::setup_db;
|
||||||
|
use crate::types::{BoundTypeVarInstance, KnownClass, TypeVarVariance};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_display_graph_output() {
|
||||||
|
let expected = indoc! {r#"
|
||||||
|
(T = str)
|
||||||
|
┡━₁ (U = str)
|
||||||
|
│ ┡━₁ always
|
||||||
|
│ └─₀ (U = bool)
|
||||||
|
│ ┡━₁ always
|
||||||
|
│ └─₀ never
|
||||||
|
└─₀ (T = bool)
|
||||||
|
┡━₁ (U = str)
|
||||||
|
│ ┡━₁ always
|
||||||
|
│ └─₀ (U = bool)
|
||||||
|
│ ┡━₁ always
|
||||||
|
│ └─₀ never
|
||||||
|
└─₀ never
|
||||||
|
"#}
|
||||||
|
.trim_end();
|
||||||
|
|
||||||
|
let db = setup_db();
|
||||||
|
let t = BoundTypeVarInstance::synthetic(&db, "T", TypeVarVariance::Invariant);
|
||||||
|
let u = BoundTypeVarInstance::synthetic(&db, "U", TypeVarVariance::Invariant);
|
||||||
|
let bool_type = KnownClass::Bool.to_instance(&db);
|
||||||
|
let str_type = KnownClass::Str.to_instance(&db);
|
||||||
|
let t_str = ConstraintSet::range(&db, str_type, t.identity(&db), str_type);
|
||||||
|
let t_bool = ConstraintSet::range(&db, bool_type, t.identity(&db), bool_type);
|
||||||
|
let u_str = ConstraintSet::range(&db, str_type, u.identity(&db), str_type);
|
||||||
|
let u_bool = ConstraintSet::range(&db, bool_type, u.identity(&db), bool_type);
|
||||||
|
let constraints = (t_str.or(&db, || t_bool)).and(&db, || u_str.or(&db, || u_bool));
|
||||||
|
let actual = constraints.node.display_graph(&db, &"").to_string();
|
||||||
|
assert_eq!(actual, expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue