#![allow(missing_docs)]
#![allow(dead_code)]
#[allow(unused_imports)]
use crate::prelude::*;
use core::cmp::Ordering;
pub type TermId = u32;
pub type TheoryId = u32;
pub type DecisionLevel = u32;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Equality {
pub lhs: TermId,
pub rhs: TermId,
}
impl Equality {
pub fn new(lhs: TermId, rhs: TermId) -> Self {
if lhs <= rhs {
Self { lhs, rhs }
} else {
Self { lhs: rhs, rhs: lhs }
}
}
pub fn flip(self) -> Self {
Self::new(self.rhs, self.lhs)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct Disequality {
pub lhs: TermId,
pub rhs: TermId,
}
impl Disequality {
pub fn new(lhs: TermId, rhs: TermId) -> Self {
if lhs <= rhs {
Self { lhs, rhs }
} else {
Self { lhs: rhs, rhs: lhs }
}
}
}
#[derive(Debug, Clone)]
pub enum EqualityExplanation {
Given,
TheoryPropagation(TheoryId, Vec<Equality>),
Transitivity(Vec<Equality>),
Congruence {
lhs_app: TermId,
rhs_app: TermId,
arg_equalities: Vec<Equality>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EqualityPriority {
pub level: u32,
pub relevancy: u32,
pub decision_level: DecisionLevel,
}
impl Ord for EqualityPriority {
fn cmp(&self, other: &Self) -> Ordering {
self.level
.cmp(&other.level)
.then_with(|| self.relevancy.cmp(&other.relevancy))
.then_with(|| other.decision_level.cmp(&self.decision_level))
}
}
impl PartialOrd for EqualityPriority {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
#[derive(Debug, Clone)]
struct PendingEquality {
equality: Equality,
priority: EqualityPriority,
explanation: EqualityExplanation,
source_theory: TheoryId,
}
impl PartialEq for PendingEquality {
fn eq(&self, other: &Self) -> bool {
self.priority == other.priority
}
}
impl Eq for PendingEquality {}
impl Ord for PendingEquality {
fn cmp(&self, other: &Self) -> Ordering {
self.priority.cmp(&other.priority)
}
}
impl PartialOrd for PendingEquality {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
#[derive(Debug, Clone)]
pub struct TermPartition {
classes: Vec<FxHashSet<TermId>>,
term_to_class: FxHashMap<TermId, usize>,
}
impl TermPartition {
pub fn new(terms: &[TermId]) -> Self {
let classes: Vec<_> = terms
.iter()
.map(|&t| {
let mut set = FxHashSet::default();
set.insert(t);
set
})
.collect();
let term_to_class: FxHashMap<_, _> =
terms.iter().enumerate().map(|(i, &t)| (t, i)).collect();
Self {
classes,
term_to_class,
}
}
pub fn merge(&mut self, t1: TermId, t2: TermId) -> Result<(), String> {
let c1 = *self.term_to_class.get(&t1).ok_or("Term not in partition")?;
let c2 = *self.term_to_class.get(&t2).ok_or("Term not in partition")?;
if c1 == c2 {
return Ok(());
}
let (src, dst) = if self.classes[c1].len() < self.classes[c2].len() {
(c1, c2)
} else {
(c2, c1)
};
let src_terms: Vec<_> = self.classes[src].iter().copied().collect();
for term in src_terms {
self.classes[dst].insert(term);
self.term_to_class.insert(term, dst);
}
self.classes[src].clear();
Ok(())
}
pub fn get_equalities(&self) -> Vec<Equality> {
let mut equalities = Vec::new();
for class in &self.classes {
if class.len() > 1 {
let terms: Vec<_> = class.iter().copied().collect();
for i in 0..terms.len() {
for j in (i + 1)..terms.len() {
equalities.push(Equality::new(terms[i], terms[j]));
}
}
}
}
equalities
}
pub fn num_classes(&self) -> usize {
self.classes.iter().filter(|c| !c.is_empty()).count()
}
pub fn are_equal(&self, t1: TermId, t2: TermId) -> bool {
if let (Some(&c1), Some(&c2)) = (self.term_to_class.get(&t1), self.term_to_class.get(&t2)) {
c1 == c2
} else {
false
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TheoryProperties {
pub is_convex: bool,
pub is_stably_infinite: bool,
pub is_incremental: bool,
pub has_explanations: bool,
pub priority: u32,
}
impl Default for TheoryProperties {
fn default() -> Self {
Self {
is_convex: true,
is_stably_infinite: true,
is_incremental: true,
has_explanations: true,
priority: 100,
}
}
}
#[derive(Debug, Clone)]
pub struct AdvancedNelsonOppenConfig {
pub delayed_sharing: bool,
pub model_based_combination: bool,
pub max_case_splits: u32,
pub conflict_driven_learning: bool,
pub relevancy_tracking: bool,
pub max_iterations: u32,
pub propagation_batch_size: usize,
pub incremental_mode: bool,
}
impl Default for AdvancedNelsonOppenConfig {
fn default() -> Self {
Self {
delayed_sharing: true,
model_based_combination: true,
max_case_splits: 100,
conflict_driven_learning: true,
relevancy_tracking: true,
max_iterations: 10000,
propagation_batch_size: 100,
incremental_mode: true,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct AdvancedNelsonOppenStats {
pub iterations: u64,
pub equalities_propagated: u64,
pub delayed_equalities: u64,
pub case_splits: u64,
pub conflicts: u64,
pub theory_calls: u64,
pub backtracks: u64,
pub learned_arrangements: u64,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum CombinationResult {
Sat,
Unsat(Vec<Equality>),
Unknown,
ResourceExceeded,
}
#[derive(Debug, Clone)]
pub struct TheoryConflict {
pub theory: TheoryId,
pub equalities: Vec<Equality>,
pub explanation: Vec<EqualityExplanation>,
}
pub trait TheorySolver {
fn theory_id(&self) -> TheoryId;
fn properties(&self) -> TheoryProperties;
fn assert_equality(&mut self, eq: Equality, level: DecisionLevel) -> Result<(), String>;
fn assert_disequality(
&mut self,
diseq: Disequality,
level: DecisionLevel,
) -> Result<(), String>;
fn check_satisfiability(&mut self) -> Result<CombinationResult, String>;
fn get_implied_equalities(&self) -> Vec<(Equality, EqualityExplanation)>;
fn backtrack(&mut self, level: DecisionLevel) -> Result<(), String>;
fn decision_level(&self) -> DecisionLevel;
fn reset(&mut self);
}
pub struct AdvancedNelsonOppen {
config: AdvancedNelsonOppenConfig,
stats: AdvancedNelsonOppenStats,
theory_properties: FxHashMap<TheoryId, TheoryProperties>,
shared_terms: FxHashSet<TermId>,
asserted_equalities: FxHashMap<DecisionLevel, Vec<Equality>>,
pending_equalities: BinaryHeap<PendingEquality>,
propagated_equalities: FxHashSet<Equality>,
decision_level: DecisionLevel,
union_find: UnionFindWithExplanation,
learned_arrangements: Vec<Vec<Equality>>,
relevancy_scores: FxHashMap<TermId, u32>,
case_split_stack: Vec<CaseSplit>,
conflicts: Vec<TheoryConflict>,
}
#[derive(Debug, Clone)]
pub struct UnionFindWithExplanation {
pub parent: FxHashMap<TermId, TermId>,
pub rank: FxHashMap<TermId, usize>,
pub explanations: FxHashMap<Equality, EqualityExplanation>,
pub decision_levels: FxHashMap<Equality, DecisionLevel>,
}
impl UnionFindWithExplanation {
fn new() -> Self {
Self {
parent: FxHashMap::default(),
rank: FxHashMap::default(),
explanations: FxHashMap::default(),
decision_levels: FxHashMap::default(),
}
}
fn find(&mut self, term: TermId) -> TermId {
if let Some(&parent) = self.parent.get(&term)
&& parent != term
{
let root = self.find(parent);
self.parent.insert(term, root);
return root;
}
term
}
fn union(
&mut self,
lhs: TermId,
rhs: TermId,
explanation: EqualityExplanation,
level: DecisionLevel,
) -> bool {
let lhs_root = self.find(lhs);
let rhs_root = self.find(rhs);
if lhs_root == rhs_root {
return false;
}
let lhs_rank = self.rank.get(&lhs_root).copied().unwrap_or(0);
let rhs_rank = self.rank.get(&rhs_root).copied().unwrap_or(0);
let (child, parent) = if lhs_rank < rhs_rank {
(lhs_root, rhs_root)
} else if lhs_rank > rhs_rank {
(rhs_root, lhs_root)
} else {
self.rank.insert(rhs_root, rhs_rank + 1);
(lhs_root, rhs_root)
};
self.parent.insert(child, parent);
let eq = Equality::new(lhs, rhs);
self.explanations.insert(eq, explanation);
self.decision_levels.insert(eq, level);
true
}
fn are_equal(&mut self, lhs: TermId, rhs: TermId) -> bool {
self.find(lhs) == self.find(rhs)
}
fn get_explanation(&self, eq: &Equality) -> Option<&EqualityExplanation> {
self.explanations.get(eq)
}
fn get_decision_level(&self, eq: &Equality) -> Option<DecisionLevel> {
self.decision_levels.get(eq).copied()
}
fn backtrack(&mut self, level: DecisionLevel) {
let to_remove: Vec<_> = self
.decision_levels
.iter()
.filter(|(_, l)| **l > level)
.map(|(eq, _)| *eq)
.collect();
for eq in to_remove {
self.explanations.remove(&eq);
self.decision_levels.remove(&eq);
}
let remaining: Vec<_> = self.explanations.keys().copied().collect();
self.parent.clear();
self.rank.clear();
for eq in remaining {
if let Some(explanation) = self.explanations.get(&eq).cloned()
&& let Some(&level) = self.decision_levels.get(&eq)
{
self.union(eq.lhs, eq.rhs, explanation, level);
}
}
}
fn clear(&mut self) {
self.parent.clear();
self.rank.clear();
self.explanations.clear();
self.decision_levels.clear();
}
}
#[derive(Debug, Clone)]
pub struct CaseSplit {
pub level: DecisionLevel,
pub terms: Vec<TermId>,
pub partition: TermPartition,
pub tried_arrangements: Vec<Vec<Equality>>,
}
impl AdvancedNelsonOppen {
pub fn new() -> Self {
Self::with_config(AdvancedNelsonOppenConfig::default())
}
pub fn with_config(config: AdvancedNelsonOppenConfig) -> Self {
Self {
config,
stats: AdvancedNelsonOppenStats::default(),
theory_properties: FxHashMap::default(),
shared_terms: FxHashSet::default(),
asserted_equalities: FxHashMap::default(),
pending_equalities: BinaryHeap::new(),
propagated_equalities: FxHashSet::default(),
decision_level: 0,
union_find: UnionFindWithExplanation::new(),
learned_arrangements: Vec::new(),
relevancy_scores: FxHashMap::default(),
case_split_stack: Vec::new(),
conflicts: Vec::new(),
}
}
pub fn register_theory(&mut self, theory_id: TheoryId, properties: TheoryProperties) {
self.theory_properties.insert(theory_id, properties);
}
pub fn add_shared_term(&mut self, term: TermId) {
self.shared_terms.insert(term);
}
pub fn stats(&self) -> &AdvancedNelsonOppenStats {
&self.stats
}
pub fn current_decision_level(&self) -> DecisionLevel {
self.decision_level
}
pub fn push_decision_level(&mut self) {
self.decision_level += 1;
}
pub fn backtrack(&mut self, level: DecisionLevel) -> Result<(), String> {
if level > self.decision_level {
return Err("Cannot backtrack to future level".to_string());
}
let levels_to_remove: Vec<_> = self
.asserted_equalities
.keys()
.filter(|&&l| l > level)
.copied()
.collect();
for l in levels_to_remove {
self.asserted_equalities.remove(&l);
}
self.union_find.backtrack(level);
let pending: Vec<_> = self.pending_equalities.drain().collect();
for p in pending {
if p.priority.decision_level <= level {
self.pending_equalities.push(p);
}
}
while let Some(split) = self.case_split_stack.last() {
if split.level > level {
self.case_split_stack.pop();
} else {
break;
}
}
self.decision_level = level;
self.stats.backtracks += 1;
Ok(())
}
pub fn assert_equality(
&mut self,
eq: Equality,
explanation: EqualityExplanation,
) -> Result<(), String> {
if self.union_find.are_equal(eq.lhs, eq.rhs) {
return Ok(());
}
self.union_find
.union(eq.lhs, eq.rhs, explanation, self.decision_level);
self.asserted_equalities
.entry(self.decision_level)
.or_default()
.push(eq);
Ok(())
}
pub fn add_pending_equality(
&mut self,
equality: Equality,
source_theory: TheoryId,
explanation: EqualityExplanation,
priority_level: u32,
) {
if self.propagated_equalities.contains(&equality) {
return;
}
let relevancy = self.compute_relevancy(&equality);
let pending = PendingEquality {
equality,
priority: EqualityPriority {
level: priority_level,
relevancy,
decision_level: self.decision_level,
},
explanation,
source_theory,
};
self.pending_equalities.push(pending);
self.stats.delayed_equalities += 1;
}
pub fn propagate_equalities(
&mut self,
theories: &mut [&mut dyn TheorySolver],
) -> Result<CombinationResult, String> {
let mut count = 0;
while let Some(pending) = self.pending_equalities.pop() {
if self.propagated_equalities.contains(&pending.equality) {
continue;
}
self.assert_equality(pending.equality, pending.explanation.clone())?;
for theory in theories.iter_mut() {
if theory.theory_id() != pending.source_theory {
theory.assert_equality(pending.equality, self.decision_level)?;
self.stats.theory_calls += 1;
}
}
self.propagated_equalities.insert(pending.equality);
self.stats.equalities_propagated += 1;
count += 1;
if count >= self.config.propagation_batch_size {
break;
}
}
Ok(CombinationResult::Sat)
}
pub fn combine(
&mut self,
theories: &mut [&mut dyn TheorySolver],
) -> Result<CombinationResult, String> {
for _iteration in 0..self.config.max_iterations {
self.stats.iterations += 1;
let prop_result = self.propagate_equalities(theories)?;
if let CombinationResult::Unsat(conflict) = prop_result {
self.stats.conflicts += 1;
return Ok(CombinationResult::Unsat(conflict));
}
let mut changed = false;
let mut non_convex_theory_id = None;
for theory in theories.iter_mut() {
self.stats.theory_calls += 1;
let result = theory.check_satisfiability()?;
match result {
CombinationResult::Unsat(conflict) => {
self.stats.conflicts += 1;
if self.config.conflict_driven_learning {
self.learn_conflict_clause(&conflict);
}
return Ok(CombinationResult::Unsat(conflict));
}
CombinationResult::Sat => {
let properties = self
.theory_properties
.get(&theory.theory_id())
.copied()
.unwrap_or_default();
let implied = theory.get_implied_equalities();
for (eq, explanation) in implied {
if self.config.delayed_sharing {
self.add_pending_equality(
eq,
theory.theory_id(),
explanation,
properties.priority,
);
} else {
self.assert_equality(eq, explanation)?;
changed = true;
}
}
if !properties.is_convex
&& self.config.model_based_combination
&& self.needs_case_split(theory.theory_id())
{
non_convex_theory_id = Some(theory.theory_id());
break;
}
}
CombinationResult::Unknown => {}
CombinationResult::ResourceExceeded => {
return Ok(CombinationResult::ResourceExceeded);
}
}
}
if let Some(theory_id) = non_convex_theory_id {
return self.handle_non_convex_theory(theories, theory_id);
}
if !changed && self.pending_equalities.is_empty() {
return Ok(CombinationResult::Sat);
}
}
Ok(CombinationResult::Unknown)
}
fn handle_non_convex_theory(
&mut self,
_theories: &mut [&mut dyn TheorySolver],
_theory_id: TheoryId,
) -> Result<CombinationResult, String> {
if self.stats.case_splits >= self.config.max_case_splits as u64 {
return Ok(CombinationResult::Unknown);
}
self.stats.case_splits += 1;
Ok(CombinationResult::Sat)
}
fn needs_case_split(&self, _theory_id: TheoryId) -> bool {
false
}
fn learn_conflict_clause(&mut self, conflict: &[Equality]) {
self.learned_arrangements.push(conflict.to_vec());
self.stats.learned_arrangements += 1;
}
fn compute_relevancy(&self, eq: &Equality) -> u32 {
if !self.config.relevancy_tracking {
return 100;
}
let lhs_relevancy = self.relevancy_scores.get(&eq.lhs).copied().unwrap_or(0);
let rhs_relevancy = self.relevancy_scores.get(&eq.rhs).copied().unwrap_or(0);
lhs_relevancy.max(rhs_relevancy)
}
pub fn update_relevancy(&mut self, term: TermId, score: u32) {
self.relevancy_scores.insert(term, score);
}
pub fn generate_arrangements(&self, terms: &[TermId]) -> Vec<TermPartition> {
if terms.is_empty() {
return vec![TermPartition::new(&[])];
}
vec![TermPartition::new(terms)]
}
pub fn refine_partition(
&self,
partition: &mut TermPartition,
constraints: &[Equality],
) -> Result<(), String> {
for eq in constraints {
partition.merge(eq.lhs, eq.rhs)?;
}
Ok(())
}
pub fn clear(&mut self) {
self.shared_terms.clear();
self.asserted_equalities.clear();
self.pending_equalities.clear();
self.propagated_equalities.clear();
self.decision_level = 0;
self.union_find.clear();
self.case_split_stack.clear();
self.conflicts.clear();
}
pub fn reset_stats(&mut self) {
self.stats = AdvancedNelsonOppenStats::default();
}
}
impl Default for AdvancedNelsonOppen {
fn default() -> Self {
Self::new()
}
}
pub struct PartitionRefinement {
partition: TermPartition,
history: Vec<TermPartition>,
}
impl PartitionRefinement {
pub fn new(terms: &[TermId]) -> Self {
let partition = TermPartition::new(terms);
Self {
partition,
history: Vec::new(),
}
}
pub fn refine_with_equality(&mut self, eq: Equality) -> Result<(), String> {
self.history.push(self.partition.clone());
self.partition.merge(eq.lhs, eq.rhs)
}
pub fn current_partition(&self) -> &TermPartition {
&self.partition
}
pub fn backtrack(&mut self) -> Result<(), String> {
self.partition = self.history.pop().ok_or("No refinement to backtrack")?;
Ok(())
}
}
pub struct ModelBasedCombination {
model: FxHashMap<TermId, TermId>,
model_queue: VecDeque<(TheoryId, Vec<(TermId, TermId)>)>,
}
impl ModelBasedCombination {
pub fn new() -> Self {
Self {
model: FxHashMap::default(),
model_queue: VecDeque::new(),
}
}
pub fn add_theory_model(&mut self, theory_id: TheoryId, assignments: Vec<(TermId, TermId)>) {
self.model_queue.push_back((theory_id, assignments));
}
pub fn merge_models(&mut self) -> Result<FxHashMap<TermId, TermId>, String> {
self.model.clear();
while let Some((_theory_id, assignments)) = self.model_queue.pop_front() {
for (term, value) in assignments {
if let Some(&existing_value) = self.model.get(&term)
&& existing_value != value
{
return Err("Model conflict".to_string());
}
self.model.insert(term, value);
}
}
Ok(self.model.clone())
}
pub fn clear(&mut self) {
self.model.clear();
self.model_queue.clear();
}
}
impl Default for ModelBasedCombination {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_equality_creation() {
let eq1 = Equality::new(1, 2);
let eq2 = Equality::new(2, 1);
assert_eq!(eq1, eq2);
}
#[test]
fn test_partition_creation() {
let terms = vec![1, 2, 3, 4];
let partition = TermPartition::new(&terms);
assert_eq!(partition.num_classes(), 4);
}
#[test]
fn test_partition_merge() {
let terms = vec![1, 2, 3, 4];
let mut partition = TermPartition::new(&terms);
partition.merge(1, 2).expect("Merge failed");
assert_eq!(partition.num_classes(), 3);
assert!(partition.are_equal(1, 2));
}
#[test]
fn test_partition_equalities() {
let terms = vec![1, 2, 3];
let mut partition = TermPartition::new(&terms);
partition.merge(1, 2).expect("Merge failed");
let equalities = partition.get_equalities();
assert_eq!(equalities.len(), 1);
assert_eq!(equalities[0], Equality::new(1, 2));
}
#[test]
fn test_union_find_creation() {
let mut uf = UnionFindWithExplanation::new();
assert!(!uf.are_equal(1, 2));
}
#[test]
fn test_union_find_union() {
let mut uf = UnionFindWithExplanation::new();
uf.union(1, 2, EqualityExplanation::Given, 0);
assert!(uf.are_equal(1, 2));
}
#[test]
fn test_union_find_transitivity() {
let mut uf = UnionFindWithExplanation::new();
uf.union(1, 2, EqualityExplanation::Given, 0);
uf.union(2, 3, EqualityExplanation::Given, 0);
assert!(uf.are_equal(1, 3));
}
#[test]
fn test_advanced_nelson_oppen_creation() {
let no = AdvancedNelsonOppen::new();
assert_eq!(no.stats().iterations, 0);
}
#[test]
fn test_register_theory() {
let mut no = AdvancedNelsonOppen::new();
let props = TheoryProperties::default();
no.register_theory(0, props);
}
#[test]
fn test_add_shared_term() {
let mut no = AdvancedNelsonOppen::new();
no.add_shared_term(1);
assert!(no.shared_terms.contains(&1));
}
#[test]
fn test_decision_level() {
let mut no = AdvancedNelsonOppen::new();
assert_eq!(no.current_decision_level(), 0);
no.push_decision_level();
assert_eq!(no.current_decision_level(), 1);
}
#[test]
fn test_backtrack() {
let mut no = AdvancedNelsonOppen::new();
no.push_decision_level();
no.push_decision_level();
assert_eq!(no.current_decision_level(), 2);
no.backtrack(1).expect("Backtrack failed");
assert_eq!(no.current_decision_level(), 1);
}
#[test]
fn test_assert_equality() {
let mut no = AdvancedNelsonOppen::new();
let eq = Equality::new(1, 2);
no.assert_equality(eq, EqualityExplanation::Given)
.expect("Assert failed");
assert!(no.union_find.are_equal(1, 2));
}
#[test]
fn test_pending_equality_priority() {
let p1 = EqualityPriority {
level: 10,
relevancy: 5,
decision_level: 0,
};
let p2 = EqualityPriority {
level: 20,
relevancy: 5,
decision_level: 0,
};
assert!(p2 > p1);
}
#[test]
fn test_partition_refinement() {
let terms = vec![1, 2, 3, 4];
let mut refinement = PartitionRefinement::new(&terms);
refinement
.refine_with_equality(Equality::new(1, 2))
.expect("Refinement failed");
assert!(refinement.current_partition().are_equal(1, 2));
}
#[test]
fn test_partition_refinement_backtrack() {
let terms = vec![1, 2, 3, 4];
let mut refinement = PartitionRefinement::new(&terms);
refinement
.refine_with_equality(Equality::new(1, 2))
.expect("Refinement failed");
refinement.backtrack().expect("Backtrack failed");
assert!(!refinement.current_partition().are_equal(1, 2));
}
#[test]
fn test_model_based_combination() {
let mut mbc = ModelBasedCombination::new();
mbc.add_theory_model(0, vec![(1, 10), (2, 20)]);
let model = mbc.merge_models().expect("Merge failed");
assert_eq!(model.get(&1), Some(&10));
assert_eq!(model.get(&2), Some(&20));
}
#[test]
fn test_model_conflict_detection() {
let mut mbc = ModelBasedCombination::new();
mbc.add_theory_model(0, vec![(1, 10)]);
mbc.add_theory_model(1, vec![(1, 20)]);
let result = mbc.merge_models();
assert!(result.is_err());
}
#[test]
fn test_theory_properties() {
let props = TheoryProperties {
is_convex: false,
is_stably_infinite: true,
..Default::default()
};
assert!(!props.is_convex);
assert!(props.is_stably_infinite);
}
#[test]
fn test_disequality_creation() {
let d1 = Disequality::new(1, 2);
let d2 = Disequality::new(2, 1);
assert_eq!(d1.lhs, d2.lhs);
assert_eq!(d1.rhs, d2.rhs);
}
}