use std::collections::BTreeMap;
use crate::expr::{Expr, VarId};
use super::{Pattern, Tag, negated, scale_relation};
const MAX_VARS: usize = 4;
const MAX_ATOMS: usize = 5;
pub(super) struct MaskedGroupCollapse;
struct Term {
coefficient: u64,
conjuncts: Vec<Expr>,
}
fn as_term(e: &Expr) -> Option<Term> {
let (coefficient, core) = match e {
Expr::Scale(c, inner) => (*c, inner.as_ref()),
Expr::Const(_) => return None,
other => (1, other),
};
let conjuncts = match core {
Expr::And(children) => children.clone(),
other => vec![other.clone()],
};
Some(Term {
coefficient,
conjuncts,
})
}
fn may_relate(term: &Term) -> bool {
term.conjuncts
.iter()
.any(|c| matches!(c, Expr::Not(_)) || !c.is_bitwise())
}
fn intersect(a: &[Expr], b: &[Expr]) -> Vec<Expr> {
let mut remaining = b.to_vec();
let mut shared = Vec::new();
for factor in a {
if let Some(position) = remaining.iter().position(|other| other == factor) {
remaining.swap_remove(position);
shared.push(factor.clone());
}
}
shared
}
fn difference(all: &[Expr], shared: &[Expr]) -> Vec<Expr> {
let mut remaining = all.to_vec();
for factor in shared {
if let Some(position) = remaining.iter().position(|other| other == factor) {
remaining.swap_remove(position);
}
}
remaining
}
fn is_linear_mba(e: &Expr) -> bool {
let is_leaf = |t: &Expr| matches!(t, Expr::Const(_)) || t.is_bitwise();
let is_term = |t: &Expr| match t {
Expr::Scale(_, inner) => is_leaf(inner),
other => is_leaf(other),
};
match e {
Expr::Add(terms) => terms.iter().all(is_term),
other => is_term(other),
}
}
fn signature(e: &Expr, vars: &[VarId], mask: u64) -> Vec<u64> {
let width = vars.iter().map(|v| v.0 + 1).max().unwrap_or(0);
let mut values = vec![0u64; width];
let mut table = Vec::with_capacity(1 << vars.len());
for column in 0..(1usize << vars.len()) {
for (bit, var) in vars.iter().enumerate() {
values[var.0] = if (column >> bit) & 1 == 1 { mask } else { 0 };
}
table.push(e.eval_bits(&values).get(mask));
}
table
}
fn bitwise_signature(e: &Expr, vars: &[VarId], mask: u64) -> Option<Vec<u64>> {
if !is_linear_mba(e) {
return None;
}
let table = signature(e, vars, mask);
table
.iter()
.all(|value| *value == 0 || *value == mask)
.then_some(table)
}
fn arithmetic(e: &Expr, mask: u64) -> Expr {
match e {
Expr::Not(inner) => (-inner.as_ref().clone() - Expr::make_const(1)).reduce_masked(mask),
other => other.clone(),
}
}
fn proves_zero(conjuncts: &[Expr], vars: &[VarId], mask: u64) -> bool {
let mut candidates = conjuncts.to_vec();
let bitwise: Vec<Expr> = conjuncts
.iter()
.filter(|c| c.is_bitwise())
.cloned()
.collect();
if bitwise.len() >= 2 {
candidates.push(Expr::And(bitwise));
}
let mut signatures: Vec<Option<Option<Vec<u64>>>> = vec![None; candidates.len()];
for (i, conjunct) in candidates.iter().enumerate() {
let negation = negated(&arithmetic(conjunct, mask), mask);
let mut hypotheses = vec![(negation.clone(), negation, true)];
if let Expr::Not(inner) = conjunct {
let base = inner.as_ref().clone();
let bound = (base.clone() - Expr::make_const(1)).reduce_masked(mask);
hypotheses.push((base, bound, false));
}
for (base, bound, needs_even_multiple) in hypotheses {
let mut bound_signature = None;
for (j, multiple) in candidates.iter().enumerate() {
if j == i {
continue;
}
match scale_relation(multiple, &base, mask) {
Some(m) if m != 0 && (!needs_even_multiple || m & 1 == 0) => {}
_ => continue,
}
let bound_signature =
bound_signature.get_or_insert_with(|| bitwise_signature(&bound, vars, mask));
for (k, witness) in candidates.iter().enumerate() {
if k == i || k == j {
continue;
}
if scale_relation(witness, &bound, mask) == Some(1) {
return true;
}
if signatures[k].is_none() {
signatures[k] = Some(bitwise_signature(witness, vars, mask));
}
let (Some(witness_bits), Some(bound_bits)) = (
signatures[k].as_ref().expect("just filled"),
&*bound_signature,
) else {
continue;
};
if witness_bits
.iter()
.zip(bound_bits)
.all(|(w, b)| w & !b & mask == 0)
{
return true;
}
}
}
}
}
false
}
impl MaskedGroupCollapse {
fn group_vanishes(
group: &[usize],
parsed: &[Option<Term>],
shared: &[Expr],
vars: &[VarId],
mask: u64,
) -> bool {
let mut primitives: Vec<Expr> = Vec::new();
let mut atoms: Vec<(u64, usize)> = Vec::with_capacity(group.len());
for &index in group {
let term = parsed[index].as_ref().expect("group members are terms");
let mut selector = 0usize;
for factor in difference(&term.conjuncts, shared) {
let position = match primitives.iter().position(|other| *other == factor) {
Some(position) => position,
None => {
primitives.push(factor);
primitives.len() - 1
}
};
if primitives.len() > MAX_ATOMS {
return false;
}
selector |= 1 << position;
}
atoms.push((term.coefficient, selector));
}
if primitives.is_empty() {
return false;
}
for column in 0..(1usize << primitives.len()) {
let sum = atoms
.iter()
.filter(|(_, selector)| selector & !column == 0)
.fold(0u64, |acc, (coefficient, _)| acc.wrapping_add(*coefficient));
if sum & mask == 0 {
continue;
}
let mut conjuncts = shared.to_vec();
for (bit, primitive) in primitives.iter().enumerate() {
conjuncts.push(if (column >> bit) & 1 == 1 {
primitive.clone()
} else {
!primitive.clone()
});
}
if !proves_zero(&conjuncts, vars, mask) {
return false;
}
}
true
}
}
impl Pattern for MaskedGroupCollapse {
fn tags(&self) -> &'static [Tag] {
&[Tag::Add]
}
fn apply(&self, e: &Expr, mask: u64) -> Option<Expr> {
let Expr::Add(terms) = e else {
return None;
};
if terms.len() < 2 {
return None;
}
let mut vars: Vec<VarId> = e.get_vars().into_iter().collect();
if vars.is_empty() || vars.len() > MAX_VARS {
return None;
}
vars.sort_unstable();
let parsed: Vec<Option<Term>> = terms.iter().map(as_term).collect();
if !parsed.iter().flatten().any(may_relate) {
return None;
}
let mut occurrences: BTreeMap<&Expr, usize> = BTreeMap::new();
for term in parsed.iter().flatten() {
for factor in &term.conjuncts {
*occurrences.entry(factor).or_default() += 1;
}
}
let pivots: Vec<Expr> = occurrences
.into_iter()
.filter(|(_, count)| *count >= 2)
.map(|(factor, _)| factor.clone())
.collect();
for pivot in pivots {
let group: Vec<usize> = parsed
.iter()
.enumerate()
.filter(|(_, term)| term.as_ref().is_some_and(|t| t.conjuncts.contains(&pivot)))
.map(|(index, _)| index)
.collect();
if group.len() < 2 {
continue;
}
let mut shared = parsed[group[0]]
.as_ref()
.expect("group members are terms")
.conjuncts
.clone();
for &index in &group[1..] {
let term = parsed[index].as_ref().expect("group members are terms");
shared = intersect(&shared, &term.conjuncts);
}
if shared.is_empty() {
continue;
}
if !group
.iter()
.any(|&index| parsed[index].as_ref().is_some_and(may_relate))
{
continue;
}
if !Self::group_vanishes(&group, &parsed, &shared, &vars, mask) {
continue;
}
let mut kept: Vec<Expr> = terms
.iter()
.enumerate()
.filter(|(index, _)| !group.contains(index))
.map(|(_, term)| term.clone())
.collect();
return Some(match kept.len() {
0 => Expr::zero(),
1 => kept.remove(0),
_ => Expr::Add(kept),
});
}
None
}
}
#[cfg(all(test, feature = "parse"))]
mod ng_probe {
use super::*;
use crate::parser::parse_expr;
use crate::varint::make_mask;
fn diagnose(e: &Expr, mask: u64) -> String {
let Expr::Add(terms) = e else {
return "no-group: residue is not a sum".into();
};
let mut vars: Vec<VarId> = e.get_vars().into_iter().collect();
vars.sort_unstable();
let parsed: Vec<Option<Term>> = terms.iter().map(as_term).collect();
let mut occurrences: BTreeMap<&Expr, usize> = BTreeMap::new();
for term in parsed.iter().flatten() {
for factor in &term.conjuncts {
*occurrences.entry(factor).or_default() += 1;
}
}
let mut best = String::from("no-group: no conjunct is shared by two terms");
for (pivot, count) in occurrences {
if count < 2 {
continue;
}
let group: Vec<usize> = parsed
.iter()
.enumerate()
.filter(|(_, t)| t.as_ref().is_some_and(|t| t.conjuncts.contains(pivot)))
.map(|(index, _)| index)
.collect();
let mut shared = parsed[group[0]].as_ref().unwrap().conjuncts.clone();
for &index in &group[1..] {
shared = intersect(&shared, &parsed[index].as_ref().unwrap().conjuncts);
}
if shared.is_empty() {
continue;
}
let mut primitives: Vec<Expr> = Vec::new();
let mut atoms: Vec<(u64, usize)> = Vec::new();
for &index in &group {
let term = parsed[index].as_ref().unwrap();
let mut selector = 0usize;
for factor in difference(&term.conjuncts, &shared) {
let position =
primitives
.iter()
.position(|o| *o == factor)
.unwrap_or_else(|| {
primitives.push(factor.clone());
primitives.len() - 1
});
selector |= 1 << position;
}
atoms.push((term.coefficient, selector));
}
if primitives.len() > MAX_ATOMS {
best = format!("too-many-atoms: {} > {MAX_ATOMS}", primitives.len());
continue;
}
let mut failures = 0;
let mut undischarged: Vec<Expr> = Vec::new();
for column in 0..(1usize << primitives.len()) {
let sum = atoms
.iter()
.filter(|(_, s)| s & !column == 0)
.fold(0u64, |acc, (c, _)| acc.wrapping_add(*c));
if sum & mask == 0 {
continue;
}
let mut conjuncts = shared.clone();
for (bit, primitive) in primitives.iter().enumerate() {
conjuncts.push(if (column >> bit) & 1 == 1 {
primitive.clone()
} else {
!primitive.clone()
});
}
if !proves_zero(&conjuncts, &vars, mask) {
failures += 1;
undischarged.push(Expr::And(conjuncts));
}
}
if failures > 0 {
let loose = terms.len() - group.len();
best = if loose > 0 {
format!("loose-terms: {loose} term(s) outside the group")
} else {
let goals: Vec<String> = undischarged
.iter()
.map(|g| format!("\n 0 == {g}"))
.collect();
format!(
"undischarged-column: {failures} of {} column(s), {} primitive(s){}",
1usize << primitives.len(),
primitives.len(),
goals.concat()
)
};
}
}
best
}
#[test]
#[ignore = "diagnostic; needs a refreshed ng report"]
fn classify_surviving_residues() {
let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../ng/loki_tiny.csv.txt");
let report = std::fs::read_to_string(&path).expect("run the dataset tests first");
let mask = make_mask(64);
let mut cases = 0;
for line in report.lines() {
let Some(text) = line.trim().strip_prefix("diff-produced: ") else {
continue;
};
let residue = parse_expr(text).expect("ng report is parseable");
let reduced = residue.reduce_masked(mask);
cases += 1;
println!("{cases:3} {}", diagnose(&reduced, mask));
}
println!("total: {cases}");
}
}