cargo/core/resolver/
conflict_cache.rs

1use std::collections::{BTreeMap, HashMap};
2
3use rustc_hash::{FxHashMap, FxHashSet};
4use tracing::trace;
5
6use super::types::ConflictMap;
7use crate::core::resolver::ResolverContext;
8use crate::core::{Dependency, PackageId};
9
10/// This is a trie for storing a large number of sets designed to
11/// efficiently see if any of the stored sets are a subset of a search set.
12enum ConflictStoreTrie {
13    /// One of the stored sets.
14    Leaf(ConflictMap),
15    /// A map from an element to a subtrie where
16    /// all the sets in the subtrie contains that element.
17    Node(BTreeMap<PackageId, ConflictStoreTrie>),
18}
19
20impl ConflictStoreTrie {
21    /// Finds any known set of conflicts, if any,
22    /// where all elements return some from `is_active` and contain `PackageId` specified.
23    /// If more than one are activated, then it will return
24    /// one that will allow for the most jump-back.
25    fn find(
26        &self,
27        is_active: &impl Fn(PackageId) -> Option<usize>,
28        must_contain: Option<PackageId>,
29        mut max_age: usize,
30    ) -> Option<(&ConflictMap, usize)> {
31        match self {
32            ConflictStoreTrie::Leaf(c) => {
33                if must_contain.is_none() {
34                    Some((c, 0))
35                } else {
36                    // We did not find `must_contain`, so we need to keep looking.
37                    None
38                }
39            }
40            ConflictStoreTrie::Node(m) => {
41                let mut out = None;
42                for (&pid, store) in must_contain
43                    .map(|f| m.range(..=f))
44                    .unwrap_or_else(|| m.range(..))
45                {
46                    // If the key is active, then we need to check all of the corresponding subtrie.
47                    if let Some(age_this) = is_active(pid) {
48                        if age_this >= max_age && must_contain != Some(pid) {
49                            // not worth looking at, it is to old.
50                            continue;
51                        }
52                        if let Some((o, age_o)) =
53                            store.find(is_active, must_contain.filter(|&f| f != pid), max_age)
54                        {
55                            let age = if must_contain == Some(pid) {
56                                // all the results will include `must_contain`
57                                // so the age of must_contain is not relevant to find the best result.
58                                age_o
59                            } else {
60                                std::cmp::max(age_this, age_o)
61                            };
62                            if max_age > age {
63                                // we found one that can jump-back further so replace the out.
64                                out = Some((o, age));
65                                // and don't look at anything older
66                                max_age = age
67                            }
68                        }
69                    }
70                    // Else, if it is not active then there is no way any of the corresponding
71                    // subtrie will be conflicting.
72                }
73                out
74            }
75        }
76    }
77
78    fn insert(&mut self, mut iter: impl Iterator<Item = PackageId>, con: ConflictMap) {
79        if let Some(pid) = iter.next() {
80            if let ConflictStoreTrie::Node(p) = self {
81                p.entry(pid)
82                    .or_insert_with(|| ConflictStoreTrie::Node(BTreeMap::new()))
83                    .insert(iter, con);
84            }
85        // Else, we already have a subset of this in the `ConflictStore`.
86        } else {
87            // We are at the end of the set we are adding, there are three cases for what to do
88            // next:
89            // 1. `self` is an empty dummy Node inserted by `or_insert_with`
90            //      in witch case we should replace it with `Leaf(con)`.
91            // 2. `self` is a `Node` because we previously inserted a superset of
92            //      the thing we are working on (I don't know if this happens in practice)
93            //      but the subset that we are working on will
94            //      always match any time the larger set would have
95            //      in witch case we can replace it with `Leaf(con)`.
96            // 3. `self` is a `Leaf` that is in the same spot in the structure as
97            //      the thing we are working on. So it is equivalent.
98            //      We can replace it with `Leaf(con)`.
99            if cfg!(debug_assertions) {
100                if let ConflictStoreTrie::Leaf(c) = self {
101                    let a: Vec<_> = con.keys().collect();
102                    let b: Vec<_> = c.keys().collect();
103                    assert_eq!(a, b);
104                }
105            }
106            *self = ConflictStoreTrie::Leaf(con)
107        }
108    }
109}
110
111pub(super) struct ConflictCache {
112    // `con_from_dep` is a cache of the reasons for each time we
113    // backtrack. For example after several backtracks we may have:
114    //
115    //  con_from_dep[`foo = "^1.0.2"`] = map!{
116    //      `foo=1.0.1`: map!{`foo=1.0.1`: Semver},
117    //      `foo=1.0.0`: map!{`foo=1.0.0`: Semver},
118    //  };
119    //
120    // This can be read as "we cannot find a candidate for dep `foo = "^1.0.2"`
121    // if either `foo=1.0.1` OR `foo=1.0.0` are activated".
122    //
123    // Another example after several backtracks we may have:
124    //
125    //  con_from_dep[`foo = ">=0.8.2, <=0.9.3"`] = map!{
126    //      `foo=0.8.1`: map!{
127    //          `foo=0.9.4`: map!{`foo=0.8.1`: Semver, `foo=0.9.4`: Semver},
128    //      }
129    //  };
130    //
131    // This can be read as "we cannot find a candidate for dep `foo = ">=0.8.2,
132    // <=0.9.3"` if both `foo=0.8.1` AND `foo=0.9.4` are activated".
133    //
134    // This is used to make sure we don't queue work we know will fail. See the
135    // discussion in https://github.com/rust-lang/cargo/pull/5168 for why this
136    // is so important. The nested HashMaps act as a kind of btree, that lets us
137    // look up which entries are still active without
138    // linearly scanning through the full list.
139    //
140    // Also, as a final note, this map is **not** ever removed from. This remains
141    // as a global cache which we never delete from. Any entry in this map is
142    // unconditionally true regardless of our resolution history of how we got
143    // here.
144    con_from_dep: FxHashMap<Dependency, ConflictStoreTrie>,
145    // `dep_from_pid` is an inverse-index of `con_from_dep`.
146    // For every `PackageId` this lists the `Dependency`s that mention it in `dep_from_pid`.
147    dep_from_pid: FxHashMap<PackageId, FxHashSet<Dependency>>,
148}
149
150impl ConflictCache {
151    pub fn new() -> ConflictCache {
152        ConflictCache {
153            con_from_dep: HashMap::default(),
154            dep_from_pid: HashMap::default(),
155        }
156    }
157    pub fn find(
158        &self,
159        dep: &Dependency,
160        is_active: &impl Fn(PackageId) -> Option<usize>,
161        must_contain: Option<PackageId>,
162        max_age: usize,
163    ) -> Option<&ConflictMap> {
164        self.con_from_dep
165            .get(dep)?
166            .find(is_active, must_contain, max_age)
167            .map(|(c, _)| c)
168    }
169    /// Finds any known set of conflicts, if any,
170    /// which are activated in `cx` and contain `PackageId` specified.
171    /// If more than one are activated, then it will return
172    /// one that will allow for the most jump-back.
173    pub fn find_conflicting(
174        &self,
175        cx: &ResolverContext,
176        dep: &Dependency,
177        must_contain: Option<PackageId>,
178    ) -> Option<&ConflictMap> {
179        let out = self.find(dep, &|id| cx.is_active(id), must_contain, usize::MAX);
180        if cfg!(debug_assertions) {
181            if let Some(c) = &out {
182                assert!(cx.is_conflicting(None, c).is_some());
183                if let Some(f) = must_contain {
184                    assert!(c.contains_key(&f));
185                }
186            }
187        }
188        out
189    }
190    pub fn conflicting(&self, cx: &ResolverContext, dep: &Dependency) -> Option<&ConflictMap> {
191        self.find_conflicting(cx, dep, None)
192    }
193
194    /// Adds to the cache a conflict of the form:
195    /// `dep` is known to be unresolvable if
196    /// all the `PackageId` entries are activated.
197    pub fn insert(&mut self, dep: &Dependency, con: &ConflictMap) {
198        self.con_from_dep
199            .entry(dep.clone())
200            .or_insert_with(|| ConflictStoreTrie::Node(BTreeMap::new()))
201            .insert(con.keys().cloned(), con.clone());
202
203        trace!(
204            "{} = \"{}\" adding a skip {:?}",
205            dep.package_name(),
206            dep.version_req(),
207            con
208        );
209
210        for c in con.keys() {
211            self.dep_from_pid.entry(*c).or_default().insert(dep.clone());
212        }
213    }
214
215    pub fn dependencies_conflicting_with(&self, pid: PackageId) -> Option<&FxHashSet<Dependency>> {
216        self.dep_from_pid.get(&pid)
217    }
218}