use super::*;
pub(super) enum StripOutcome {
Stripped(CnfFormula, VariableStripping),
Nothing,
Incomplete,
}
pub(super) fn strip_once(formula: &CnfFormula) -> StripOutcome {
let (forced_vars, backbone) = collect_forced_vars(formula);
let dead_vars = collect_dead_vars(formula, &forced_vars);
if backbone.is_empty() && (dead_vars.is_empty() || dead_vars.len() == formula.num_vars as usize)
{
return StripOutcome::Nothing;
}
let renumbering = Renumber::keeping(formula.num_vars as usize, |v| {
!forced_vars.contains(&v) && !dead_vars.contains(&v)
});
let Some(stripped_clauses) = rewrite_clauses(formula, &forced_vars, &renumbering) else {
return StripOutcome::Incomplete;
};
let stripped = CnfFormula {
num_vars: renumbering.num_new_vars(),
clauses: stripped_clauses,
};
if !dead_vars.is_empty() {
diag!(
"[free-variable-stripping] {} vars with zero occurrences removed",
dead_vars.len(),
);
}
let mut dead_sorted: Vec<VarId> = dead_vars.into_iter().collect();
dead_sorted.sort_unstable_by_key(|v| v.0);
let reduction = VariableStripping {
backbone,
dead: dead_sorted,
renumbering,
};
StripOutcome::Stripped(stripped, reduction)
}
pub(super) fn strip_backbone_vars(formula: &CnfFormula) -> Option<(CnfFormula, VariableStripping)> {
match strip_once(formula) {
StripOutcome::Stripped(f, r) => Some((f, r)),
StripOutcome::Nothing => None,
StripOutcome::Incomplete => {
let (propagated, forced_lits) =
crate::preprocess::unit_propagation::propagate(&formula.clauses, formula.num_vars);
if crate::cnf::contains_empty_clause(&propagated) {
diag!(
"[backbone-stripping] skipped: unit-propagation cleanup found UNSAT; \
compiling the un-stripped formula",
);
return None;
}
let mut clauses = propagated;
clauses.extend(forced_lits.iter().map(|l| Clause::new(vec![*l])));
let cleaned = CnfFormula {
num_vars: formula.num_vars,
clauses,
};
match strip_once(&cleaned) {
StripOutcome::Stripped(f, r) => {
diag!(
"[backbone-stripping] recovered via unit-propagation cleanup \
(incomplete preprocessing): {} → {} vars",
formula.num_vars,
f.num_vars,
);
Some((f, r))
}
StripOutcome::Nothing | StripOutcome::Incomplete => None,
}
}
}
}
pub(super) fn is_backbone_unit(
clause: &Clause,
forced_vars: &std::collections::HashSet<VarId>,
) -> bool {
clause.literals.len() == 1 && forced_vars.contains(&clause.literals[0].var)
}
pub(super) fn collect_forced_vars(
formula: &CnfFormula,
) -> (std::collections::HashSet<VarId>, Vec<(VarId, bool)>) {
let mut forced_vars = std::collections::HashSet::new();
let mut backbone = Vec::new();
for clause in &formula.clauses {
if clause.literals.len() == 1 {
let lit = clause.literals[0];
if forced_vars.insert(lit.var) {
backbone.push((lit.var, lit.positive));
}
}
}
(forced_vars, backbone)
}
pub(super) fn collect_dead_vars(
formula: &CnfFormula,
forced_vars: &std::collections::HashSet<VarId>,
) -> std::collections::HashSet<VarId> {
let mut var_occurs = vec![false; formula.num_vars as usize];
for clause in &formula.clauses {
if is_backbone_unit(clause, forced_vars) {
continue;
}
for lit in &clause.literals {
var_occurs[lit.var.idx()] = true;
}
}
(0..formula.num_vars)
.map(VarId)
.filter(|v| !forced_vars.contains(v) && !var_occurs[v.idx()])
.collect()
}
pub(super) fn rewrite_clauses(
formula: &CnfFormula,
forced_vars: &std::collections::HashSet<VarId>,
renumbering: &Renumber,
) -> Option<Vec<Clause>> {
let mut out = Vec::new();
for clause in &formula.clauses {
if is_backbone_unit(clause, forced_vars) {
continue;
}
let mut new_lits = Vec::with_capacity(clause.literals.len());
for lit in &clause.literals {
new_lits.push(renumbering.apply_lit(*lit)?);
}
out.push(Clause::new(new_lits));
}
Some(out)
}
pub(super) fn apply_equiv_reduction(
formula: &CnfFormula,
mapping: Option<EquivMapping>,
reduce_equivalences: bool,
) -> Option<EquivReduction> {
if !reduce_equivalences {
return None;
}
let mapping = mapping?;
let (reduced, renumbering) = mapping.reduce_formula(formula);
diag!(
"[equiv-reduction] {} → {} representative vars",
formula.num_vars,
reduced.num_vars,
);
Some(EquivReduction {
formula: reduced,
mapping,
renumbering,
})
}