diff --git a/src/lib.rs b/src/lib.rs index fbda8fab..b3b3643d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -34,7 +34,9 @@ pub use id::{ }; use itertools::Itertools; pub use requirement::Requirement; -pub use solver::{EmptySolvables, Problem, Solver, SolverCache, UnsolvableOrCancelled}; +pub use solver::{ + EmptySolvables, Problem, Solver, SolverCache, SolverConfig, UnsolvableOrCancelled, +}; pub use solver_id::{DenseId, IdMap, IdSet, SolverId, SparseId}; pub use utils::{IndexedSet, Mapping, MappingIter}; @@ -158,6 +160,15 @@ pub trait DependencyProvider: Sized + Interner { fn should_cancel_with_value(&self) -> Option> { None } + + /// Returns the solver configuration for this dependency provider. + /// + /// Override this to customize solver behavior. The returned config is used + /// by [`Solver::new`] unless explicitly overridden with + /// [`Solver::with_config`]. + fn solver_config(&self) -> SolverConfig { + SolverConfig::default() + } } /// A list of candidate solvables for a specific package. This is returned from diff --git a/src/solver/encoding.rs b/src/solver/encoding.rs index 2c774db6..e9a40c32 100644 --- a/src/solver/encoding.rs +++ b/src/solver/encoding.rs @@ -1,6 +1,10 @@ use std::{any::Any, collections::VecDeque}; -use super::{SolverState, clause::WatchedLiterals, conditions}; +use super::{ + SolverConfig, SolverState, + clause::{Clause, WatchedLiterals}, + conditions, +}; use crate::{ Candidates, ConditionId, ConditionalRequirement, DenseIndex, Dependencies, DependencyProvider, Requirement, SolverCache, StringId, VariableId, VersionSetId, @@ -35,6 +39,7 @@ type RequirementCondition<'a, S> = Option<(ConditionId, Vec { state: &'a mut SolverState, cache: &'cache SolverCache, + config: &'a SolverConfig, level: u32, /// The dependencies of the root solvable. @@ -156,12 +161,14 @@ impl<'a, 'cache, D: DependencyProvider> Encoder<'a, 'cache, D> { pub fn new( state: &'a mut SolverState, cache: &'cache SolverCache, + config: &'a SolverConfig, root_dependencies: &'cache Dependencies, level: u32, ) -> Self { Self { state, cache, + config, root_dependencies, pending_futures: FuturesUnordered::new(), conflicting_clauses: Vec::new(), @@ -643,6 +650,12 @@ impl<'a, 'cache, D: DependencyProvider> Encoder<'a, 'cache, D> { // Pairwise encoding: one (¬parent ∨ ¬candidate) clause per // excluded candidate. for &forbidden_candidate in candidates { + if SolvableIdOrRoot::from(forbidden_candidate) == solvable_id { + if self.config.forbid_self_conflicts { + self.add_self_conflict_clause(variable, constraint); + } + continue; + } let forbidden_candidate_var = self.state.variable_map.intern_solvable(forbidden_candidate); let (watched_literals, conflict, kind) = WatchedLiterals::constrains( @@ -675,6 +688,12 @@ impl<'a, 'cache, D: DependencyProvider> Encoder<'a, 'cache, D> { .insert(constraint, aux_variable); for &forbidden_candidate in candidates { + if SolvableIdOrRoot::from(forbidden_candidate) == solvable_id { + if self.config.forbid_self_conflicts { + self.add_self_conflict_clause(variable, constraint); + } + continue; + } let forbidden_candidate_var = self.state.variable_map.intern_solvable(forbidden_candidate); let (watched_literals, kind) = WatchedLiterals::constrains_excluded( @@ -765,6 +784,19 @@ impl<'a, 'cache, D: DependencyProvider> Encoder<'a, 'cache, D> { variable } + /// Adds a unary constrains clause that forbids a solvable that conflicts + /// with something it also provides (a self-conflict). + fn add_self_conflict_clause(&mut self, variable: VariableId, constraint: VersionSetId) { + let kind = Clause::Constrains(variable, variable, constraint); + let clause_id = self.state.add_clause(None, kind); + + self.state.negative_assertions.push((variable, clause_id)); + + if self.state.decision_tracker.assigned_value(variable) == Some(true) { + self.conflicting_clauses.push(clause_id); + } + } + /// Enqueues retrieving the dependencies for a solvable. /// /// This method requests the dependencies for the given solvable in an diff --git a/src/solver/mod.rs b/src/solver/mod.rs index 72fb440c..16b68161 100644 --- a/src/solver/mod.rs +++ b/src/solver/mod.rs @@ -176,6 +176,23 @@ impl Clauses { type RequirementCandidateVariables = Vec>; +/// Configuration options for the solver. +#[derive(Debug, Clone)] +pub struct SolverConfig { + /// When `true`, a package that conflicts with something it also provides + /// (i.e. it conflicts with itself) is marked uninstallable. When `false`, + /// self-conflicts are silently ignored. + pub forbid_self_conflicts: bool, +} + +impl Default for SolverConfig { + fn default() -> Self { + Self { + forbid_self_conflicts: true, + } + } +} + /// Drives the SAT solving process. pub struct Solver { /// The runtime to use for async operations. @@ -187,6 +204,9 @@ pub struct Solver { /// Holds the current state of the solver. pub(crate) state: SolverState, + /// Solver configuration options. + config: SolverConfig, + /// The activity add factor. This is a value that is added to the activity /// score of each package that is part of a conflict. activity_add: f32, @@ -368,10 +388,12 @@ impl Solver { /// Creates a single threaded block solver, using the provided /// [`DependencyProvider`]. pub fn new(provider: D) -> Self { + let config = provider.solver_config(); Self { cache: SolverCache::new(provider), async_runtime: NowOrNeverRuntime, state: SolverState::default(), + config, activity_add: 1.0, activity_decay: 0.95, } @@ -448,6 +470,7 @@ impl Solver { async_runtime: runtime, cache: self.cache, state: self.state, + config: self.config, activity_decay: self.activity_decay, activity_add: self.activity_add, } @@ -464,6 +487,12 @@ impl Solver { } } + /// Set the solver configuration. + #[must_use] + pub fn with_config(self, config: SolverConfig) -> Self { + Self { config, ..self } + } + /// Solves the given [`Problem`]. /// /// The solver first solves for the root requirements and constraints, and @@ -634,7 +663,7 @@ impl Solver { #[cfg(feature = "diagnostics")] let encoding_start = std::time::Instant::now(); let conflicting_clauses = self.async_runtime.block_on( - Encoder::new(&mut self.state, &self.cache, root_deps, level) + Encoder::new(&mut self.state, &self.cache, &self.config, root_deps, level) .encode([root_solvable]), )?; #[cfg(feature = "diagnostics")] @@ -789,7 +818,7 @@ impl Solver { #[cfg(feature = "diagnostics")] let encoding_start = std::time::Instant::now(); let conflicting_clauses = self.async_runtime.block_on( - Encoder::new(&mut self.state, &self.cache, root_deps, level) + Encoder::new(&mut self.state, &self.cache, &self.config, root_deps, level) .encode_with_deferred(solvable_ids.iter().copied(), deferred_to_encode), )?; #[cfg(feature = "diagnostics")] diff --git a/tests/solver/main.rs b/tests/solver/main.rs index 7dae87c7..7bd4d955 100644 --- a/tests/solver/main.rs +++ b/tests/solver/main.rs @@ -7,7 +7,7 @@ use insta::assert_snapshot; use itertools::Itertools; use resolvo::{ ConditionalRequirement, DependencyProvider, Interner, Problem, SolvableId, Solver, - UnsolvableOrCancelled, VersionSetId, + SolverConfig, UnsolvableOrCancelled, VersionSetId, }; use tracing_test::traced_test; @@ -2109,6 +2109,77 @@ fn test_constrains_multiple_parents() { x=1 "###); } +mod test_self_conflict { + use super::*; + + /// When `forbid_self_conflicts` is false, a package that constrains itself + /// is silently allowed. Some ecosystems (e.g. RPM) explicitly support this. + /// The real-world examples are structured a bit differently however. + #[test] + fn test_self_conflict_allowed() { + let mut provider = BundleBoxProvider::new(); + // a=1 constrains "a" to [2,100) — version 1 is NOT in that range, + // so a=1 appears as a non-matching candidate for its own constraint + // (i.e. a self-conflict). + provider.add_package("a", 1.into(), &[], &["a 2..100"]); + + let requirements = provider.requirements(&["a"]); + let config = SolverConfig { + forbid_self_conflicts: false, + }; + let mut solver = Solver::new(provider).with_config(config); + let problem = Problem::new().requirements(requirements); + let solved = solver.solve(problem).unwrap(); + let result = transaction_to_string(solver.provider(), &solved); + assert_snapshot!(result, @r" + a=1 + "); + } + + /// When `forbid_self_conflicts` is true (the default), a package that + /// constrains itself is marked uninstallable. + #[test] + fn test_self_conflict_forbidden() { + let mut provider = BundleBoxProvider::new(); + provider.add_package("a", 1.into(), &[], &["a 2..100"]); + + let requirements = provider.requirements(&["a"]); + let mut solver = Solver::new(provider); + let problem = Problem::new().requirements(requirements); + match solver.solve(problem) { + Ok(_) => panic!("expected unsat due to self-conflict"), + Err(UnsolvableOrCancelled::Unsolvable(_)) => {} + Err(UnsolvableOrCancelled::Cancelled(_)) => { + panic!("expected unsolvable, not cancelled") + } + } + } + + /// `allow_self_conflicts` works correctly when the self-conflicting + /// package is a transitive dependency discovered in a later solver pass. + #[test] + fn test_self_conflict_allowed_transitive() { + let mut provider = BundleBoxProvider::new(); + // "a" has a self-conflict and is a transitive dep of "app" via "lib". + provider.add_package("a", 1.into(), &[], &["a 2..100"]); + provider.add_package("lib", 1.into(), &["a"], &[]); + provider.add_package("app", 1.into(), &["lib"], &[]); + + let requirements = provider.requirements(&["app"]); + let config = SolverConfig { + forbid_self_conflicts: false, + }; + let mut solver = Solver::new(provider).with_config(config); + let problem = Problem::new().requirements(requirements); + let solved = solver.solve(problem).unwrap(); + let result = transaction_to_string(solver.provider(), &solved); + assert_snapshot!(result, @r" + a=1 + app=1 + lib=1 + "); + } +} // ============================================================================ // Decide-queue wake-up scenarios