blob: d67a1c24981507456c41f4ea2a5d4c0cc08a5cd2 [file] [log] [blame]
use std::collections::hash_map::Entry;
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};
use std::fmt::Write;
use cargo::core::dependency::DepKind;
use cargo::core::{Dependency, FeatureMap, FeatureValue, PackageId, Summary};
use cargo::util::interning::{InternedString, INTERNED_DEFAULT};
use cargo_platform::Platform;
use varisat::ExtendFormula;
const fn num_bits<T>() -> usize {
std::mem::size_of::<T>() * 8
}
fn log_bits(x: usize) -> usize {
if x == 0 {
return 0;
}
assert!(x > 0);
(num_bits::<usize>() as u32 - x.leading_zeros()) as usize
}
/// At this point is possible to select every version of every package.
///
/// So we need to mark certain versions as incompatible with each other.
/// We could add a clause not A, not B for all A and B that are incompatible,
fn sat_at_most_one(solver: &mut varisat::Solver<'_>, vars: &[varisat::Var]) {
if vars.len() <= 1 {
return;
} else if vars.len() == 2 {
solver.add_clause(&[vars[0].negative(), vars[1].negative()]);
return;
} else if vars.len() == 3 {
solver.add_clause(&[vars[0].negative(), vars[1].negative()]);
solver.add_clause(&[vars[0].negative(), vars[2].negative()]);
solver.add_clause(&[vars[1].negative(), vars[2].negative()]);
return;
}
// There are more efficient ways to do it for large numbers of versions.
//
// Use the "Binary Encoding" from
// https://www.it.uu.se/research/group/astra/ModRef10/papers/Alan%20M.%20Frisch%20and%20Paul%20A.%20Giannoros.%20SAT%20Encodings%20of%20the%20At-Most-k%20Constraint%20-%20ModRef%202010.pdf
let bits: Vec<varisat::Var> = solver.new_var_iter(log_bits(vars.len())).collect();
for (i, p) in vars.iter().enumerate() {
for b in 0..bits.len() {
solver.add_clause(&[p.negative(), bits[b].lit(((1 << b) & i) > 0)]);
}
}
}
fn sat_at_most_one_by_key<K: std::hash::Hash + Eq>(
solver: &mut varisat::Solver<'_>,
data: impl Iterator<Item = (K, varisat::Var)>,
) -> HashMap<K, Vec<varisat::Var>> {
// No two packages with the same keys set
let mut by_keys: HashMap<K, Vec<varisat::Var>> = HashMap::new();
for (p, v) in data {
by_keys.entry(p).or_default().push(v)
}
for key in by_keys.values() {
sat_at_most_one(solver, key);
}
by_keys
}
type DependencyVarMap<'a> =
HashMap<InternedString, HashMap<(DepKind, Option<&'a Platform>), varisat::Var>>;
type DependencyFeatureVarMap<'a> = HashMap<
InternedString,
HashMap<(DepKind, Option<&'a Platform>), HashMap<InternedString, varisat::Var>>,
>;
fn create_dependencies_vars<'a>(
solver: &mut varisat::Solver<'_>,
pkg_var: varisat::Var,
pkg_dependencies: &'a [Dependency],
pkg_features: &FeatureMap,
) -> (DependencyVarMap<'a>, DependencyFeatureVarMap<'a>) {
let mut var_for_is_dependencies_used = DependencyVarMap::new();
let mut var_for_is_dependencies_features_used = DependencyFeatureVarMap::new();
for dep in pkg_dependencies {
let (name, kind, platform) = (dep.name_in_toml(), dep.kind(), dep.platform());
var_for_is_dependencies_used
.entry(name)
.or_default()
.insert((kind, platform), solver.new_var());
let dep_feature_vars = var_for_is_dependencies_features_used
.entry(name)
.or_default()
.entry((kind, platform))
.or_default();
for &feature_name in dep.features() {
if let Entry::Vacant(entry) = dep_feature_vars.entry(feature_name) {
entry.insert(solver.new_var());
}
}
}
for feature_values in pkg_features.values() {
for feature_value in feature_values {
let FeatureValue::DepFeature {
dep_name,
dep_feature,
weak: _,
} = *feature_value
else {
continue;
};
for dep_feature_vars in var_for_is_dependencies_features_used
.get_mut(&dep_name)
.expect("feature dep name exists")
.values_mut()
{
if let Entry::Vacant(entry) = dep_feature_vars.entry(dep_feature) {
entry.insert(solver.new_var());
}
}
}
}
// If a package dependency is used, then the package is used
for dep_var_map in var_for_is_dependencies_used.values() {
for dep_var in dep_var_map.values() {
solver.add_clause(&[dep_var.negative(), pkg_var.positive()]);
}
}
// If a dependency feature is used, then the dependency is used
for (&dep_name, map) in &mut var_for_is_dependencies_features_used {
for (&(dep_kind, dep_platform), dep_feature_var_map) in map {
for dep_feature_var in dep_feature_var_map.values() {
let dep_var_map = &var_for_is_dependencies_used[&dep_name];
let dep_var = dep_var_map[&(dep_kind, dep_platform)];
solver.add_clause(&[dep_feature_var.negative(), dep_var.positive()]);
}
}
}
(
var_for_is_dependencies_used,
var_for_is_dependencies_features_used,
)
}
fn process_pkg_dependencies(
solver: &mut varisat::Solver<'_>,
var_for_is_dependencies_used: &DependencyVarMap<'_>,
var_for_is_dependencies_features_used: &DependencyFeatureVarMap<'_>,
pkg_var: varisat::Var,
pkg_dependencies: &[Dependency],
) {
// Add clauses for package dependencies
for dep in pkg_dependencies {
let (name, kind, platform) = (dep.name_in_toml(), dep.kind(), dep.platform());
let dep_var_map = &var_for_is_dependencies_used[&name];
let dep_var = dep_var_map[&(kind, platform)];
if !dep.is_optional() {
solver.add_clause(&[pkg_var.negative(), dep_var.positive()]);
}
for &feature_name in dep.features() {
let dep_feature_var =
&var_for_is_dependencies_features_used[&name][&(kind, platform)][&feature_name];
solver.add_clause(&[dep_var.negative(), dep_feature_var.positive()]);
}
}
}
fn process_pkg_features(
solver: &mut varisat::Solver<'_>,
var_for_is_dependencies_used: &DependencyVarMap<'_>,
var_for_is_dependencies_features_used: &DependencyFeatureVarMap<'_>,
pkg_feature_var_map: &HashMap<InternedString, varisat::Var>,
pkg_dependencies: &[Dependency],
pkg_features: &FeatureMap,
) {
let optional_dependencies = pkg_dependencies
.iter()
.filter(|dep| dep.is_optional())
.map(|dep| (dep.kind(), dep.platform(), dep.name_in_toml()))
.collect::<HashSet<_>>();
// Add clauses for package features
for (&feature_name, feature_values) in pkg_features {
for feature_value in feature_values {
let pkg_feature_var = pkg_feature_var_map[&feature_name];
match *feature_value {
FeatureValue::Feature(other_feature_name) => {
solver.add_clause(&[
pkg_feature_var.negative(),
pkg_feature_var_map[&other_feature_name].positive(),
]);
}
FeatureValue::Dep { dep_name } => {
// Add a clause for each dependency with the provided name (normal/build/dev with target)
for (&(dep_kind, _), &dep_var) in &var_for_is_dependencies_used[&dep_name] {
if dep_kind == DepKind::Development {
continue;
}
solver.add_clause(&[pkg_feature_var.negative(), dep_var.positive()]);
}
}
FeatureValue::DepFeature {
dep_name,
dep_feature: dep_feature_name,
weak,
} => {
// Behavior of the feature:
// * if dependency `dep_name` is not optional, its feature `"dep_feature_name"` is activated.
// * if dependency `dep_name` is optional:
// - if this is a weak dependency feature:
// - feature `"dep_feature_name"` of dependency `dep_name` is activated if `dep_name` has been activated via another feature.
// - if this is not a weak dependency feature:
// - feature `dep_name` is activated if it exists.
// - dependency `dep_name` is activated.
// - feature `"dep_feature_name"` of dependency `dep_name` is activated.
// Add clauses for each dependency with the provided name (normal/build/dev with target)
let dep_var_map = &var_for_is_dependencies_used[&dep_name];
for (&(dep_kind, dep_platform), &dep_var) in dep_var_map {
if dep_kind == DepKind::Development {
continue;
}
let dep_feature_var = &var_for_is_dependencies_features_used[&dep_name]
[&(dep_kind, dep_platform)][&dep_feature_name];
solver.add_clause(&[
pkg_feature_var.negative(),
dep_var.negative(),
dep_feature_var.positive(),
]);
let key = (dep_kind, dep_platform, dep_name);
if !weak && optional_dependencies.contains(&key) {
solver.add_clause(&[pkg_feature_var.negative(), dep_var.positive()]);
if let Some(other_feature_var) = pkg_feature_var_map.get(&dep_name) {
solver.add_clause(&[
pkg_feature_var.negative(),
other_feature_var.positive(),
]);
}
}
}
}
}
}
}
}
fn process_compatible_dep_summaries(
solver: &mut varisat::Solver<'_>,
var_for_is_dependencies_used: &DependencyVarMap<'_>,
var_for_is_dependencies_features_used: &DependencyFeatureVarMap<'_>,
var_for_is_packages_used: &HashMap<PackageId, varisat::Var>,
var_for_is_packages_features_used: &HashMap<PackageId, HashMap<InternedString, varisat::Var>>,
by_name: &HashMap<InternedString, Vec<Summary>>,
pkg_dependencies: &[Dependency],
check_dev_dependencies: bool,
) {
for dep in pkg_dependencies {
if dep.kind() == DepKind::Development && !check_dev_dependencies {
continue;
}
let (name, kind, platform) = (dep.name_in_toml(), dep.kind(), dep.platform());
let dep_var = var_for_is_dependencies_used[&name][&(kind, platform)];
let dep_feature_var_map = &var_for_is_dependencies_features_used[&name][&(kind, platform)];
let compatible_pkg_ids = by_name
.get(&dep.package_name())
.into_iter()
.flatten()
.filter(|s| dep.matches(s))
.filter(|s| dep.features().iter().all(|f| s.features().contains_key(f)))
.map(|s| s.package_id())
.collect::<Vec<_>>();
// At least one compatible package should be activated
let dep_clause = compatible_pkg_ids
.iter()
.map(|id| var_for_is_packages_used[&id].positive())
.chain([dep_var.negative()])
.collect::<Vec<_>>();
solver.add_clause(&dep_clause);
for (&feature_name, &dep_feature_var) in dep_feature_var_map {
// At least one compatible package with the additional feature should be activated
let dep_feature_clause = compatible_pkg_ids
.iter()
.filter_map(|id| var_for_is_packages_features_used[&id].get(&feature_name))
.map(|var| var.positive())
.chain([dep_feature_var.negative()])
.collect::<Vec<_>>();
solver.add_clause(&dep_feature_clause);
}
if dep.uses_default_features() {
// For the selected package for this dependency, the `"default"` feature should be activated if it exists
let mut dep_default_clause = vec![dep_var.negative()];
for &id in &compatible_pkg_ids {
let s_var = var_for_is_packages_used[&id];
let s_feature_var_map = &var_for_is_packages_features_used[&id];
if let Some(s_default_feature_var) = s_feature_var_map.get(&INTERNED_DEFAULT) {
dep_default_clause
.extend_from_slice(&[s_var.negative(), s_default_feature_var.positive()]);
} else {
dep_default_clause.push(s_var.positive());
}
}
solver.add_clause(&dep_default_clause);
}
}
}
/// Resolution can be reduced to the SAT problem.
///
/// So this is an alternative implementation
/// of the resolver that uses a SAT library for the hard work. This is intended to be easy to read,
/// as compared to the real resolver.
///
/// For the subset of functionality that are currently made by `registry_strategy`,
/// this will find a valid resolution if one exists.
///
/// The SAT library does not optimize for the newer version,
/// so the selected packages may not match the real resolver.
pub struct SatResolver {
solver: varisat::Solver<'static>,
old_root_vars: Vec<varisat::Var>,
var_for_is_packages_used: HashMap<PackageId, varisat::Var>,
var_for_is_packages_features_used: HashMap<PackageId, HashMap<InternedString, varisat::Var>>,
by_name: HashMap<InternedString, Vec<Summary>>,
}
impl SatResolver {
pub fn new<'a>(registry: impl IntoIterator<Item = &'a Summary>) -> Self {
let mut by_name: HashMap<InternedString, Vec<Summary>> = HashMap::new();
for pkg in registry {
by_name.entry(pkg.name()).or_default().push(pkg.clone());
}
let mut solver = varisat::Solver::new();
// Create boolean variables for packages and packages features
let mut var_for_is_packages_used = HashMap::new();
let mut var_for_is_packages_features_used = HashMap::<_, HashMap<_, _>>::new();
for pkg in by_name.values().flatten() {
let pkg_id = pkg.package_id();
var_for_is_packages_used.insert(pkg_id, solver.new_var());
var_for_is_packages_features_used.insert(
pkg_id,
(pkg.features().keys().map(|&f| (f, solver.new_var()))).collect(),
);
}
// If a package feature is used, then the package is used
for (&pkg_id, pkg_feature_var_map) in &var_for_is_packages_features_used {
for pkg_feature_var in pkg_feature_var_map.values() {
let pkg_var = var_for_is_packages_used[&pkg_id];
solver.add_clause(&[pkg_feature_var.negative(), pkg_var.positive()]);
}
}
// No two packages with the same links set
sat_at_most_one_by_key(
&mut solver,
by_name
.values()
.flatten()
.map(|s| (s.links(), var_for_is_packages_used[&s.package_id()]))
.filter(|(l, _)| l.is_some()),
);
// No two semver compatible versions of the same package
sat_at_most_one_by_key(
&mut solver,
var_for_is_packages_used
.iter()
.map(|(p, &v)| (p.as_activations_key(), v)),
);
for pkg in by_name.values().flatten() {
let pkg_id = pkg.package_id();
let pkg_dependencies = pkg.dependencies();
let pkg_features = pkg.features();
let pkg_var = var_for_is_packages_used[&pkg_id];
// Create boolean variables for dependencies and dependencies features
let (var_for_is_dependencies_used, var_for_is_dependencies_features_used) =
create_dependencies_vars(&mut solver, pkg_var, pkg_dependencies, pkg_features);
process_pkg_dependencies(
&mut solver,
&var_for_is_dependencies_used,
&var_for_is_dependencies_features_used,
pkg_var,
pkg_dependencies,
);
process_pkg_features(
&mut solver,
&var_for_is_dependencies_used,
&var_for_is_dependencies_features_used,
&var_for_is_packages_features_used[&pkg_id],
pkg_dependencies,
pkg_features,
);
process_compatible_dep_summaries(
&mut solver,
&var_for_is_dependencies_used,
&var_for_is_dependencies_features_used,
&var_for_is_packages_used,
&var_for_is_packages_features_used,
&by_name,
pkg_dependencies,
false,
);
}
// We don't need to `solve` now. We know that "use nothing" will satisfy all the clauses so far.
// But things run faster if we let it spend some time figuring out how the constraints interact before we add assumptions.
solver
.solve()
.expect("docs say it can't error in default config");
SatResolver {
solver,
old_root_vars: Vec::new(),
var_for_is_packages_used,
var_for_is_packages_features_used,
by_name,
}
}
pub fn sat_resolve(&mut self, root_dependencies: &[Dependency]) -> bool {
let SatResolver {
solver,
old_root_vars,
var_for_is_packages_used,
var_for_is_packages_features_used,
by_name,
} = self;
let root_var = solver.new_var();
// Create boolean variables for dependencies and dependencies features
let (var_for_is_dependencies_used, var_for_is_dependencies_features_used) =
create_dependencies_vars(solver, root_var, root_dependencies, &FeatureMap::new());
process_pkg_dependencies(
solver,
&var_for_is_dependencies_used,
&var_for_is_dependencies_features_used,
root_var,
root_dependencies,
);
process_compatible_dep_summaries(
solver,
&var_for_is_dependencies_used,
&var_for_is_dependencies_features_used,
var_for_is_packages_used,
var_for_is_packages_features_used,
by_name,
root_dependencies,
true,
);
// Root package is always used.
// Root vars from previous runs are deactivated.
let assumption = old_root_vars
.iter()
.map(|v| v.negative())
.chain([root_var.positive()])
.collect::<Vec<_>>();
old_root_vars.push(root_var);
solver.assume(&assumption);
solver
.solve()
.expect("docs say it can't error in default config")
}
pub fn sat_is_valid_solution(&mut self, pkgs: &[(PackageId, Vec<InternedString>)]) -> bool {
let contains_pkg = |pkg| pkgs.iter().any(|(p, _)| p == pkg);
let contains_pkg_feature =
|pkg, f| pkgs.iter().any(|(p, flist)| p == pkg && flist.contains(f));
for (p, _) in pkgs {
if p.name() != "root" && !self.var_for_is_packages_used.contains_key(p) {
return false;
}
}
// Root vars from previous runs are deactivated
let assumption = (self.old_root_vars.iter().map(|v| v.negative()))
.chain(
self.var_for_is_packages_used
.iter()
.map(|(p, v)| v.lit(contains_pkg(p))),
)
.chain(
self.var_for_is_packages_features_used
.iter()
.flat_map(|(p, fmap)| {
fmap.iter()
.map(move |(f, v)| v.lit(contains_pkg_feature(p, f)))
}),
)
.collect::<Vec<_>>();
self.solver.assume(&assumption);
self.solver
.solve()
.expect("docs say it can't error in default config")
}
pub fn used_packages(&self) -> Option<String> {
self.solver.model().map(|lits| {
let lits: HashSet<_> = lits
.iter()
.filter(|l| l.is_positive())
.map(|l| l.var())
.collect();
let mut used_packages = BTreeMap::<PackageId, BTreeSet<InternedString>>::new();
for (&p, v) in self.var_for_is_packages_used.iter() {
if lits.contains(v) {
used_packages.entry(p).or_default();
}
}
for (&p, map) in &self.var_for_is_packages_features_used {
for (&f, v) in map {
if lits.contains(v) {
used_packages
.get_mut(&p)
.expect("the feature is activated without the package being activated")
.insert(f);
}
}
}
let mut out = String::from("used:\n");
for (package, feature_names) in used_packages {
writeln!(&mut out, " {package}").unwrap();
for feature_name in feature_names {
writeln!(&mut out, " + {feature_name}").unwrap();
}
}
out
})
}
}