use std::collections::{HashMap, HashSet};
use std::str::FromStr;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::OnceLock;
use pest::error::Error;
use regex::Regex;
use CType::{EQ, LE, LT};
use crate::datastructures::Assignment;
use crate::formulas::formula_cache::formula_factory_caches::FormulaFactoryCaches;
use crate::formulas::formula_cache::simple_cache::SimpleCache;
use crate::formulas::CType::{GE, GT};
use crate::formulas::Literal::Pos;
use crate::formulas::{AuxVarType, CType, CardinalityConstraint, EncodedFormula, FormulaFactoryConfig, Literal, PbConstraint, Variable};
use crate::operations::transformations::{self, CnfEncoder, Substitution};
use crate::parser::pseudo_boolean_parser::{parse, Rule};
use super::formula_cache::equivalence_cache::EquivalenceCache;
use super::formula_cache::formula_encoding::{Encoding, FormulaEncoding, SmallFormulaEncoding};
use super::formula_cache::implication_cache::ImplicationCache;
use super::formula_cache::nary_formula_cache::NaryFormulaCache;
use super::formula_cache::not_cache::NotCache;
use super::{Formula, FormulaType, LitType, VarType};
const FF_ID_LENGTH: i32 = 4;
pub(super) const AUX_PREFIX: &str = "@RESERVED_";
pub(super) const AUX_REGEX: &str = "^@RESERVED_(?P<FF_ID>[0-9A-Z]*)_(?P<AUX_TYPE>(CNF)|(CC)|(PB))_(?P<INDEX>[0-9]+)$";
static AUX_REGEX_LOCK: OnceLock<Regex> = OnceLock::new();
type FilterResult = (Vec<SmallFormulaEncoding>, HashSet<SmallFormulaEncoding>, Vec<FormulaEncoding>, HashSet<FormulaEncoding>);
pub struct FormulaFactory {
pub(crate) id: String,
pub config: FormulaFactoryConfig,
pub(crate) variables: SimpleCache<String>,
pub(crate) ands: NaryFormulaCache,
pub(crate) ors: NaryFormulaCache,
pub(crate) nots: NotCache,
pub(crate) impls: ImplicationCache,
pub(crate) equivs: EquivalenceCache,
pub(crate) ccs: SimpleCache<CardinalityConstraint>,
pub(crate) pbcs: SimpleCache<PbConstraint>,
cnf_counter: AtomicUsize,
cc_counter: AtomicUsize,
pb_counter: AtomicUsize,
pub(crate) caches: FormulaFactoryCaches,
}
impl Default for FormulaFactory {
fn default() -> Self {
Self::new()
}
}
impl FormulaFactory {
pub fn new() -> Self {
Self::with_id(&generate_random_id())
}
pub fn with_id(id: &str) -> Self {
Self {
id: id.into(),
config: FormulaFactoryConfig::new(),
variables: SimpleCache::new(),
ands: NaryFormulaCache::new(FormulaType::And),
ors: NaryFormulaCache::new(FormulaType::Or),
nots: NotCache::new(),
impls: ImplicationCache::new(),
equivs: EquivalenceCache::new(),
ccs: SimpleCache::new(),
pbcs: SimpleCache::new(),
cnf_counter: AtomicUsize::new(0),
cc_counter: AtomicUsize::new(0),
pb_counter: AtomicUsize::new(0),
caches: FormulaFactoryCaches::new(),
}
}
pub fn parse(&self, input: &str) -> Result<EncodedFormula, Box<Error<Rule>>> {
parse(self, input)
}
#[allow(clippy::unused_self)]
pub fn verum(&self) -> EncodedFormula {
EncodedFormula::from(FormulaEncoding::encode_type(FormulaType::True))
}
#[allow(clippy::unused_self)]
pub fn falsum(&self) -> EncodedFormula {
EncodedFormula::from(FormulaEncoding::encode_type(FormulaType::False))
}
pub fn constant(&self, value: bool) -> EncodedFormula {
if value {
self.verum()
} else {
self.falsum()
}
}
pub fn var(&self, name: &str) -> Variable {
Variable::try_from(self.variable(name)).unwrap()
}
pub fn variable(&self, name: &str) -> EncodedFormula {
EncodedFormula::from(self.variables.get_or_insert(name.into(), FormulaType::Lit(LitType::Pos(VarType::FF))))
}
pub fn parsed_variable(&self, name: &str) -> EncodedFormula {
let aux_regex = AUX_REGEX_LOCK.get_or_init(|| Regex::new(AUX_REGEX).unwrap());
aux_regex.captures(name).map_or_else(
|| self.variable(name),
|captures| {
if captures["FF_ID"] == self.id {
let aux_type = AuxVarType::from_str(&captures["AUX_TYPE"]).unwrap();
let index = captures["INDEX"].parse::<u64>().unwrap();
Pos(Variable::Aux(aux_type, index)).into()
} else {
self.variable(name)
}
},
)
}
pub fn lit(&self, name: &str, phase: bool) -> Literal {
Literal::new(self.var(name), phase)
}
pub fn literal(&self, name: &str, phase: bool) -> EncodedFormula {
EncodedFormula::from(self.lit(name, phase))
}
pub fn and(&self, operands: &[EncodedFormula]) -> EncodedFormula {
match self.filter(operands, FormulaType::And) {
None => self.falsum(),
Some((new_ops32, new_set32, new_ops64, new_set64)) => {
if new_ops32.is_empty() && new_ops64.is_empty() {
self.verum()
} else if new_ops32.len() == 1 && new_ops64.is_empty() {
new_ops32[0].to_formula()
} else if new_ops32.is_empty() && new_ops64.len() == 1 {
new_ops64[0].to_formula()
} else {
EncodedFormula::from(self.ands.get_or_insert(new_ops32, new_set32, new_ops64, new_set64))
}
}
}
}
pub fn or(&self, operands: &[EncodedFormula]) -> EncodedFormula {
match self.filter(operands, FormulaType::Or) {
None => self.verum(),
Some((new_ops32, new_set32, new_ops64, new_set64)) => {
if new_ops32.is_empty() && new_ops64.is_empty() {
self.falsum()
} else if new_ops32.len() == 1 && new_ops64.is_empty() {
new_ops32[0].to_formula()
} else if new_ops32.is_empty() && new_ops64.len() == 1 {
new_ops64[0].to_formula()
} else {
EncodedFormula::from(self.ors.get_or_insert(new_ops32, new_set32, new_ops64, new_set64))
}
}
}
}
pub fn clause(&self, operands: &[Literal]) -> EncodedFormula {
self.or(&operands.iter().map(|&lit| lit.into()).collect::<Box<[_]>>())
}
pub fn implication(&self, left: EncodedFormula, right: EncodedFormula) -> EncodedFormula {
if left.is_verum() {
right
} else if left.is_falsum() || right.is_verum() {
self.verum()
} else if right.is_falsum() {
self.not(left)
} else if left == right {
self.verum()
} else {
EncodedFormula::from(self.impls.get_or_insert((left, right)))
}
}
pub fn equivalence(&self, left: EncodedFormula, right: EncodedFormula) -> EncodedFormula {
if left.is_verum() {
right
} else if left.is_falsum() {
self.not(right)
} else if right.is_verum() {
left
} else if right.is_falsum() {
self.not(left)
} else if left == right {
self.verum()
} else if left == self.negate(right) {
self.falsum()
} else {
EncodedFormula::from(self.equivs.get_or_insert((left, right)))
}
}
pub fn not(&self, op: EncodedFormula) -> EncodedFormula {
match op.formula_type() {
FormulaType::False | FormulaType::True | FormulaType::Lit(_) | FormulaType::Not => self.negate(op),
_ => EncodedFormula::from(self.nots.get_or_insert(op)),
}
}
pub fn negate(&self, formula: EncodedFormula) -> EncodedFormula {
match formula.unpack(self) {
Formula::Pbc(pbc) => pbc.clone().negate(self),
Formula::Cc(cc) => cc.clone().negate(self),
Formula::Equiv(_) | Formula::Impl(_) | Formula::Or(_) | Formula::And(_) => self.not(formula),
Formula::Not(op) => op,
Formula::Lit(lit) => lit.negate().into(),
Formula::True => self.falsum(),
Formula::False => self.verum(),
}
}
#[allow(clippy::cast_possible_wrap)]
pub fn cc<V: Into<Box<[Variable]>>>(&self, comparator: CType, rhs: u64, variables: V) -> EncodedFormula {
assert!(is_cc(comparator, rhs as i64), "Given values do not represent a cardinality constraint.");
self.construct_cc_unsafe(comparator, rhs as i64, variables.into())
}
pub fn exo<V: Into<Box<[Variable]>>>(&self, variables: V) -> EncodedFormula {
self.cc(EQ, 1, variables)
}
pub fn amo<V: Into<Box<[Variable]>>>(&self, variables: V) -> EncodedFormula {
self.cc(LE, 1, variables)
}
pub fn pbc<L, C>(&self, comparator: CType, rhs: i64, literals: L, coefficients: C) -> EncodedFormula
where
L: Into<Box<[Literal]>>,
C: Into<Box<[i64]>>, {
let l = literals.into();
let c = coefficients.into();
assert_eq!(l.len(), c.len(), "The number of literals and coefficients in a pseudo-boolean constraint must be the same.");
if l.is_empty() {
self.constant(evaluate_trivial_pb_constraint(comparator, rhs))
} else if is_lit_cc(comparator, rhs, &l, &c) {
self.construct_cc_unsafe(comparator, rhs, l.iter().map(|&lit| lit.variable()).collect())
} else {
EncodedFormula::from(self.pbcs.get_or_insert(PbConstraint::new(l, c, comparator, rhs), FormulaType::Pbc))
}
}
#[must_use]
pub fn cnf_of(&self, formula: EncodedFormula) -> EncodedFormula {
CnfEncoder::stateless(self.config.cnf_config.clone()).transform(formula, self)
}
#[must_use]
pub fn nnf_of(&self, formula: EncodedFormula) -> EncodedFormula {
transformations::nnf(formula, self)
}
pub fn evaluate(&self, formula: EncodedFormula, assignment: &Assignment) -> bool {
let res = match formula.unpack(self) {
Formula::Pbc(pbc) => pbc.evaluate(assignment),
Formula::Cc(cc) => cc.evaluate(assignment),
Formula::Equiv((left, right)) => self.evaluate(left, assignment) == self.evaluate(right, assignment),
Formula::Impl((left, right)) => !self.evaluate(left, assignment) || self.evaluate(right, assignment),
Formula::Or(mut ops) => ops.any(|op| self.evaluate(op, assignment)),
Formula::And(mut ops) => ops.all(|op| self.evaluate(op, assignment)),
Formula::Not(op) => !self.evaluate(op, assignment),
Formula::Lit(lit) => assignment.evaluate_lit(lit),
Formula::True => true,
Formula::False => false,
};
res
}
pub fn restrict(&self, formula: EncodedFormula, assignment: &Assignment) -> EncodedFormula {
transformations::restrict(formula, assignment, self)
}
pub fn substitute(&self, formula: EncodedFormula, substitution: &Substitution) -> EncodedFormula {
transformations::substitute(formula, substitution, self)
}
pub fn substitute_var(&self, formula: EncodedFormula, variable: Variable, substitute: EncodedFormula) -> EncodedFormula {
let mut substitution = HashMap::new();
substitution.insert(variable, substitute);
self.substitute(formula, &substitution)
}
pub fn shrink_to_fit(&self) {
self.variables.shrink_to_fix();
self.ands.shrink_to_fit();
self.ors.shrink_to_fit();
self.nots.shrink_to_fit();
self.impls.shrink_to_fit();
self.equivs.shrink_to_fit();
}
pub fn number_of_cached_nodes(&self) -> usize {
self.variables.len() + self.ands.len() + self.ors.len() + self.nots.len() + self.impls.len() + self.equivs.len()
}
pub fn id(&self) -> String {
self.id.clone()
}
pub fn print_stats(&self) -> String {
format!(
"FormulaFactory Stats\n\
num_variables: {},\n\
num_ands: {},\n\
num_ors: {},\n\
num_nots: {},\n\
num_impls: {},\n\
num_equivs: {},\n\
cnf_counter: {},\n\
cc_counter: {}\
",
self.variables.len(),
self.ands.len(),
self.ors.len(),
self.nots.len(),
self.impls.len(),
self.equivs.len(),
self.cnf_counter.load(Ordering::SeqCst),
self.cc_counter.load(Ordering::SeqCst)
)
}
pub(crate) fn new_cnf_variable(&self) -> Variable {
let n = self.cnf_counter.fetch_add(1, Ordering::SeqCst);
Variable::Aux(AuxVarType::CNF, n as u64)
}
pub(crate) fn new_cc_variable(&self) -> Variable {
let n = self.cc_counter.fetch_add(1, Ordering::SeqCst);
Variable::Aux(AuxVarType::CC, n as u64)
}
pub(crate) fn new_pb_variable(&self) -> Variable {
let n = self.pb_counter.fetch_add(1, Ordering::SeqCst);
Variable::Aux(AuxVarType::PB, n as u64)
}
pub(crate) fn var_name(&self, index: FormulaEncoding) -> &str {
&self.variables[index]
}
#[allow(clippy::cast_sign_loss)]
fn construct_cc_unsafe(&self, comparator: CType, rhs: i64, variables: Box<[Variable]>) -> EncodedFormula {
if variables.is_empty() {
self.constant(evaluate_trivial_pb_constraint(comparator, rhs))
} else {
let (comp, r): (CType, u64) = if rhs >= 0 {
(comparator, rhs as u64)
} else {
assert_eq!(comparator, GT);
(GE, 0)
};
let cc = self.ccs.get_or_insert(CardinalityConstraint::new(variables, comp, r), FormulaType::Cc);
EncodedFormula::from(cc)
}
}
fn filter(&self, ops: &[EncodedFormula], op_type: FormulaType) -> Option<FilterResult> {
let flattened_ops = self.flatten_ops(ops, op_type);
let mut reduced32 = Vec::new();
let mut reduced_set32 = HashSet::new();
let mut reduced64 = Vec::new();
let mut reduced_set64 = HashSet::new();
for op in flattened_ops {
if op.is_verum() {
if op_type == FormulaType::Or {
return None;
}
} else if op.is_falsum() {
if op_type == FormulaType::And {
return None;
}
} else if op.is_type(op_type) {
for &sub_op in &*op.operands(self) {
let sub_encoded = sub_op.encoding;
if sub_encoded.is_large() {
if reduced_set64.insert(sub_encoded) {
reduced64.push(sub_encoded);
}
} else if reduced_set32.insert(sub_encoded.as_32()) {
reduced32.push(sub_encoded.as_32());
}
}
} else if self.contains_complement(&reduced_set32, &reduced_set64, op) {
return None;
} else {
let op_encoded = op.encoding;
if op_encoded.is_large() {
if reduced_set64.insert(op_encoded) {
reduced64.push(op_encoded);
}
} else if reduced_set32.insert(op_encoded.as_32()) {
reduced32.push(op_encoded.as_32());
}
};
}
Some((reduced32, reduced_set32, reduced64, reduced_set64))
}
fn flatten_ops(&self, ops: &[EncodedFormula], op_type: FormulaType) -> Vec<EncodedFormula> {
let mut nops = Vec::with_capacity(ops.len());
for &op in ops {
if op.is_type(op_type) {
nops.extend(self.flatten_ops(&op.operands(self), op_type));
} else {
nops.push(op);
}
}
nops
}
fn contains_complement(
&self,
set32: &HashSet<SmallFormulaEncoding>,
set64: &HashSet<FormulaEncoding>,
formula: EncodedFormula,
) -> bool {
use Formula::{False, Lit, Not, True};
match formula.unpack(self) {
True => {
let enc = &FormulaEncoding::encode_type(FormulaType::False);
set32.contains(&enc.as_32()) || set64.contains(enc)
}
False => {
let enc = &FormulaEncoding::encode_type(FormulaType::True);
set32.contains(&enc.as_32()) || set64.contains(enc)
}
Lit(lit) => {
let enc = &EncodedFormula::from(lit.negate()).encoding;
set32.contains(&enc.as_32()) || set64.contains(enc)
}
Not(op) => set32.contains(&op.encoding.as_32()) || set64.contains(&op.encoding),
_ => self.nots.lookup(formula).map(|not| set32.contains(¬.as_32()) || set64.contains(¬)) == Some(true),
}
}
}
const fn evaluate_trivial_pb_constraint(comparator: CType, rhs: i64) -> bool {
match comparator {
EQ => rhs == 0,
GT => rhs < 0,
GE => rhs <= 0,
LT => rhs > 0,
LE => rhs >= 0,
}
}
fn is_lit_cc(comparator: CType, rhs: i64, literals: &[Literal], coefficients: &[i64]) -> bool {
literals.iter().all(Literal::phase) && coefficients.iter().all(|&c| c == 1) && is_cc(comparator, rhs)
}
fn is_cc(comparator: CType, rhs: i64) -> bool {
comparator == LE && rhs >= 0
|| comparator == LT && rhs >= 1
|| comparator == GE && rhs >= 0
|| comparator == GT && rhs >= -1
|| comparator == EQ && rhs >= 0
}
fn generate_random_id() -> String {
(0..FF_ID_LENGTH).map(|_| fastrand::alphanumeric().to_uppercase().to_string()).collect::<String>()
}