Skip to content

Commit ffef172

Browse files
konstinzanieb
andauthored
Improve documentation of partial solution (#294)
* Improve documentation of partial solution Improve the docstrings for the partial solution and rename some fields to be easier to understand. * Update src/internal/core.rs Co-authored-by: Zanie Blue <contact@zanie.dev> * Update src/internal/partial_solution.rs Co-authored-by: Zanie Blue <contact@zanie.dev> * Add context --------- Co-authored-by: Zanie Blue <contact@zanie.dev>
1 parent e9170cb commit ffef172

File tree

2 files changed

+104
-47
lines changed

2 files changed

+104
-47
lines changed

src/internal/core.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ pub(crate) struct State<DP: DependencyProvider> {
2121
#[allow(clippy::type_complexity)]
2222
incompatibilities: Map<Id<DP::P>, Vec<IncompDpId<DP>>>,
2323

24-
/// Store the ids of incompatibilities that are already contradicted.
24+
/// As an optimization, store the ids of incompatibilities that are already contradicted.
25+
///
2526
/// For each one keep track of the decision level when it was found to be contradicted.
2627
/// These will stay contradicted until we have backtracked beyond its associated decision level.
2728
contradicted_incompatibilities: Map<IncompDpId<DP>, DecisionLevel>,

src/internal/partial_solution.rs

Lines changed: 102 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -30,36 +30,61 @@ impl DecisionLevel {
3030
#[derive(Clone, Debug)]
3131
pub(crate) struct PartialSolution<DP: DependencyProvider> {
3232
next_global_index: u32,
33+
/// The number of decisions that have been made, equal to the number of packages with decisions.
3334
current_decision_level: DecisionLevel,
34-
/// `package_assignments` is primarily a HashMap from a package to its
35-
/// `PackageAssignments`. But it can also keep the items in an order.
36-
/// We maintain three sections in this order:
37-
/// 1. `[..current_decision_level]` Are packages that have had a decision made sorted by the `decision_level`.
38-
/// This makes it very efficient to extract the solution, And to backtrack to a particular decision level.
39-
/// 2. `[current_decision_level..changed_this_decision_level]` Are packages that have **not** had there assignments
40-
/// changed since the last time `prioritize` has been called. Within this range there is no sorting.
41-
/// 3. `[changed_this_decision_level..]` Contains all packages that **have** had there assignments changed since
42-
/// the last time `prioritize` has been called. The inverse is not necessarily true, some packages in the range
43-
/// did not have a change. Within this range there is no sorting.
35+
/// Store for all known package decisions and package derivations.
36+
///
37+
/// "assignment" refers to both packages with decisions and package with only derivations and
38+
/// no decision yet. We combine this in a single index map, where different sections (of
39+
/// indexes) contain package with different level of information, and make a decision moves a
40+
/// package from the derivations sections to the decisions section.
41+
///
42+
/// `[..current_decision_level]`: Packages that have had a decision made, sorted by the
43+
/// `decision_level`. The section is can be seen as the partial solution, it contains a
44+
/// mapping from package name to decided version. The sorting makes it very efficient to
45+
/// extract the solution, and to backtrack to a particular decision level. The
46+
/// `AssignmentsIntersection` is always a `Decision`.
47+
///
48+
/// `[prioritize_decision_level..]`: Packages that are dependencies of some other package,
49+
/// but have not yet been decided. The `AssignmentsIntersection` is always a `Derivations`, the
50+
/// derivations store the obligations from the decided packages. This section has two
51+
/// subsections to optimize the number of `prioritize` calls:
52+
///
53+
/// `[current_decision_level..prioritize_decision_level]`: The assignments of packages in this
54+
/// range have not changed since the last time `prioritize` has been called, their
55+
/// priority in `prioritized_potential_packages` is fresh. There is no sorting within this
56+
/// range.
57+
///
58+
/// `[prioritize_decision_level..]`: The assignments of packages in this range may have changed
59+
/// since the last time `prioritize` has been called, their priority in
60+
/// `prioritized_potential_packages` needs to be refreshed. There is no sorting within this
61+
/// range.
4462
#[allow(clippy::type_complexity)]
4563
package_assignments: FnvIndexMap<Id<DP::P>, PackageAssignments<DP::P, DP::VS, DP::M>>,
46-
/// `prioritized_potential_packages` is primarily a HashMap from a package with no desition and a positive assignment
47-
/// to its `Priority`. But, it also maintains a max heap of packages by `Priority` order.
64+
/// Index into `package_assignments` to decide which packages need to be re-prioritized.
65+
prioritize_decision_level: usize,
66+
/// The undecided packages order by their `Priority`.
67+
///
68+
/// The max heap allows quickly `pop`ing the highest priority package.
4869
prioritized_potential_packages:
4970
PriorityQueue<Id<DP::P>, DP::Priority, BuildHasherDefault<FxHasher>>,
50-
changed_this_decision_level: usize,
71+
/// Whether we have never backtracked, to enable fast path optimizations.
5172
has_ever_backtracked: bool,
5273
}
5374

54-
/// Package assignments contain the potential decision and derivations
55-
/// that have already been made for a given package,
56-
/// as well as the intersection of terms by all of these.
75+
/// A package assignment is either a decision or a list of (accumulated) derivations without a
76+
/// decision.
5777
#[derive(Clone, Debug)]
5878
struct PackageAssignments<P: Package, VS: VersionSet, M: Eq + Clone + Debug + Display> {
79+
/// Whether the assigment is a decision or a derivation.
80+
assignments_intersection: AssignmentsIntersection<VS>,
81+
/// All constraints on the package version from previous decisions, accumulated by decision
82+
/// level.
83+
dated_derivations: SmallVec<DatedDerivation<P, VS, M>>,
84+
/// Smallest [`DecisionLevel`] in `dated_derivations`.
5985
smallest_decision_level: DecisionLevel,
86+
/// Highest [`DecisionLevel`] in `dated_derivations`.
6087
highest_decision_level: DecisionLevel,
61-
dated_derivations: SmallVec<DatedDerivation<P, VS, M>>,
62-
assignments_intersection: AssignmentsIntersection<VS>,
6388
}
6489

6590
impl<P: Package, VS: VersionSet, M: Eq + Clone + Debug + Display> Display
@@ -85,8 +110,13 @@ impl<P: Package, VS: VersionSet, M: Eq + Clone + Debug + Display> Display
85110
#[derive(Clone, Debug)]
86111
struct DatedDerivation<P: Package, VS: VersionSet, M: Eq + Clone + Debug + Display> {
87112
global_index: u32,
113+
/// Only decisions up this level has been used to compute the accumulated term.
88114
decision_level: DecisionLevel,
89115
cause: IncompId<P, VS, M>,
116+
/// The intersection of all terms up to `decision_level`.
117+
///
118+
/// It may not contain all terms of this `decision_level`, there may be more than one
119+
/// `DatedDerivation` per decision level.
90120
accumulated_intersection: Term<VS>,
91121
}
92122

@@ -100,15 +130,25 @@ impl<P: Package, VS: VersionSet, M: Eq + Clone + Debug + Display> Display
100130

101131
#[derive(Clone, Debug)]
102132
enum AssignmentsIntersection<VS: VersionSet> {
103-
Decision((u32, VS::V, Term<VS>)),
133+
/// A decision on package for version has been made at the given level.
134+
Decision {
135+
decision_level: u32,
136+
version: VS::V,
137+
/// The version, but as positive singleton term.
138+
term: Term<VS>,
139+
},
104140
Derivations(Term<VS>),
105141
}
106142

107143
impl<VS: VersionSet> Display for AssignmentsIntersection<VS> {
108144
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
109145
match self {
110-
Self::Decision((lvl, version, _)) => {
111-
write!(f, "Decision: level {}, v = {}", lvl, version)
146+
Self::Decision {
147+
decision_level,
148+
version,
149+
term: _,
150+
} => {
151+
write!(f, "Decision: level {}, v = {}", decision_level, version)
112152
}
113153
Self::Derivations(term) => write!(f, "Derivations term: {}", term),
114154
}
@@ -135,7 +175,7 @@ impl<DP: DependencyProvider> PartialSolution<DP> {
135175
current_decision_level: DecisionLevel(0),
136176
package_assignments: FnvIndexMap::default(),
137177
prioritized_potential_packages: PriorityQueue::default(),
138-
changed_this_decision_level: 0,
178+
prioritize_decision_level: 0,
139179
has_ever_backtracked: false,
140180
}
141181
}
@@ -173,7 +213,9 @@ impl<DP: DependencyProvider> PartialSolution<DP> {
173213
None => panic!("Derivations must already exist"),
174214
Some(pa) => match &pa.assignments_intersection {
175215
// Cannot be called when a decision has already been taken.
176-
AssignmentsIntersection::Decision(_) => panic!("Already existing decision"),
216+
AssignmentsIntersection::Decision { .. } => {
217+
panic!("Already existing decision")
218+
}
177219
// Cannot be called if the versions is not contained in the terms' intersection.
178220
AssignmentsIntersection::Derivations(term) => {
179221
debug_assert!(
@@ -187,7 +229,7 @@ impl<DP: DependencyProvider> PartialSolution<DP> {
187229
},
188230
}
189231
assert_eq!(
190-
self.changed_this_decision_level,
232+
self.prioritize_decision_level,
191233
self.package_assignments.len()
192234
);
193235
}
@@ -198,11 +240,11 @@ impl<DP: DependencyProvider> PartialSolution<DP> {
198240
.get_full_mut(&package)
199241
.expect("Derivations must already exist");
200242
pa.highest_decision_level = self.current_decision_level;
201-
pa.assignments_intersection = AssignmentsIntersection::Decision((
202-
self.next_global_index,
203-
version.clone(),
204-
Term::exact(version),
205-
));
243+
pa.assignments_intersection = AssignmentsIntersection::Decision {
244+
decision_level: self.next_global_index,
245+
version: version.clone(),
246+
term: Term::exact(version),
247+
};
206248
// Maintain that the beginning of the `package_assignments` Have all decisions in sorted order.
207249
if new_idx != old_idx {
208250
self.package_assignments.swap_indices(new_idx, old_idx);
@@ -233,17 +275,17 @@ impl<DP: DependencyProvider> PartialSolution<DP> {
233275
pa.highest_decision_level = self.current_decision_level;
234276
match &mut pa.assignments_intersection {
235277
// Check that add_derivation is never called in the wrong context.
236-
AssignmentsIntersection::Decision(_) => {
278+
AssignmentsIntersection::Decision { .. } => {
237279
panic!("add_derivation should not be called after a decision")
238280
}
239281
AssignmentsIntersection::Derivations(t) => {
240282
*t = t.intersection(&dated_derivation.accumulated_intersection);
241283
dated_derivation.accumulated_intersection = t.clone();
242284
if t.is_positive() {
243-
// we can use `swap_indices` to make `changed_this_decision_level` only go down by 1
285+
// we can use `swap_indices` to make `prioritize_decision_level` only go down by 1
244286
// but the copying is slower then the larger search
245-
self.changed_this_decision_level =
246-
std::cmp::min(self.changed_this_decision_level, idx);
287+
self.prioritize_decision_level =
288+
std::cmp::min(self.prioritize_decision_level, idx);
247289
}
248290
}
249291
}
@@ -252,8 +294,8 @@ impl<DP: DependencyProvider> PartialSolution<DP> {
252294
Entry::Vacant(v) => {
253295
let term = dated_derivation.accumulated_intersection.clone();
254296
if term.is_positive() {
255-
self.changed_this_decision_level =
256-
std::cmp::min(self.changed_this_decision_level, pa_last_index);
297+
self.prioritize_decision_level =
298+
std::cmp::min(self.prioritize_decision_level, pa_last_index);
257299
}
258300
v.insert(PackageAssignments {
259301
smallest_decision_level: self.current_decision_level,
@@ -270,12 +312,12 @@ impl<DP: DependencyProvider> PartialSolution<DP> {
270312
&mut self,
271313
prioritizer: impl Fn(Id<DP::P>, &DP::VS) -> DP::Priority,
272314
) -> Option<Id<DP::P>> {
273-
let check_all = self.changed_this_decision_level
315+
let check_all = self.prioritize_decision_level
274316
== self.current_decision_level.0.saturating_sub(1) as usize;
275317
let current_decision_level = self.current_decision_level;
276318
let prioritized_potential_packages = &mut self.prioritized_potential_packages;
277319
self.package_assignments
278-
.get_range(self.changed_this_decision_level..)
320+
.get_range(self.prioritize_decision_level..)
279321
.unwrap()
280322
.iter()
281323
.filter(|(_, pa)| {
@@ -290,7 +332,7 @@ impl<DP: DependencyProvider> PartialSolution<DP> {
290332
let priority = prioritizer(p, r);
291333
prioritized_potential_packages.push(p, priority);
292334
});
293-
self.changed_this_decision_level = self.package_assignments.len();
335+
self.prioritize_decision_level = self.package_assignments.len();
294336
prioritized_potential_packages.pop().map(|(p, _)| p)
295337
}
296338

@@ -302,7 +344,11 @@ impl<DP: DependencyProvider> PartialSolution<DP> {
302344
.iter()
303345
.take(self.current_decision_level.0 as usize)
304346
.map(|(&p, pa)| match &pa.assignments_intersection {
305-
AssignmentsIntersection::Decision((_, v, _)) => (p, v.clone()),
347+
AssignmentsIntersection::Decision {
348+
decision_level: _,
349+
version: v,
350+
term: _,
351+
} => (p, v.clone()),
306352
AssignmentsIntersection::Derivations(_) => {
307353
panic!("Derivations in the Decision part")
308354
}
@@ -347,7 +393,7 @@ impl<DP: DependencyProvider> PartialSolution<DP> {
347393
});
348394
// Throw away all stored priority levels, And mark that they all need to be recomputed.
349395
self.prioritized_potential_packages.clear();
350-
self.changed_this_decision_level = self.current_decision_level.0.saturating_sub(1) as usize;
396+
self.prioritize_decision_level = self.current_decision_level.0.saturating_sub(1) as usize;
351397
self.has_ever_backtracked = true;
352398
}
353399

@@ -484,7 +530,11 @@ impl<DP: DependencyProvider> PartialSolution<DP> {
484530
} else {
485531
match &satisfier_pa.assignments_intersection {
486532
AssignmentsIntersection::Derivations(_) => panic!("must be a decision"),
487-
AssignmentsIntersection::Decision((_, _, term)) => term.clone(),
533+
AssignmentsIntersection::Decision {
534+
decision_level: _,
535+
version: _,
536+
term,
537+
} => term.clone(),
488538
}
489539
};
490540

@@ -532,9 +582,11 @@ impl<P: Package, VS: VersionSet, M: Eq + Clone + Debug + Display> PackageAssignm
532582
// If it wasn't found in the derivations,
533583
// it must be the decision which is last (if called in the right context).
534584
match &self.assignments_intersection {
535-
AssignmentsIntersection::Decision((global_index, _, _)) => {
536-
(None, *global_index, self.highest_decision_level)
537-
}
585+
AssignmentsIntersection::Decision {
586+
decision_level: global_index,
587+
version: _,
588+
term: _,
589+
} => (None, *global_index, self.highest_decision_level),
538590
AssignmentsIntersection::Derivations(accumulated_intersection) => {
539591
unreachable!(
540592
concat!(
@@ -555,7 +607,11 @@ impl<VS: VersionSet> AssignmentsIntersection<VS> {
555607
/// Returns the term intersection of all assignments (decision included).
556608
fn term(&self) -> &Term<VS> {
557609
match self {
558-
Self::Decision((_, _, term)) => term,
610+
Self::Decision {
611+
decision_level: _,
612+
version: _,
613+
term,
614+
} => term,
559615
Self::Derivations(term) => term,
560616
}
561617
}
@@ -566,7 +622,7 @@ impl<VS: VersionSet> AssignmentsIntersection<VS> {
566622
/// in the partial solution.
567623
fn potential_package_filter<P: Package>(&self, package: Id<P>) -> Option<(Id<P>, &VS)> {
568624
match self {
569-
Self::Decision(_) => None,
625+
Self::Decision { .. } => None,
570626
Self::Derivations(term_intersection) => {
571627
if term_intersection.is_positive() {
572628
Some((package, term_intersection.unwrap_positive()))

0 commit comments

Comments
 (0)