use crate::chc::{ChcSystem, PredId, PredicateApp, Rule};
use crate::frames::LemmaId;
use crate::pdr::SpacerError;
use oxiz_core::{TermId, TermManager};
use smallvec::SmallVec;
use std::collections::{HashMap, HashSet};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum NonLinearError {
#[error("non-linear rule not supported: {0}")]
Unsupported(String),
#[error("interpolation failed: {0}")]
InterpolationFailed(String),
#[error("product construction failed: {0}")]
ProductConstructionFailed(String),
#[error("spacer error: {0}")]
Spacer(#[from] SpacerError),
}
pub type NonLinearResult<T> = Result<T, NonLinearError>;
#[derive(Debug, Clone)]
pub struct NonLinearRuleAnalysis {
pub rule_id: usize,
pub body_pred_count: usize,
pub body_preds: SmallVec<[PredId; 4]>,
pub head_pred: PredId,
pub nonlinearity_degree: usize,
pub is_cyclic: bool,
}
impl NonLinearRuleAnalysis {
pub fn analyze(rule: &Rule) -> Self {
let body_preds: SmallVec<[PredId; 4]> =
rule.body.predicates.iter().map(|app| app.pred).collect();
let body_pred_count = body_preds.len();
let nonlinearity_degree = body_pred_count.saturating_sub(1);
let head_pred = rule.head_predicate().unwrap_or(PredId(0));
Self {
rule_id: 0, body_pred_count,
body_preds,
head_pred,
nonlinearity_degree,
is_cyclic: false, }
}
pub fn is_linear(&self) -> bool {
self.body_pred_count <= 1
}
pub fn is_binary_nonlinear(&self) -> bool {
self.body_pred_count == 2
}
}
pub struct NonLinearAnalyzer {
rule_analyses: Vec<NonLinearRuleAnalysis>,
dependencies: HashMap<PredId, HashSet<PredId>>,
sccs: Vec<HashSet<PredId>>,
}
impl NonLinearAnalyzer {
pub fn new() -> Self {
Self {
rule_analyses: Vec::new(),
dependencies: HashMap::new(),
sccs: Vec::new(),
}
}
pub fn analyze(&mut self, system: &ChcSystem) -> NonLinearResult<()> {
self.rule_analyses.clear();
for (idx, rule) in system.rules().enumerate() {
let mut analysis = NonLinearRuleAnalysis::analyze(rule);
analysis.rule_id = idx;
self.rule_analyses.push(analysis);
}
self.build_dependency_graph(system);
self.compute_sccs();
self.mark_cyclic_rules();
Ok(())
}
pub fn statistics(&self) -> NonLinearStats {
let total_rules = self.rule_analyses.len();
let linear_rules = self.rule_analyses.iter().filter(|a| a.is_linear()).count();
let binary_nonlinear = self
.rule_analyses
.iter()
.filter(|a| a.is_binary_nonlinear())
.count();
let higher_order_nonlinear = self
.rule_analyses
.iter()
.filter(|a| a.nonlinearity_degree > 1)
.count();
let max_body_preds = self
.rule_analyses
.iter()
.map(|a| a.body_pred_count)
.max()
.unwrap_or(0);
NonLinearStats {
total_rules,
linear_rules,
binary_nonlinear,
higher_order_nonlinear,
max_body_preds,
num_sccs: self.sccs.len(),
}
}
fn build_dependency_graph(&mut self, system: &ChcSystem) {
self.dependencies.clear();
for rule in system.rules() {
if let Some(head_pred) = rule.head_predicate() {
for app in &rule.body.predicates {
self.dependencies
.entry(head_pred)
.or_default()
.insert(app.pred);
}
}
}
}
fn compute_sccs(&mut self) {
self.sccs.clear();
let mut all_preds: HashSet<PredId> = self.dependencies.keys().copied().collect();
for deps in self.dependencies.values() {
all_preds.extend(deps);
}
if all_preds.is_empty() {
return;
}
let mut index = 0u32;
let mut stack = Vec::new();
let mut indices: HashMap<PredId, u32> = HashMap::new();
let mut lowlinks: HashMap<PredId, u32> = HashMap::new();
let mut on_stack: HashSet<PredId> = HashSet::new();
for &pred in &all_preds {
if !indices.contains_key(&pred) {
self.tarjan_strongconnect(
pred,
&mut index,
&mut stack,
&mut indices,
&mut lowlinks,
&mut on_stack,
);
}
}
}
fn tarjan_strongconnect(
&mut self,
pred: PredId,
index: &mut u32,
stack: &mut Vec<PredId>,
indices: &mut HashMap<PredId, u32>,
lowlinks: &mut HashMap<PredId, u32>,
on_stack: &mut HashSet<PredId>,
) {
indices.insert(pred, *index);
lowlinks.insert(pred, *index);
*index += 1;
stack.push(pred);
on_stack.insert(pred);
let successors: Vec<PredId> = self
.dependencies
.get(&pred)
.map(|s| s.iter().copied().collect())
.unwrap_or_default();
for succ in successors {
if !indices.contains_key(&succ) {
self.tarjan_strongconnect(succ, index, stack, indices, lowlinks, on_stack);
let succ_lowlink = *lowlinks
.get(&succ)
.expect("lowlink exists for visited node");
let pred_lowlink = lowlinks.get(&pred).expect("lowlink exists for predecessor");
lowlinks.insert(pred, (*pred_lowlink).min(succ_lowlink));
} else if on_stack.contains(&succ) {
let succ_index = *indices.get(&succ).expect("index exists for visited node");
let pred_lowlink = lowlinks.get(&pred).expect("lowlink exists for predecessor");
lowlinks.insert(pred, (*pred_lowlink).min(succ_index));
}
}
if lowlinks.get(&pred) == indices.get(&pred) {
let mut scc = HashSet::new();
loop {
let w = stack.pop().expect("collection validated to be non-empty");
on_stack.remove(&w);
scc.insert(w);
if w == pred {
break;
}
}
self.sccs.push(scc);
}
}
fn mark_cyclic_rules(&mut self) {
for analysis in &mut self.rule_analyses {
analysis.is_cyclic = self.sccs.iter().any(|scc| {
scc.contains(&analysis.head_pred)
&& analysis.body_preds.iter().any(|p| scc.contains(p))
});
}
}
pub fn get_analysis(&self, rule_id: usize) -> Option<&NonLinearRuleAnalysis> {
self.rule_analyses.get(rule_id)
}
pub fn is_linear(&self) -> bool {
self.rule_analyses.iter().all(|a| a.is_linear())
}
}
impl Default for NonLinearAnalyzer {
fn default() -> Self {
Self::new()
}
}
#[derive(Debug, Clone)]
pub struct NonLinearStats {
pub total_rules: usize,
pub linear_rules: usize,
pub binary_nonlinear: usize,
pub higher_order_nonlinear: usize,
pub max_body_preds: usize,
pub num_sccs: usize,
}
#[derive(Debug, Clone)]
pub struct TreeInterpolant {
pub body_interpolants: Vec<TermId>,
pub combined: TermId,
pub tree_structure: InterpolantTree,
}
#[derive(Debug, Clone)]
pub enum InterpolantTree {
Leaf { pred: PredId, interpolant: TermId },
Binary {
left: Box<InterpolantTree>,
right: Box<InterpolantTree>,
combined: TermId,
},
}
pub struct NonLinearLemmaCombiner;
impl NonLinearLemmaCombiner {
pub fn combine_lemmas(
_terms: &mut TermManager,
_predicates: &[PredId],
_lemmas: &[LemmaId],
constraint: TermId,
) -> NonLinearResult<TermId> {
Ok(constraint)
}
pub fn compute_tree_interpolant(
terms: &mut TermManager,
body_apps: &[PredicateApp],
constraint: TermId,
) -> NonLinearResult<TreeInterpolant> {
if body_apps.is_empty() {
return Ok(TreeInterpolant {
body_interpolants: Vec::new(),
combined: terms.mk_true(),
tree_structure: InterpolantTree::Leaf {
pred: PredId(0),
interpolant: terms.mk_true(),
},
});
}
if body_apps.len() == 1 {
return Ok(TreeInterpolant {
body_interpolants: vec![constraint],
combined: constraint,
tree_structure: InterpolantTree::Leaf {
pred: body_apps[0].pred,
interpolant: constraint,
},
});
}
let mid = body_apps.len() / 2;
let left_apps = &body_apps[..mid];
let right_apps = &body_apps[mid..];
let left_tree = Self::compute_tree_interpolant(terms, left_apps, constraint)?;
let right_tree = Self::compute_tree_interpolant(terms, right_apps, constraint)?;
let combined_interpolant = constraint;
let mut body_interpolants = Vec::new();
body_interpolants.extend(left_tree.body_interpolants);
body_interpolants.extend(right_tree.body_interpolants);
Ok(TreeInterpolant {
body_interpolants,
combined: combined_interpolant,
tree_structure: InterpolantTree::Binary {
left: Box::new(left_tree.tree_structure),
right: Box::new(right_tree.tree_structure),
combined: combined_interpolant,
},
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::chc::{ChcSystem, PredicateApp, RuleBody, RuleHead};
use oxiz_core::TermManager;
#[test]
fn test_linear_rule_analysis() {
let mut terms = TermManager::new();
let mut system = ChcSystem::new();
let p = system.declare_predicate("P", [terms.sorts.int_sort]);
let x = terms.mk_var("x", terms.sorts.int_sort);
let zero = terms.mk_int(0);
let constraint = terms.mk_eq(x, zero);
let rule = Rule {
id: crate::chc::RuleId(0),
vars: vec![("x".to_string(), terms.sorts.int_sort)].into(),
body: RuleBody::init(constraint),
head: RuleHead::Predicate(PredicateApp::new(p, [x])),
name: None,
};
let analysis = NonLinearRuleAnalysis::analyze(&rule);
assert!(analysis.is_linear());
assert_eq!(analysis.nonlinearity_degree, 0);
}
#[test]
fn test_binary_nonlinear_rule_analysis() {
let mut terms = TermManager::new();
let mut system = ChcSystem::new();
let p = system.declare_predicate("P", [terms.sorts.int_sort]);
let x1 = terms.mk_var("x1", terms.sorts.int_sort);
let x2 = terms.mk_var("x2", terms.sorts.int_sort);
let rule = Rule {
id: crate::chc::RuleId(0),
vars: vec![
("x1".to_string(), terms.sorts.int_sort),
("x2".to_string(), terms.sorts.int_sort),
]
.into(),
body: RuleBody::new(
vec![PredicateApp::new(p, [x1]), PredicateApp::new(p, [x2])],
terms.mk_true(),
),
head: RuleHead::Predicate(PredicateApp::new(p, [terms.mk_add([x1, x2])])),
name: None,
};
let analysis = NonLinearRuleAnalysis::analyze(&rule);
assert!(!analysis.is_linear());
assert!(analysis.is_binary_nonlinear());
assert_eq!(analysis.nonlinearity_degree, 1);
assert_eq!(analysis.body_pred_count, 2);
}
#[test]
fn test_nonlinear_analyzer() {
let mut terms = TermManager::new();
let mut system = ChcSystem::new();
let p = system.declare_predicate("P", [terms.sorts.int_sort]);
let x = terms.mk_var("x", terms.sorts.int_sort);
let zero = terms.mk_int(0);
system.add_rule(
vec![("x".to_string(), terms.sorts.int_sort)],
RuleBody::init(terms.mk_eq(x, zero)),
RuleHead::Predicate(PredicateApp::new(p, [x])),
None,
);
let x1 = terms.mk_var("x1", terms.sorts.int_sort);
let x2 = terms.mk_var("x2", terms.sorts.int_sort);
system.add_rule(
vec![
("x1".to_string(), terms.sorts.int_sort),
("x2".to_string(), terms.sorts.int_sort),
],
RuleBody::new(
vec![PredicateApp::new(p, [x1]), PredicateApp::new(p, [x2])],
terms.mk_true(),
),
RuleHead::Predicate(PredicateApp::new(p, [terms.mk_add([x1, x2])])),
None,
);
let mut analyzer = NonLinearAnalyzer::new();
analyzer
.analyze(&system)
.expect("test operation should succeed");
let stats = analyzer.statistics();
assert_eq!(stats.total_rules, 2);
assert_eq!(stats.linear_rules, 1);
assert_eq!(stats.binary_nonlinear, 1);
assert_eq!(stats.max_body_preds, 2);
assert!(!analyzer.is_linear());
}
}