1 // SPDX-License-Identifier: MPL-2.0 2 3 //! Core model and functions 4 //! to write a functional PubGrub algorithm. 5 6 use std::collections::HashSet as Set; 7 8 use crate::error::PubGrubError; 9 use crate::internal::arena::Arena; 10 use crate::internal::incompatibility::{IncompId, Incompatibility, Relation}; 11 use crate::internal::partial_solution::SatisfierSearch::{ 12 DifferentDecisionLevels, SameDecisionLevels, 13 }; 14 use crate::internal::partial_solution::{DecisionLevel, PartialSolution}; 15 use crate::internal::small_vec::SmallVec; 16 use crate::package::Package; 17 use crate::report::DerivationTree; 18 use crate::solver::DependencyConstraints; 19 use crate::type_aliases::Map; 20 use crate::version::Version; 21 22 /// Current state of the PubGrub algorithm. 23 #[derive(Clone)] 24 pub struct State<P: Package, V: Version> { 25 root_package: P, 26 root_version: V, 27 28 incompatibilities: Map<P, Vec<IncompId<P, V>>>, 29 30 /// Store the ids of incompatibilities that are already contradicted 31 /// and will stay that way until the next conflict and backtrack is operated. 32 contradicted_incompatibilities: rustc_hash::FxHashSet<IncompId<P, V>>, 33 34 /// Partial solution. 35 /// TODO: remove pub. 36 pub partial_solution: PartialSolution<P, V>, 37 38 /// The store is the reference storage for all incompatibilities. 39 pub incompatibility_store: Arena<Incompatibility<P, V>>, 40 41 /// This is a stack of work to be done in `unit_propagation`. 42 /// It can definitely be a local variable to that method, but 43 /// this way we can reuse the same allocation for better performance. 44 unit_propagation_buffer: SmallVec<P>, 45 } 46 47 impl<P: Package, V: Version> State<P, V> { 48 /// Initialization of PubGrub state. init(root_package: P, root_version: V) -> Self49 pub fn init(root_package: P, root_version: V) -> Self { 50 let mut incompatibility_store = Arena::new(); 51 let not_root_id = incompatibility_store.alloc(Incompatibility::not_root( 52 root_package.clone(), 53 root_version.clone(), 54 )); 55 let mut incompatibilities = Map::default(); 56 incompatibilities.insert(root_package.clone(), vec![not_root_id]); 57 Self { 58 root_package, 59 root_version, 60 incompatibilities, 61 contradicted_incompatibilities: rustc_hash::FxHashSet::default(), 62 partial_solution: PartialSolution::empty(), 63 incompatibility_store, 64 unit_propagation_buffer: SmallVec::Empty, 65 } 66 } 67 68 /// Add an incompatibility to the state. add_incompatibility(&mut self, incompat: Incompatibility<P, V>)69 pub fn add_incompatibility(&mut self, incompat: Incompatibility<P, V>) { 70 let id = self.incompatibility_store.alloc(incompat); 71 self.merge_incompatibility(id); 72 } 73 74 /// Add an incompatibility to the state. add_incompatibility_from_dependencies( &mut self, package: P, version: V, deps: &DependencyConstraints<P, V>, ) -> std::ops::Range<IncompId<P, V>>75 pub fn add_incompatibility_from_dependencies( 76 &mut self, 77 package: P, 78 version: V, 79 deps: &DependencyConstraints<P, V>, 80 ) -> std::ops::Range<IncompId<P, V>> { 81 // Create incompatibilities and allocate them in the store. 82 let new_incompats_id_range = self 83 .incompatibility_store 84 .alloc_iter(deps.iter().map(|dep| { 85 Incompatibility::from_dependency(package.clone(), version.clone(), dep) 86 })); 87 // Merge the newly created incompatibilities with the older ones. 88 for id in IncompId::range_to_iter(new_incompats_id_range.clone()) { 89 self.merge_incompatibility(id); 90 } 91 new_incompats_id_range 92 } 93 94 /// Check if an incompatibility is terminal. is_terminal(&self, incompatibility: &Incompatibility<P, V>) -> bool95 pub fn is_terminal(&self, incompatibility: &Incompatibility<P, V>) -> bool { 96 incompatibility.is_terminal(&self.root_package, &self.root_version) 97 } 98 99 /// Unit propagation is the core mechanism of the solving algorithm. 100 /// CF <https://github.com/dart-lang/pub/blob/master/doc/solver.md#unit-propagation> unit_propagation(&mut self, package: P) -> Result<(), PubGrubError<P, V>>101 pub fn unit_propagation(&mut self, package: P) -> Result<(), PubGrubError<P, V>> { 102 self.unit_propagation_buffer.clear(); 103 self.unit_propagation_buffer.push(package); 104 while let Some(current_package) = self.unit_propagation_buffer.pop() { 105 // Iterate over incompatibilities in reverse order 106 // to evaluate first the newest incompatibilities. 107 let mut conflict_id = None; 108 // We only care about incompatibilities if it contains the current package. 109 for &incompat_id in self.incompatibilities[¤t_package].iter().rev() { 110 if self.contradicted_incompatibilities.contains(&incompat_id) { 111 continue; 112 } 113 let current_incompat = &self.incompatibility_store[incompat_id]; 114 match self.partial_solution.relation(current_incompat) { 115 // If the partial solution satisfies the incompatibility 116 // we must perform conflict resolution. 117 Relation::Satisfied => { 118 conflict_id = Some(incompat_id); 119 break; 120 } 121 Relation::AlmostSatisfied(package_almost) => { 122 self.unit_propagation_buffer.push(package_almost.clone()); 123 // Add (not term) to the partial solution with incompat as cause. 124 self.partial_solution.add_derivation( 125 package_almost, 126 incompat_id, 127 &self.incompatibility_store, 128 ); 129 // With the partial solution updated, the incompatibility is now contradicted. 130 self.contradicted_incompatibilities.insert(incompat_id); 131 } 132 Relation::Contradicted(_) => { 133 self.contradicted_incompatibilities.insert(incompat_id); 134 } 135 _ => {} 136 } 137 } 138 if let Some(incompat_id) = conflict_id { 139 let (package_almost, root_cause) = self.conflict_resolution(incompat_id)?; 140 self.unit_propagation_buffer.clear(); 141 self.unit_propagation_buffer.push(package_almost.clone()); 142 // Add to the partial solution with incompat as cause. 143 self.partial_solution.add_derivation( 144 package_almost, 145 root_cause, 146 &self.incompatibility_store, 147 ); 148 // After conflict resolution and the partial solution update, 149 // the root cause incompatibility is now contradicted. 150 self.contradicted_incompatibilities.insert(root_cause); 151 } 152 } 153 // If there are no more changed packages, unit propagation is done. 154 Ok(()) 155 } 156 157 /// Return the root cause and the backtracked model. 158 /// CF <https://github.com/dart-lang/pub/blob/master/doc/solver.md#unit-propagation> conflict_resolution( &mut self, incompatibility: IncompId<P, V>, ) -> Result<(P, IncompId<P, V>), PubGrubError<P, V>>159 fn conflict_resolution( 160 &mut self, 161 incompatibility: IncompId<P, V>, 162 ) -> Result<(P, IncompId<P, V>), PubGrubError<P, V>> { 163 let mut current_incompat_id = incompatibility; 164 let mut current_incompat_changed = false; 165 loop { 166 if self.incompatibility_store[current_incompat_id] 167 .is_terminal(&self.root_package, &self.root_version) 168 { 169 return Err(PubGrubError::NoSolution( 170 self.build_derivation_tree(current_incompat_id), 171 )); 172 } else { 173 let (package, satisfier_search_result) = self.partial_solution.satisfier_search( 174 &self.incompatibility_store[current_incompat_id], 175 &self.incompatibility_store, 176 ); 177 match satisfier_search_result { 178 DifferentDecisionLevels { 179 previous_satisfier_level, 180 } => { 181 self.backtrack( 182 current_incompat_id, 183 current_incompat_changed, 184 previous_satisfier_level, 185 ); 186 return Ok((package, current_incompat_id)); 187 } 188 SameDecisionLevels { satisfier_cause } => { 189 let prior_cause = Incompatibility::prior_cause( 190 current_incompat_id, 191 satisfier_cause, 192 &package, 193 &self.incompatibility_store, 194 ); 195 current_incompat_id = self.incompatibility_store.alloc(prior_cause); 196 current_incompat_changed = true; 197 } 198 } 199 } 200 } 201 } 202 203 /// Backtracking. backtrack( &mut self, incompat: IncompId<P, V>, incompat_changed: bool, decision_level: DecisionLevel, )204 fn backtrack( 205 &mut self, 206 incompat: IncompId<P, V>, 207 incompat_changed: bool, 208 decision_level: DecisionLevel, 209 ) { 210 self.partial_solution 211 .backtrack(decision_level, &self.incompatibility_store); 212 self.contradicted_incompatibilities.clear(); 213 if incompat_changed { 214 self.merge_incompatibility(incompat); 215 } 216 } 217 218 /// Add this incompatibility into the set of all incompatibilities. 219 /// 220 /// Pub collapses identical dependencies from adjacent package versions 221 /// into individual incompatibilities. 222 /// This substantially reduces the total number of incompatibilities 223 /// and makes it much easier for Pub to reason about multiple versions of packages at once. 224 /// 225 /// For example, rather than representing 226 /// foo 1.0.0 depends on bar ^1.0.0 and 227 /// foo 1.1.0 depends on bar ^1.0.0 228 /// as two separate incompatibilities, 229 /// they are collapsed together into the single incompatibility {foo ^1.0.0, not bar ^1.0.0} 230 /// (provided that no other version of foo exists between 1.0.0 and 2.0.0). 231 /// We could collapse them into { foo (1.0.0 ∪ 1.1.0), not bar ^1.0.0 } 232 /// without having to check the existence of other versions though. 233 /// 234 /// Here we do the simple stupid thing of just growing the Vec. 235 /// It may not be trivial since those incompatibilities 236 /// may already have derived others. merge_incompatibility(&mut self, id: IncompId<P, V>)237 fn merge_incompatibility(&mut self, id: IncompId<P, V>) { 238 for (pkg, _term) in self.incompatibility_store[id].iter() { 239 self.incompatibilities 240 .entry(pkg.clone()) 241 .or_default() 242 .push(id); 243 } 244 } 245 246 // Error reporting ######################################################### 247 build_derivation_tree(&self, incompat: IncompId<P, V>) -> DerivationTree<P, V>248 fn build_derivation_tree(&self, incompat: IncompId<P, V>) -> DerivationTree<P, V> { 249 let shared_ids = self.find_shared_ids(incompat); 250 Incompatibility::build_derivation_tree(incompat, &shared_ids, &self.incompatibility_store) 251 } 252 find_shared_ids(&self, incompat: IncompId<P, V>) -> Set<IncompId<P, V>>253 fn find_shared_ids(&self, incompat: IncompId<P, V>) -> Set<IncompId<P, V>> { 254 let mut all_ids = Set::new(); 255 let mut shared_ids = Set::new(); 256 let mut stack = vec![incompat]; 257 while let Some(i) = stack.pop() { 258 if let Some((id1, id2)) = self.incompatibility_store[i].causes() { 259 if all_ids.contains(&i) { 260 shared_ids.insert(i); 261 } else { 262 all_ids.insert(i); 263 stack.push(id1); 264 stack.push(id2); 265 } 266 } 267 } 268 shared_ids 269 } 270 } 271