use std::cmp::Reverse;
use std::fmt::{Debug, Display};
use std::hash::BuildHasherDefault;
use priority_queue::PriorityQueue;
use rustc_hash::FxHasher;
use crate::internal::{
Arena, HashArena, Id, IncompDpId, IncompId, Incompatibility, Relation, SmallMap, SmallVec,
};
use crate::{DependencyProvider, Package, Term, VersionSet};
type FnvIndexMap<K, V> = indexmap::IndexMap<K, V, BuildHasherDefault<FxHasher>>;
type FnvIndexSet<T> = indexmap::IndexSet<T, BuildHasherDefault<FxHasher>>;
#[derive(Debug, Copy, Clone, Ord, PartialOrd, Eq, PartialEq)]
pub(crate) struct DecisionLevel(pub(crate) u32);
impl DecisionLevel {
pub(crate) fn increment(self) -> Self {
Self(self.0 + 1)
}
}
#[derive(Clone, Debug)]
pub(crate) struct PartialSolution<DP: DependencyProvider> {
next_global_index: u32,
current_decision_level: DecisionLevel,
#[allow(clippy::type_complexity)]
package_assignments: FnvIndexMap<Id<DP::P>, PackageAssignments<DP::P, DP::VS, DP::M>>,
#[allow(clippy::type_complexity)]
prioritized_potential_packages:
PriorityQueue<Id<DP::P>, (DP::Priority, Reverse<u32>), BuildHasherDefault<FxHasher>>,
outdated_priorities: FnvIndexSet<Id<DP::P>>,
has_ever_backtracked: bool,
}
#[derive(Clone, Debug)]
struct PackageAssignments<P: Package, VS: VersionSet, M: Eq + Clone + Debug + Display> {
assignments_intersection: AssignmentsIntersection<VS>,
dated_derivations: SmallVec<DatedDerivation<P, VS, M>>,
smallest_decision_level: DecisionLevel,
highest_decision_level: DecisionLevel,
}
impl<P: Package, VS: VersionSet, M: Eq + Clone + Debug + Display> Display
for PackageAssignments<P, VS, M>
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let derivations: Vec<_> = self
.dated_derivations
.iter()
.map(|dd| dd.to_string())
.collect();
write!(
f,
"decision range: {:?}..{:?}\nderivations:\n {}\n,assignments_intersection: {}",
self.smallest_decision_level,
self.highest_decision_level,
derivations.join("\n "),
self.assignments_intersection
)
}
}
#[derive(Clone, Debug)]
struct DatedDerivation<P: Package, VS: VersionSet, M: Eq + Clone + Debug + Display> {
global_index: u32,
decision_level: DecisionLevel,
cause: IncompId<P, VS, M>,
accumulated_intersection: Term<VS>,
}
impl<P: Package, VS: VersionSet, M: Eq + Clone + Debug + Display> Display
for DatedDerivation<P, VS, M>
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{:?}, cause: {:?}", self.decision_level, self.cause)
}
}
#[derive(Clone, Debug)]
enum AssignmentsIntersection<VS: VersionSet> {
Decision {
decision_level: u32,
version: VS::V,
term: Term<VS>,
},
Derivations(Term<VS>),
}
impl<VS: VersionSet> Display for AssignmentsIntersection<VS> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Decision {
decision_level,
version,
term: _,
} => {
write!(f, "Decision: level {decision_level}, v = {version}")
}
Self::Derivations(term) => write!(f, "Derivations term: {term}"),
}
}
}
#[derive(Clone, Debug)]
pub(crate) enum SatisfierSearch<P: Package, VS: VersionSet, M: Eq + Clone + Debug + Display> {
DifferentDecisionLevels {
previous_satisfier_level: DecisionLevel,
},
SameDecisionLevels {
satisfier_cause: IncompId<P, VS, M>,
},
}
type SatisfiedMap<P, VS, M> = SmallMap<Id<P>, (Option<IncompId<P, VS, M>>, u32, DecisionLevel)>;
impl<DP: DependencyProvider> PartialSolution<DP> {
pub(crate) fn empty() -> Self {
Self {
next_global_index: 0,
current_decision_level: DecisionLevel(0),
package_assignments: FnvIndexMap::default(),
prioritized_potential_packages: PriorityQueue::default(),
outdated_priorities: FnvIndexSet::default(),
has_ever_backtracked: false,
}
}
pub(crate) fn display<'a>(&'a self, package_store: &'a HashArena<DP::P>) -> impl Display + 'a {
struct PSDisplay<'a, DP: DependencyProvider>(&'a PartialSolution<DP>, &'a HashArena<DP::P>);
impl<DP: DependencyProvider> Display for PSDisplay<'_, DP> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let mut assignments: Vec<_> = self
.0
.package_assignments
.iter()
.map(|(p, pa)| format!("{:?} = '{}': {}", p, self.1[*p], pa))
.collect();
assignments.sort();
write!(
f,
"next_global_index: {}\ncurrent_decision_level: {:?}\npackage_assignments:\n{}",
self.0.next_global_index,
self.0.current_decision_level,
assignments.join("\t\n")
)
}
}
PSDisplay(self, package_store)
}
pub(crate) fn add_decision(&mut self, package: Id<DP::P>, version: DP::V) {
if cfg!(debug_assertions) {
match self.package_assignments.get_mut(&package) {
None => panic!("Derivations must already exist"),
Some(pa) => match &pa.assignments_intersection {
AssignmentsIntersection::Decision { .. } => {
panic!("Already existing decision")
}
AssignmentsIntersection::Derivations(term) => {
debug_assert!(
term.contains(&version),
"{package:?}: {version} was expected to be contained in {term}",
)
}
},
}
}
let new_idx = self.current_decision_level.0 as usize;
self.current_decision_level = self.current_decision_level.increment();
let (old_idx, _, pa) = self
.package_assignments
.get_full_mut(&package)
.expect("Derivations must already exist");
pa.highest_decision_level = self.current_decision_level;
pa.assignments_intersection = AssignmentsIntersection::Decision {
decision_level: self.next_global_index,
version: version.clone(),
term: Term::exact(version),
};
if new_idx != old_idx {
self.package_assignments.swap_indices(new_idx, old_idx);
}
self.next_global_index += 1;
}
pub(crate) fn add_derivation(
&mut self,
package: Id<DP::P>,
cause: IncompDpId<DP>,
store: &Arena<Incompatibility<DP::P, DP::VS, DP::M>>,
) {
use indexmap::map::Entry;
let mut dated_derivation = DatedDerivation {
global_index: self.next_global_index,
decision_level: self.current_decision_level,
cause,
accumulated_intersection: store[cause].get(package).unwrap().negate(),
};
self.next_global_index += 1;
match self.package_assignments.entry(package) {
Entry::Occupied(mut occupied) => {
let pa = occupied.get_mut();
pa.highest_decision_level = self.current_decision_level;
match &mut pa.assignments_intersection {
AssignmentsIntersection::Decision { .. } => {
panic!("add_derivation should not be called after a decision")
}
AssignmentsIntersection::Derivations(t) => {
*t = t.intersection(&dated_derivation.accumulated_intersection);
dated_derivation.accumulated_intersection = t.clone();
if t.is_positive() {
self.outdated_priorities.insert(package);
}
}
}
pa.dated_derivations.push(dated_derivation);
}
Entry::Vacant(v) => {
let term = dated_derivation.accumulated_intersection.clone();
if term.is_positive() {
self.outdated_priorities.insert(package);
}
v.insert(PackageAssignments {
smallest_decision_level: self.current_decision_level,
highest_decision_level: self.current_decision_level,
dated_derivations: SmallVec::One([dated_derivation]),
assignments_intersection: AssignmentsIntersection::Derivations(term),
});
}
}
}
#[cold]
pub(crate) fn pick_highest_priority_pkg(
&mut self,
mut prioritizer: impl FnMut(Id<DP::P>, &DP::VS) -> DP::Priority,
) -> Option<(Id<DP::P>, &DP::VS)> {
let prioritized_potential_packages = &mut self.prioritized_potential_packages;
while let Some(p) = self.outdated_priorities.pop() {
let Some(pa) = self.package_assignments.get(&p) else {
continue;
};
let Some(r) = pa.assignments_intersection.potential_package_filter() else {
continue;
};
let priority = prioritizer(p, r);
prioritized_potential_packages.push(p, (priority, Reverse(p.into_raw() as u32)));
}
while let Some(p) = self.prioritized_potential_packages.pop().map(|(p, _)| p) {
let Some(pa) = self.package_assignments.get(&p) else {
continue;
};
if let Some(r) = pa.assignments_intersection.potential_package_filter() {
return Some((p, r));
}
}
None
}
pub(crate) fn extract_solution(&self) -> impl Iterator<Item = (Id<DP::P>, DP::V)> + '_ {
self.package_assignments
.iter()
.take(self.current_decision_level.0 as usize)
.map(|(&p, pa)| match &pa.assignments_intersection {
AssignmentsIntersection::Decision {
decision_level: _,
version: v,
term: _,
} => (p, v.clone()),
AssignmentsIntersection::Derivations(_) => {
let mut context = String::new();
for (id, assignment) in self
.package_assignments
.iter()
.take(self.current_decision_level.0 as usize)
{
context.push_str(&format!(
" * {:?} {:?}\n",
id, assignment.assignments_intersection
));
}
panic!(
"Derivations in the Decision part. Decision level {}\n{}",
self.current_decision_level.0, context
)
}
})
}
pub(crate) fn backtrack(&mut self, decision_level: DecisionLevel) {
self.current_decision_level = decision_level;
self.package_assignments.retain(|p, pa| {
if pa.smallest_decision_level > decision_level {
false
} else if pa.highest_decision_level <= decision_level {
if pa
.assignments_intersection
.potential_package_filter()
.is_some()
&& self.prioritized_potential_packages.get(p).is_none()
{
self.outdated_priorities.insert(*p);
}
true
} else {
while pa.dated_derivations.last().map(|dd| dd.decision_level) > Some(decision_level)
{
pa.dated_derivations.pop();
}
debug_assert!(!pa.dated_derivations.is_empty());
let last = pa.dated_derivations.last().unwrap();
pa.highest_decision_level = last.decision_level;
pa.assignments_intersection =
AssignmentsIntersection::Derivations(last.accumulated_intersection.clone());
self.prioritized_potential_packages.remove(p);
if pa.assignments_intersection.term().is_positive() {
self.outdated_priorities.insert(*p);
}
true
}
});
self.has_ever_backtracked = true;
}
pub(crate) fn add_package_version_incompatibilities(
&mut self,
package: Id<DP::P>,
version: DP::V,
new_incompatibilities: std::ops::Range<IncompId<DP::P, DP::VS, DP::M>>,
store: &Arena<Incompatibility<DP::P, DP::VS, DP::M>>,
) -> Option<IncompId<DP::P, DP::VS, DP::M>> {
if !self.has_ever_backtracked {
log::info!("add_decision: {package:?} @ {version} without checking dependencies");
self.add_decision(package, version);
return None;
}
let package_term = Term::exact(version.clone());
let relation = |incompat: IncompId<DP::P, DP::VS, DP::M>| {
store[incompat].relation(|p| {
if p == package {
Some(&package_term)
} else {
self.term_intersection_for_package(p)
}
})
};
if let Some(satisfied) = Id::range_to_iter(new_incompatibilities)
.find(|incompat| relation(*incompat) == Relation::Satisfied)
{
log::info!(
"rejecting decision {package:?} @ {version} because its dependencies conflict"
);
Some(satisfied)
} else {
log::info!("adding decision: {package:?} @ {version}");
self.add_decision(package, version);
None
}
}
pub(crate) fn relation(
&self,
incompat: &Incompatibility<DP::P, DP::VS, DP::M>,
) -> Relation<DP::P> {
incompat.relation(|package| self.term_intersection_for_package(package))
}
pub(crate) fn term_intersection_for_package(
&self,
package: Id<DP::P>,
) -> Option<&Term<DP::VS>> {
self.package_assignments
.get(&package)
.map(|pa| pa.assignments_intersection.term())
}
#[allow(clippy::type_complexity)]
pub(crate) fn satisfier_search(
&self,
incompat: &Incompatibility<DP::P, DP::VS, DP::M>,
store: &Arena<Incompatibility<DP::P, DP::VS, DP::M>>,
) -> (Id<DP::P>, SatisfierSearch<DP::P, DP::VS, DP::M>) {
let satisfied_map = Self::find_satisfier(incompat, &self.package_assignments);
let (&satisfier_package, &(satisfier_cause, _, satisfier_decision_level)) = satisfied_map
.iter()
.max_by_key(|(_p, (_, global_index, _))| global_index)
.unwrap();
let previous_satisfier_level = Self::find_previous_satisfier(
incompat,
satisfier_package,
satisfied_map,
&self.package_assignments,
store,
);
let search_result = if previous_satisfier_level >= satisfier_decision_level {
SatisfierSearch::SameDecisionLevels {
satisfier_cause: satisfier_cause.unwrap(),
}
} else {
SatisfierSearch::DifferentDecisionLevels {
previous_satisfier_level,
}
};
(satisfier_package, search_result)
}
#[allow(clippy::type_complexity)]
fn find_satisfier(
incompat: &Incompatibility<DP::P, DP::VS, DP::M>,
package_assignments: &FnvIndexMap<Id<DP::P>, PackageAssignments<DP::P, DP::VS, DP::M>>,
) -> SatisfiedMap<DP::P, DP::VS, DP::M> {
let mut satisfied = SmallMap::Empty;
for (package, incompat_term) in incompat.iter() {
let pa = package_assignments.get(&package).expect("Must exist");
satisfied.insert(package, pa.satisfier(package, &incompat_term.negate()));
}
satisfied
}
#[allow(clippy::type_complexity)]
fn find_previous_satisfier(
incompat: &Incompatibility<DP::P, DP::VS, DP::M>,
satisfier_package: Id<DP::P>,
mut satisfied_map: SatisfiedMap<DP::P, DP::VS, DP::M>,
package_assignments: &FnvIndexMap<Id<DP::P>, PackageAssignments<DP::P, DP::VS, DP::M>>,
store: &Arena<Incompatibility<DP::P, DP::VS, DP::M>>,
) -> DecisionLevel {
let satisfier_pa = package_assignments.get(&satisfier_package).unwrap();
let (satisfier_cause, _gidx, _dl) = satisfied_map.get(&satisfier_package).unwrap();
let accum_term = if let &Some(cause) = satisfier_cause {
store[cause].get(satisfier_package).unwrap().negate()
} else {
match &satisfier_pa.assignments_intersection {
AssignmentsIntersection::Derivations(_) => panic!("must be a decision"),
AssignmentsIntersection::Decision {
decision_level: _,
version: _,
term,
} => term.clone(),
}
};
let incompat_term = incompat
.get(satisfier_package)
.expect("satisfier package not in incompat");
satisfied_map.insert(
satisfier_package,
satisfier_pa.satisfier(
satisfier_package,
&accum_term.intersection(&incompat_term.negate()),
),
);
let (_, &(_, _, decision_level)) = satisfied_map
.iter()
.max_by_key(|(_p, (_, global_index, _))| global_index)
.unwrap();
decision_level.max(DecisionLevel(1))
}
pub(crate) fn current_decision_level(&self) -> DecisionLevel {
self.current_decision_level
}
}
impl<P: Package, VS: VersionSet, M: Eq + Clone + Debug + Display> PackageAssignments<P, VS, M> {
fn satisfier(
&self,
package: Id<P>,
start_term: &Term<VS>,
) -> (Option<IncompId<P, VS, M>>, u32, DecisionLevel) {
let empty = Term::empty();
let idx = self
.dated_derivations
.as_slice()
.partition_point(|dd| !dd.accumulated_intersection.is_disjoint(start_term));
if let Some(dd) = self.dated_derivations.get(idx) {
debug_assert_eq!(dd.accumulated_intersection.intersection(start_term), empty);
return (Some(dd.cause), dd.global_index, dd.decision_level);
}
match &self.assignments_intersection {
AssignmentsIntersection::Decision {
decision_level: global_index,
version: _,
term: _,
} => (None, *global_index, self.highest_decision_level),
AssignmentsIntersection::Derivations(accumulated_intersection) => {
unreachable!(
concat!(
"while processing package {:?}: ",
"accum_term = {} has overlap with incompat_term = {}, ",
"which means the last assignment should have been a decision, ",
"but instead it was a derivation. This shouldn't be possible! ",
"(Maybe your Version ordering is broken?)"
),
package, accumulated_intersection, start_term
)
}
}
}
}
impl<VS: VersionSet> AssignmentsIntersection<VS> {
fn term(&self) -> &Term<VS> {
match self {
Self::Decision {
decision_level: _,
version: _,
term,
} => term,
Self::Derivations(term) => term,
}
}
fn potential_package_filter(&self) -> Option<&VS> {
match self {
Self::Decision { .. } => None,
Self::Derivations(term_intersection) => {
if term_intersection.is_positive() {
Some(term_intersection.unwrap_positive())
} else {
None
}
}
}
}
}