1 // SPDX-License-Identifier: MPL-2.0
2 
3 //! A Memory acts like a structured partial solution
4 //! where terms are regrouped by package in a [Map](crate::type_aliases::Map).
5 
6 use crate::internal::arena::Arena;
7 use crate::internal::incompatibility::{IncompId, Incompatibility, Relation};
8 use crate::internal::small_map::SmallMap;
9 use crate::package::Package;
10 use crate::range::Range;
11 use crate::term::Term;
12 use crate::type_aliases::{Map, SelectedDependencies};
13 use crate::version::Version;
14 
15 use super::small_vec::SmallVec;
16 
17 #[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
18 pub struct DecisionLevel(pub u32);
19 
20 impl DecisionLevel {
increment(self) -> Self21     pub fn increment(self) -> Self {
22         Self(self.0 + 1)
23     }
24 }
25 
26 /// The partial solution contains all package assignments,
27 /// organized by package and historically ordered.
28 #[derive(Clone, Debug)]
29 pub struct PartialSolution<P: Package, V: Version> {
30     next_global_index: u32,
31     current_decision_level: DecisionLevel,
32     package_assignments: Map<P, PackageAssignments<P, V>>,
33 }
34 
35 /// Package assignments contain the potential decision and derivations
36 /// that have already been made for a given package,
37 /// as well as the intersection of terms by all of these.
38 #[derive(Clone, Debug)]
39 struct PackageAssignments<P: Package, V: Version> {
40     smallest_decision_level: DecisionLevel,
41     highest_decision_level: DecisionLevel,
42     dated_derivations: SmallVec<DatedDerivation<P, V>>,
43     assignments_intersection: AssignmentsIntersection<V>,
44 }
45 
46 #[derive(Clone, Debug)]
47 pub struct DatedDerivation<P: Package, V: Version> {
48     global_index: u32,
49     decision_level: DecisionLevel,
50     cause: IncompId<P, V>,
51 }
52 
53 #[derive(Clone, Debug)]
54 enum AssignmentsIntersection<V: Version> {
55     Decision((u32, V, Term<V>)),
56     Derivations(Term<V>),
57 }
58 
59 #[derive(Clone, Debug)]
60 pub enum SatisfierSearch<P: Package, V: Version> {
61     DifferentDecisionLevels {
62         previous_satisfier_level: DecisionLevel,
63     },
64     SameDecisionLevels {
65         satisfier_cause: IncompId<P, V>,
66     },
67 }
68 
69 impl<P: Package, V: Version> PartialSolution<P, V> {
70     /// Initialize an empty PartialSolution.
empty() -> Self71     pub fn empty() -> Self {
72         Self {
73             next_global_index: 0,
74             current_decision_level: DecisionLevel(0),
75             package_assignments: Map::default(),
76         }
77     }
78 
79     /// Add a decision.
add_decision(&mut self, package: P, version: V)80     pub fn add_decision(&mut self, package: P, version: V) {
81         // Check that add_decision is never used in the wrong context.
82         if cfg!(debug_assertions) {
83             match self.package_assignments.get_mut(&package) {
84                 None => panic!("Derivations must already exist"),
85                 Some(pa) => match &pa.assignments_intersection {
86                     // Cannot be called when a decision has already been taken.
87                     AssignmentsIntersection::Decision(_) => panic!("Already existing decision"),
88                     // Cannot be called if the versions is not contained in the terms intersection.
89                     AssignmentsIntersection::Derivations(term) => {
90                         debug_assert!(term.contains(&version))
91                     }
92                 },
93             }
94         }
95         self.current_decision_level = self.current_decision_level.increment();
96         let mut pa = self
97             .package_assignments
98             .get_mut(&package)
99             .expect("Derivations must already exist");
100         pa.highest_decision_level = self.current_decision_level;
101         pa.assignments_intersection = AssignmentsIntersection::Decision((
102             self.next_global_index,
103             version.clone(),
104             Term::exact(version),
105         ));
106         self.next_global_index += 1;
107     }
108 
109     /// Add a derivation.
add_derivation( &mut self, package: P, cause: IncompId<P, V>, store: &Arena<Incompatibility<P, V>>, )110     pub fn add_derivation(
111         &mut self,
112         package: P,
113         cause: IncompId<P, V>,
114         store: &Arena<Incompatibility<P, V>>,
115     ) {
116         use std::collections::hash_map::Entry;
117         let term = store[cause].get(&package).unwrap().negate();
118         let dated_derivation = DatedDerivation {
119             global_index: self.next_global_index,
120             decision_level: self.current_decision_level,
121             cause,
122         };
123         self.next_global_index += 1;
124         match self.package_assignments.entry(package) {
125             Entry::Occupied(mut occupied) => {
126                 let mut pa = occupied.get_mut();
127                 pa.highest_decision_level = self.current_decision_level;
128                 match &mut pa.assignments_intersection {
129                     // Check that add_derivation is never called in the wrong context.
130                     AssignmentsIntersection::Decision(_) => {
131                         panic!("add_derivation should not be called after a decision")
132                     }
133                     AssignmentsIntersection::Derivations(t) => {
134                         *t = t.intersection(&term);
135                     }
136                 }
137                 pa.dated_derivations.push(dated_derivation);
138             }
139             Entry::Vacant(v) => {
140                 v.insert(PackageAssignments {
141                     smallest_decision_level: self.current_decision_level,
142                     highest_decision_level: self.current_decision_level,
143                     dated_derivations: SmallVec::One([dated_derivation]),
144                     assignments_intersection: AssignmentsIntersection::Derivations(term),
145                 });
146             }
147         }
148     }
149 
150     /// Extract potential packages for the next iteration of unit propagation.
151     /// Return `None` if there is no suitable package anymore, which stops the algorithm.
152     /// A package is a potential pick if there isn't an already
153     /// selected version (no "decision")
154     /// and if it contains at least one positive derivation term
155     /// in the partial solution.
potential_packages(&self) -> Option<impl Iterator<Item = (&P, &Range<V>)>>156     pub fn potential_packages(&self) -> Option<impl Iterator<Item = (&P, &Range<V>)>> {
157         let mut iter = self
158             .package_assignments
159             .iter()
160             .filter_map(|(p, pa)| pa.assignments_intersection.potential_package_filter(p))
161             .peekable();
162         if iter.peek().is_some() {
163             Some(iter)
164         } else {
165             None
166         }
167     }
168 
169     /// If a partial solution has, for every positive derivation,
170     /// a corresponding decision that satisfies that assignment,
171     /// it's a total solution and version solving has succeeded.
extract_solution(&self) -> Option<SelectedDependencies<P, V>>172     pub fn extract_solution(&self) -> Option<SelectedDependencies<P, V>> {
173         let mut solution = Map::default();
174         for (p, pa) in &self.package_assignments {
175             match &pa.assignments_intersection {
176                 AssignmentsIntersection::Decision((_, v, _)) => {
177                     solution.insert(p.clone(), v.clone());
178                 }
179                 AssignmentsIntersection::Derivations(term) => {
180                     if term.is_positive() {
181                         return None;
182                     }
183                 }
184             }
185         }
186         Some(solution)
187     }
188 
189     /// Backtrack the partial solution to a given decision level.
backtrack( &mut self, decision_level: DecisionLevel, store: &Arena<Incompatibility<P, V>>, )190     pub fn backtrack(
191         &mut self,
192         decision_level: DecisionLevel,
193         store: &Arena<Incompatibility<P, V>>,
194     ) {
195         self.current_decision_level = decision_level;
196         self.package_assignments.retain(|p, pa| {
197             if pa.smallest_decision_level > decision_level {
198                 // Remove all entries that have a smallest decision level higher than the backtrack target.
199                 false
200             } else if pa.highest_decision_level <= decision_level {
201                 // Do not change entries older than the backtrack decision level target.
202                 true
203             } else {
204                 // smallest_decision_level <= decision_level < highest_decision_level
205                 //
206                 // Since decision_level < highest_decision_level,
207                 // We can be certain that there will be no decision in this package assignments
208                 // after backtracking, because such decision would have been the last
209                 // assignment and it would have the "highest_decision_level".
210 
211                 // Truncate the history.
212                 while pa.dated_derivations.last().map(|dd| dd.decision_level) > Some(decision_level)
213                 {
214                     pa.dated_derivations.pop();
215                 }
216                 debug_assert!(!pa.dated_derivations.is_empty());
217 
218                 // Update highest_decision_level.
219                 pa.highest_decision_level = pa.dated_derivations.last().unwrap().decision_level;
220 
221                 // Recompute the assignments intersection.
222                 pa.assignments_intersection = AssignmentsIntersection::Derivations(
223                     pa.dated_derivations
224                         .iter()
225                         .fold(Term::any(), |acc, dated_derivation| {
226                             let term = store[dated_derivation.cause].get(&p).unwrap().negate();
227                             acc.intersection(&term)
228                         }),
229                 );
230                 true
231             }
232         });
233     }
234 
235     /// We can add the version to the partial solution as a decision
236     /// if it doesn't produce any conflict with the new incompatibilities.
237     /// In practice I think it can only produce a conflict if one of the dependencies
238     /// (which are used to make the new incompatibilities)
239     /// is already in the partial solution with an incompatible version.
add_version( &mut self, package: P, version: V, new_incompatibilities: std::ops::Range<IncompId<P, V>>, store: &Arena<Incompatibility<P, V>>, )240     pub fn add_version(
241         &mut self,
242         package: P,
243         version: V,
244         new_incompatibilities: std::ops::Range<IncompId<P, V>>,
245         store: &Arena<Incompatibility<P, V>>,
246     ) {
247         let exact = Term::exact(version.clone());
248         let not_satisfied = |incompat: &Incompatibility<P, V>| {
249             incompat.relation(|p| {
250                 if p == &package {
251                     Some(&exact)
252                 } else {
253                     self.term_intersection_for_package(p)
254                 }
255             }) != Relation::Satisfied
256         };
257 
258         // Check none of the dependencies (new_incompatibilities)
259         // would create a conflict (be satisfied).
260         if store[new_incompatibilities].iter().all(not_satisfied) {
261             self.add_decision(package, version);
262         }
263     }
264 
265     /// Check if the terms in the partial solution satisfy the incompatibility.
relation(&self, incompat: &Incompatibility<P, V>) -> Relation<P>266     pub fn relation(&self, incompat: &Incompatibility<P, V>) -> Relation<P> {
267         incompat.relation(|package| self.term_intersection_for_package(package))
268     }
269 
270     /// Retrieve intersection of terms related to package.
term_intersection_for_package(&self, package: &P) -> Option<&Term<V>>271     pub fn term_intersection_for_package(&self, package: &P) -> Option<&Term<V>> {
272         self.package_assignments
273             .get(package)
274             .map(|pa| pa.assignments_intersection.term())
275     }
276 
277     /// Figure out if the satisfier and previous satisfier are of different decision levels.
satisfier_search( &self, incompat: &Incompatibility<P, V>, store: &Arena<Incompatibility<P, V>>, ) -> (P, SatisfierSearch<P, V>)278     pub fn satisfier_search(
279         &self,
280         incompat: &Incompatibility<P, V>,
281         store: &Arena<Incompatibility<P, V>>,
282     ) -> (P, SatisfierSearch<P, V>) {
283         let satisfied_map = Self::find_satisfier(incompat, &self.package_assignments, store);
284         let (satisfier_package, &(satisfier_index, _, satisfier_decision_level)) = satisfied_map
285             .iter()
286             .max_by_key(|(_p, (_, global_index, _))| global_index)
287             .unwrap();
288         let satisfier_package = satisfier_package.clone();
289         let previous_satisfier_level = Self::find_previous_satisfier(
290             incompat,
291             &satisfier_package,
292             satisfied_map,
293             &self.package_assignments,
294             store,
295         );
296         if previous_satisfier_level < satisfier_decision_level {
297             let search_result = SatisfierSearch::DifferentDecisionLevels {
298                 previous_satisfier_level,
299             };
300             (satisfier_package, search_result)
301         } else {
302             let satisfier_pa = self.package_assignments.get(&satisfier_package).unwrap();
303             let dd = &satisfier_pa.dated_derivations[satisfier_index];
304             let search_result = SatisfierSearch::SameDecisionLevels {
305                 satisfier_cause: dd.cause,
306             };
307             (satisfier_package, search_result)
308         }
309     }
310 
311     /// A satisfier is the earliest assignment in partial solution such that the incompatibility
312     /// is satisfied by the partial solution up to and including that assignment.
313     ///
314     /// Returns a map indicating for each package term, when that was first satisfied in history.
315     /// If we effectively found a satisfier, the returned map must be the same size that incompat.
316     ///
317     /// Question: This is possible since we added a "global_index" to every dated_derivation.
318     /// It would be nice if we could get rid of it, but I don't know if then it will be possible
319     /// to return a coherent previous_satisfier_level.
find_satisfier( incompat: &Incompatibility<P, V>, package_assignments: &Map<P, PackageAssignments<P, V>>, store: &Arena<Incompatibility<P, V>>, ) -> SmallMap<P, (usize, u32, DecisionLevel)>320     fn find_satisfier(
321         incompat: &Incompatibility<P, V>,
322         package_assignments: &Map<P, PackageAssignments<P, V>>,
323         store: &Arena<Incompatibility<P, V>>,
324     ) -> SmallMap<P, (usize, u32, DecisionLevel)> {
325         let mut satisfied = SmallMap::Empty;
326         for (package, incompat_term) in incompat.iter() {
327             let pa = package_assignments.get(package).expect("Must exist");
328             satisfied.insert(
329                 package.clone(),
330                 pa.satisfier(package, incompat_term, Term::any(), store),
331             );
332         }
333         satisfied
334     }
335 
336     /// Earliest assignment in the partial solution before satisfier
337     /// such that incompatibility is satisfied by the partial solution up to
338     /// and including that assignment plus satisfier.
find_previous_satisfier( incompat: &Incompatibility<P, V>, satisfier_package: &P, mut satisfied_map: SmallMap<P, (usize, u32, DecisionLevel)>, package_assignments: &Map<P, PackageAssignments<P, V>>, store: &Arena<Incompatibility<P, V>>, ) -> DecisionLevel339     fn find_previous_satisfier(
340         incompat: &Incompatibility<P, V>,
341         satisfier_package: &P,
342         mut satisfied_map: SmallMap<P, (usize, u32, DecisionLevel)>,
343         package_assignments: &Map<P, PackageAssignments<P, V>>,
344         store: &Arena<Incompatibility<P, V>>,
345     ) -> DecisionLevel {
346         // First, let's retrieve the previous derivations and the initial accum_term.
347         let satisfier_pa = package_assignments.get(satisfier_package).unwrap();
348         let (satisfier_index, _gidx, _dl) = satisfied_map.get_mut(satisfier_package).unwrap();
349 
350         let accum_term = if *satisfier_index == satisfier_pa.dated_derivations.len() {
351             match &satisfier_pa.assignments_intersection {
352                 AssignmentsIntersection::Derivations(_) => panic!("must be a decision"),
353                 AssignmentsIntersection::Decision((_, _, term)) => term.clone(),
354             }
355         } else {
356             let dd = &satisfier_pa.dated_derivations[*satisfier_index];
357             store[dd.cause].get(satisfier_package).unwrap().negate()
358         };
359 
360         let incompat_term = incompat
361             .get(satisfier_package)
362             .expect("satisfier package not in incompat");
363 
364         satisfied_map.insert(
365             satisfier_package.clone(),
366             satisfier_pa.satisfier(satisfier_package, incompat_term, accum_term, store),
367         );
368 
369         // Finally, let's identify the decision level of that previous satisfier.
370         let (_, &(_, _, decision_level)) = satisfied_map
371             .iter()
372             .max_by_key(|(_p, (_, global_index, _))| global_index)
373             .unwrap();
374         decision_level.max(DecisionLevel(1))
375     }
376 }
377 
378 impl<P: Package, V: Version> PackageAssignments<P, V> {
satisfier( &self, package: &P, incompat_term: &Term<V>, start_term: Term<V>, store: &Arena<Incompatibility<P, V>>, ) -> (usize, u32, DecisionLevel)379     fn satisfier(
380         &self,
381         package: &P,
382         incompat_term: &Term<V>,
383         start_term: Term<V>,
384         store: &Arena<Incompatibility<P, V>>,
385     ) -> (usize, u32, DecisionLevel) {
386         // Term where we accumulate intersections until incompat_term is satisfied.
387         let mut accum_term = start_term;
388         // Indicate if we found a satisfier in the list of derivations, otherwise it will be the decision.
389         for (idx, dated_derivation) in self.dated_derivations.iter().enumerate() {
390             let this_term = store[dated_derivation.cause].get(package).unwrap().negate();
391             accum_term = accum_term.intersection(&this_term);
392             if accum_term.subset_of(incompat_term) {
393                 // We found the derivation causing satisfaction.
394                 return (
395                     idx,
396                     dated_derivation.global_index,
397                     dated_derivation.decision_level,
398                 );
399             }
400         }
401         // If it wasn't found in the derivations,
402         // it must be the decision which is last (if called in the right context).
403         match self.assignments_intersection {
404             AssignmentsIntersection::Decision((global_index, _, _)) => (
405                 self.dated_derivations.len(),
406                 global_index,
407                 self.highest_decision_level,
408             ),
409             AssignmentsIntersection::Derivations(_) => {
410                 panic!("This must be a decision")
411             }
412         }
413     }
414 }
415 
416 impl<V: Version> AssignmentsIntersection<V> {
417     /// Returns the term intersection of all assignments (decision included).
term(&self) -> &Term<V>418     fn term(&self) -> &Term<V> {
419         match self {
420             Self::Decision((_, _, term)) => term,
421             Self::Derivations(term) => term,
422         }
423     }
424 
425     /// A package is a potential pick if there isn't an already
426     /// selected version (no "decision")
427     /// and if it contains at least one positive derivation term
428     /// in the partial solution.
potential_package_filter<'a, P: Package>( &'a self, package: &'a P, ) -> Option<(&'a P, &'a Range<V>)>429     fn potential_package_filter<'a, P: Package>(
430         &'a self,
431         package: &'a P,
432     ) -> Option<(&'a P, &'a Range<V>)> {
433         match self {
434             Self::Decision(_) => None,
435             Self::Derivations(term_intersection) => {
436                 if term_intersection.is_positive() {
437                     Some((package, term_intersection.unwrap_positive()))
438                 } else {
439                     None
440                 }
441             }
442         }
443     }
444 }
445