use std::collections::BTreeMap;
#[derive(Debug, Clone)]
pub struct SatProblem {
pub num_vars: usize,
pub clauses: Vec<Vec<i32>>,
pub var_names: BTreeMap<usize, String>,
}
#[derive(Debug, Clone, serde::Serialize)]
pub enum SatResult {
Satisfiable {
assignment: BTreeMap<String, bool>,
},
Unsatisfiable {
conflict_clause: Vec<String>,
},
}
pub fn build_sat_problem(resources: &[String], deps: &[(String, String)]) -> SatProblem {
let mut var_map: BTreeMap<String, usize> = BTreeMap::new();
let mut var_names: BTreeMap<usize, String> = BTreeMap::new();
for (i, name) in resources.iter().enumerate() {
let idx = i + 1; var_map.insert(name.clone(), idx);
var_names.insert(idx, name.clone());
}
let mut clauses = Vec::with_capacity(deps.len() + resources.len());
for (dependent, dependency) in deps {
if let (Some(&a), Some(&b)) = (var_map.get(dependent), var_map.get(dependency)) {
clauses.push(vec![-(a as i32), b as i32]);
}
}
for &idx in var_map.values() {
clauses.push(vec![idx as i32]);
}
SatProblem {
num_vars: resources.len(),
clauses,
var_names,
}
}
pub fn solve(problem: &SatProblem) -> SatResult {
let mut assignment = vec![None; problem.num_vars + 1];
if dpll(&problem.clauses, &mut assignment, problem.num_vars) {
build_sat_result(&assignment, &problem.var_names)
} else {
build_unsat_result(&problem.clauses, &problem.var_names)
}
}
fn dpll(clauses: &[Vec<i32>], assignment: &mut [Option<bool>], num_vars: usize) -> bool {
let simplified = propagate_units(clauses, assignment);
if all_satisfied(&simplified, assignment) {
return true;
}
if has_empty_clause(&simplified, assignment) {
return false;
}
let var = pick_unassigned(assignment, num_vars);
let Some(var) = var else {
return all_satisfied(&simplified, assignment);
};
assignment[var] = Some(true);
if dpll(&simplified, assignment, num_vars) {
return true;
}
assignment[var] = Some(false);
if dpll(&simplified, assignment, num_vars) {
return true;
}
assignment[var] = None;
false
}
fn propagate_units(clauses: &[Vec<i32>], assignment: &mut [Option<bool>]) -> Vec<Vec<i32>> {
let mut result = clauses.to_vec();
let mut changed = true;
while changed {
changed = false;
for clause in &result.clone() {
if clause.len() == 1 {
let lit = clause[0];
let var = lit.unsigned_abs() as usize;
let val = lit > 0;
if assignment[var].is_none() {
assignment[var] = Some(val);
changed = true;
}
}
}
result = simplify_clauses(&result, assignment);
}
result
}
fn simplify_clauses(clauses: &[Vec<i32>], assignment: &[Option<bool>]) -> Vec<Vec<i32>> {
clauses
.iter()
.filter(|clause| !clause_satisfied(clause, assignment))
.cloned()
.collect()
}
fn clause_satisfied(clause: &[i32], assignment: &[Option<bool>]) -> bool {
clause.iter().any(|&lit| {
let var = lit.unsigned_abs() as usize;
let val = lit > 0;
assignment.get(var).copied().flatten() == Some(val)
})
}
fn all_satisfied(clauses: &[Vec<i32>], assignment: &[Option<bool>]) -> bool {
clauses.iter().all(|c| clause_satisfied(c, assignment))
}
fn has_empty_clause(clauses: &[Vec<i32>], assignment: &[Option<bool>]) -> bool {
clauses.iter().any(|clause| {
clause.iter().all(|&lit| {
let var = lit.unsigned_abs() as usize;
let val = lit > 0;
assignment.get(var).copied().flatten() == Some(!val)
})
})
}
fn pick_unassigned(assignment: &[Option<bool>], num_vars: usize) -> Option<usize> {
(1..=num_vars).find(|&i| assignment[i].is_none())
}
fn build_sat_result(assignment: &[Option<bool>], var_names: &BTreeMap<usize, String>) -> SatResult {
let mut map = BTreeMap::new();
for (&idx, name) in var_names {
map.insert(name.clone(), assignment[idx].unwrap_or(true));
}
SatResult::Satisfiable { assignment: map }
}
fn build_unsat_result(clauses: &[Vec<i32>], var_names: &BTreeMap<usize, String>) -> SatResult {
let conflict: Vec<String> = clauses
.first()
.map(|c| {
c.iter()
.map(|&lit| {
let var = lit.unsigned_abs() as usize;
let name = var_names
.get(&var)
.cloned()
.unwrap_or_else(|| format!("v{var}"));
if lit > 0 {
name
} else {
format!("!{name}")
}
})
.collect()
})
.unwrap_or_default();
SatResult::Unsatisfiable {
conflict_clause: conflict,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_satisfiable_linear_deps() {
let resources = vec!["A".into(), "B".into(), "C".into()];
let deps = vec![("B".into(), "A".into()), ("C".into(), "B".into())];
let problem = build_sat_problem(&resources, &deps);
let result = solve(&problem);
assert!(matches!(result, SatResult::Satisfiable { .. }));
}
#[test]
fn test_satisfiable_no_deps() {
let resources = vec!["X".into(), "Y".into()];
let deps = vec![];
let problem = build_sat_problem(&resources, &deps);
let result = solve(&problem);
if let SatResult::Satisfiable { assignment } = result {
assert_eq!(assignment.len(), 2);
assert!(assignment["X"]);
assert!(assignment["Y"]);
} else {
panic!("expected satisfiable");
}
}
#[test]
fn test_satisfiable_diamond() {
let resources = vec!["A".into(), "B".into(), "C".into(), "D".into()];
let deps = vec![
("B".into(), "A".into()),
("C".into(), "A".into()),
("D".into(), "B".into()),
("D".into(), "C".into()),
];
let problem = build_sat_problem(&resources, &deps);
let result = solve(&problem);
assert!(matches!(result, SatResult::Satisfiable { .. }));
}
#[test]
fn test_single_resource() {
let resources = vec!["solo".into()];
let deps = vec![];
let problem = build_sat_problem(&resources, &deps);
let result = solve(&problem);
if let SatResult::Satisfiable { assignment } = result {
assert!(assignment["solo"]);
} else {
panic!("expected satisfiable");
}
}
#[test]
fn test_sat_result_serde() {
let result = SatResult::Satisfiable {
assignment: BTreeMap::from([("A".into(), true), ("B".into(), false)]),
};
let json = serde_json::to_string(&result).unwrap();
assert!(json.contains("\"Satisfiable\""));
}
#[test]
fn test_build_problem_structure() {
let resources = vec!["A".into(), "B".into()];
let deps = vec![("B".into(), "A".into())];
let problem = build_sat_problem(&resources, &deps);
assert_eq!(problem.num_vars, 2);
assert_eq!(problem.clauses.len(), 3);
}
#[test]
fn test_unsatisfiable_contradiction() {
let mut var_names = BTreeMap::new();
var_names.insert(1, "A".into());
let problem = SatProblem {
num_vars: 1,
clauses: vec![vec![1], vec![-1]], var_names,
};
let result = solve(&problem);
assert!(
matches!(result, SatResult::Unsatisfiable { .. }),
"contradictory clauses should be unsatisfiable"
);
}
#[test]
fn test_unsatisfiable_conflict_clause_names() {
let mut var_names = BTreeMap::new();
var_names.insert(1, "pkg-a".into());
var_names.insert(2, "pkg-b".into());
let problem = SatProblem {
num_vars: 2,
clauses: vec![vec![1], vec![-1, 2], vec![-2]],
var_names,
};
let result = solve(&problem);
match result {
SatResult::Unsatisfiable { conflict_clause } => {
assert!(!conflict_clause.is_empty());
}
_ => panic!("expected unsatisfiable"),
}
}
#[test]
fn test_unknown_var_in_unsat_result() {
let var_names = BTreeMap::new(); let result = build_unsat_result(&[vec![1, -2]], &var_names);
if let SatResult::Unsatisfiable { conflict_clause } = result {
assert!(conflict_clause.iter().any(|c| c.starts_with("v")));
} else {
panic!("expected unsatisfiable");
}
}
#[test]
fn test_empty_clauses_unsat_result() {
let var_names = BTreeMap::new();
let result = build_unsat_result(&[], &var_names);
if let SatResult::Unsatisfiable { conflict_clause } = result {
assert!(conflict_clause.is_empty());
} else {
panic!("expected unsatisfiable");
}
}
#[test]
fn test_dpll_backtracking() {
let mut var_names = BTreeMap::new();
var_names.insert(1, "X".into());
var_names.insert(2, "Y".into());
var_names.insert(3, "Z".into());
let problem = SatProblem {
num_vars: 3,
clauses: vec![
vec![1, 2], vec![-1, 3], vec![2, -3], vec![1, -2], ],
var_names,
};
let result = solve(&problem);
assert!(matches!(result, SatResult::Satisfiable { .. }));
}
#[test]
fn test_has_empty_clause() {
let assignment: Vec<Option<bool>> = vec![None, Some(false)]; let clauses = vec![vec![1i32]]; assert!(has_empty_clause(&clauses, &assignment));
}
#[test]
fn test_clause_satisfied() {
let assignment: Vec<Option<bool>> = vec![None, Some(true), Some(false)];
assert!(clause_satisfied(&[1], &assignment)); assert!(!clause_satisfied(&[-1], &assignment)); assert!(clause_satisfied(&[-2], &assignment)); assert!(!clause_satisfied(&[2], &assignment)); }
#[test]
fn test_all_satisfied_empty() {
let assignment: Vec<Option<bool>> = vec![None];
assert!(all_satisfied(&[], &assignment));
}
#[test]
fn test_pick_unassigned_all_assigned() {
let assignment: Vec<Option<bool>> = vec![None, Some(true), Some(false)];
assert_eq!(pick_unassigned(&assignment, 2), None);
}
#[test]
fn test_pick_unassigned_first() {
let assignment: Vec<Option<bool>> = vec![None, None, Some(true)];
assert_eq!(pick_unassigned(&assignment, 2), Some(1));
}
#[test]
fn test_simplify_clauses() {
let assignment: Vec<Option<bool>> = vec![None, Some(true)]; let clauses = vec![vec![1], vec![-1, 2]]; let simplified = simplify_clauses(&clauses, &assignment);
assert_eq!(simplified.len(), 1); }
#[test]
fn test_deps_with_unknown_resources() {
let resources = vec!["A".into()];
let deps = vec![("A".into(), "MISSING".into())];
let problem = build_sat_problem(&resources, &deps);
assert_eq!(problem.clauses.len(), 1);
}
#[test]
fn test_many_resources_satisfiable() {
let resources: Vec<String> = (0..10).map(|i| format!("r{i}")).collect();
let deps: Vec<(String, String)> = (1..10)
.map(|i| (format!("r{i}"), format!("r{}", i - 1)))
.collect();
let problem = build_sat_problem(&resources, &deps);
let result = solve(&problem);
if let SatResult::Satisfiable { assignment } = result {
assert_eq!(assignment.len(), 10);
assert!(assignment.values().all(|&v| v));
} else {
panic!("linear chain should be satisfiable");
}
}
#[test]
fn test_unsat_result_negative_literal_formatting() {
let mut var_names = BTreeMap::new();
var_names.insert(1, "svc".into());
let result = build_unsat_result(&[vec![-1]], &var_names);
if let SatResult::Unsatisfiable { conflict_clause } = result {
assert_eq!(conflict_clause, vec!["!svc"]);
}
}
#[test]
fn test_propagate_units_assigns_unit_clause() {
let clauses = vec![vec![1], vec![-1, 2]];
let mut assignment: Vec<Option<bool>> = vec![None, None, None];
let result = propagate_units(&clauses, &mut assignment);
assert_eq!(assignment[1], Some(true));
assert!(result.len() <= clauses.len());
}
}