| * [Overview](#overview) |
| * [Definitions](#definitions) |
| * [Term](#term) |
| * [Incompatibility](#incompatibility) |
| * [Partial Solution](#partial-solution) |
| * [Derivation Graph](#derivation-graph) |
| * [The Algorithm](#the-algorithm) |
| * [Unit Propagation](#unit-propagation) |
| * [Conflict Resolution](#conflict-resolution) |
| * [Decision Making](#decision-making) |
| * [Error Reporting](#error-reporting) |
| * [Examples](#examples) |
| * [No Conflicts](#no-conflicts) |
| * [Avoiding Conflict During Decision Making](#avoiding-conflict-during-decision-making) |
| * [Performing Conflict Resolution](#performing-conflict-resolution) |
| * [Conflict Resolution With a Partial Satisfier](#conflict-resolution-with-a-partial-satisfier) |
| * [Linear Error Reporting](#linear-error-reporting) |
| * [Branching Error Reporting](#branching-error-reporting) |
| * [Differences From CDCL and Answer Set Solving](#differences-from-cdcl-and-answer-set-solving) |
| * [Version Ranges](#version-ranges) |
| * [Implicit Mutual Exclusivity](#implicit-mutual-exclusivity) |
| * [Lazy Formulas](#lazy-formulas) |
| * [No Unfounded Set Detection](#no-unfounded-set-detection) |
| |
| # Overview |
| |
| Choosing appropriate versions is a core piece of a package manager's |
| functionality, and a tricky one to do well. Many different package managers use |
| many different algorithms, but they often end up taking exponential time for |
| real-world use cases or producing difficult-to-understand output when no |
| solution is found. Pub's version solving algorithm, called **Pubgrub**, solves |
| these issues by adapting state-of-the-art techniques for solving |
| [Boolean satisfiability][] and related difficult search problems. |
| |
| [Boolean satisfiability]: https://en.wikipedia.org/wiki/Boolean_satisfiability_problem |
| |
| Given a universe of package versions with constrained dependencies on one |
| another, one of which is designated as the root, version solving is the problem |
| of finding a set of package versions such that |
| |
| * each version's dependencies are satisfied; |
| * only one version of each package is selected; and |
| * no extra packages are selected–that is, all selected packages are |
| transitively reachable from the root package. |
| |
| This is an [NP-hard][] problem, which means that there's (probably) no algorithm |
| for solving it efficiently in all cases. However, there are approaches that are |
| efficient in enough cases to be useful in practice. Pubgrub is one such |
| algorithm. It's based on the [Conflict-Driven Clause Learning][] algorithm for |
| solving the NP-hard [Boolean satisfiability problem][], and particularly on the |
| version of that algorithm used by the [clasp][] answer set solver as described |
| in the book [*Answer Set Solving in Practice*][book] by Gebser *et al*. |
| |
| [NP-hard]: https://en.wikipedia.org/wiki/NP-hardness |
| [Conflict-Driven Clause Learning]: https://en.wikipedia.org/wiki/Conflict-Driven_Clause_Learning |
| [Boolean satisfiability problem]: https://en.wikipedia.org/wiki/Boolean_satisfiability_problem |
| [clasp]: https://potassco.org/clasp/ |
| [book]: https://potassco.org/book/ |
| |
| At a high level, Pubgrub works like many other search algorithms. Its core loop |
| involves speculatively choosing package versions that match outstanding |
| dependencies. Eventually one of two things happens: |
| |
| * All dependencies are satisfied, in which case a solution has been found and |
| Pubgrub has succeeded. |
| |
| * It finds a dependency that can't be satisfied, in which case the current set |
| of versions are incompatible and the solver needs to backtrack. |
| |
| When a conflict is found, Pubgrub backtracks to the package that caused the |
| conflict and chooses a different version. However, unlike many search |
| algorithms, it also records the root cause of that conflict. This is the |
| "conflict-driven clause learning" that lends CDCL its name. |
| |
| Recording the root causes of conflicts allows Pubgrub to avoid retreading dead |
| ends in the search space when the context has changed. This makes the solver |
| substantially more efficient than a naïve search algorithm when there are |
| consistent causes for each conflict. If no solution exists, clause learning also |
| allows Pubgrub to explain to the user the root causes of the conflicts that |
| prevented a solution from being found. |
| |
| # Definitions |
| |
| ## Term |
| |
| The fundamental unit on which Pubgrub operates is a `Term`, which represents a |
| statement about a package that may be true or false for a given selection of |
| package versions. For example, `foo ^1.0.0` is a term that's true if `foo 1.2.3` |
| is selected and false if `foo 2.3.4` is selected. Conversely, `not foo ^1.0.0` |
| is false if `foo 1.2.3` is selected and true if `foo 2.3.4` is selected or if |
| no version of `foo` is selected at all. |
| |
| We say that a set of terms `S` "satisfies" a term `t` if `t` must be true |
| whenever every term in `S` is true. Conversely, `S` "contradicts" `t` if `t` |
| must be false whenever every term in `S` is true. If neither of these is true, |
| we say that `S` is "inconclusive" for `t`. As a shorthand, we say that a term |
| `v` satisfies or contradicts `t` if `{v}` satisfies or contradicts it. For |
| example: |
| |
| * `{foo >=1.0.0, foo <2.0.0}` satisfies `foo ^1.0.0`, |
| * `foo ^1.5.0` contradicts `not foo ^1.0.0`, |
| * and `foo ^1.0.0` is inconclusive for `foo ^1.5.0`. |
| |
| Terms can be viewed as denoting sets of allowed versions, with negative terms |
| denoting the complement of the corresponding positive term. Set relations and |
| operations can be defined accordingly. For example: |
| |
| * `foo ^1.0.0 ∪ foo ^2.0.0` is `foo >=1.0.0 <3.0.0`. |
| * `foo >=1.0.0 ∩ not foo >=2.0.0` is `foo ^1.0.0`. |
| * `foo ^1.0.0 \ foo ^1.5.0` is `foo >=1.0.0 <1.5.0`. |
| |
| > **Note:** we use the [ISO 31-11 standard notation][ISO 31-11] for set |
| > operations. |
| |
| [ISO 31-11]: https://en.wikipedia.org/wiki/ISO_31-11#Sets |
| |
| This turns out to be useful for computing satisfaction and contradiction. Given |
| a term `t` and a set of terms `S`, we have the following identities: |
| |
| * `S` satisfies `t` if and only if `⋂S ⊆ t`. |
| * `S` contradicts `t` if and only if `⋂S` is disjoint with `t`. |
| |
| ## Incompatibility |
| |
| An incompatibility is a set of terms that are not *all* allowed to be true. A |
| given set of package versions can only be valid according to an incompatibility |
| if at least one of the incompatibility's terms is false for that solution. For |
| example, the incompatibility `{foo ^1.0.0, bar ^2.0.0}` indicates that |
| `foo ^1.0.0` is incompatible with `bar ^2.0.0`, so a solution that contains |
| `foo 1.1.0` and `bar 2.0.2` would be invalid. Incompatibilities are |
| *context-independent*, meaning that their terms are mutually incompatible |
| regardless of which versions are selected at any given point in time. |
| |
| There are two sources of incompatibilities: |
| |
| 1. An incompatibility may come from an external fact about packages—for example, |
| "`foo ^1.0.0` depends on `bar ^2.0.0`" is represented as the incompatibility |
| `{foo ^1.0.0, not bar ^2.0.0}`, while "`foo <1.3.0` has an incompatible SDK |
| constraint" is represented by the incompatibility `{not foo <1.3.0}`. These |
| are known as "external incompatibilities", and they track the external facts |
| that caused them to be generated. |
| |
| 2. An incompatibility may also be derived from two existing incompatibilities |
| during [conflict resolution](#conflict-resolution). These are known as |
| "derived incompatibilities", and we call the prior incompatibilities from |
| which they were derived their "causes". Derived incompatibilities are used to |
| avoid exploring the same dead-end portion of the state space over and over. |
| |
| Incompatibilities are normalized so that at most one term refers to any given |
| package name. For example, `{foo >=1.0.0, foo <2.0.0}` is normalized to |
| `{foo ^1.0.0}`. Derived incompatibilities with more than one term are also |
| normalized to remove positive terms referring to the root package, since these |
| terms will always be satisfied. |
| |
| We say that a set of terms `S` satisfies an incompatibility `I` if `S` satisfies |
| every term in `I`. We say that `S` contradicts `I` if `S` contradicts at least |
| one term in `I`. If `S` satisfies all but one of `I`'s terms and is inconclusive |
| for the remaining term, we say `S` "almost satisfies" `I` and we call the |
| remaining term the "unsatisfied term". |
| |
| ## Partial Solution |
| |
| A partial solution is an ordered list of terms known as "assignments". It |
| represents Pubgrub's current best guess about what's true for the eventual set |
| of package versions that will comprise the total solution. The solver |
| continuously modifies its partial solution as it progresses through the search |
| space. |
| |
| There are two categories of assignments. **Decisions** are assignments that |
| select individual package versions (pub's `PackageId`s). They represent guesses |
| Pubgrub has made about versions that might work. **Derivations** are assignments |
| that usually select version ranges (pub's `PackageRange`s). They represent terms |
| that must be true given the previous assignments in the partial solution and any |
| incompatibilities we know about. Each derivation keeps track of its "cause", the |
| incompatibility that caused it to be derived. The process of finding new |
| derivations is known as [unit propagation](#unit-propagation). |
| |
| Each assignment has an associated "decision level", a non-negative integer |
| indicating the number of decisions at or before it in the partial solution other |
| than the root package. This is used to determine how far back to look for root |
| causes during [conflict resolution](#conflict-resolution), and how far back to |
| jump when a conflict is found. |
| |
| If a partial solution has, for every positive derivation, a corresponding |
| decision that satisfies that assignment, it's a total solution and version |
| solving has succeeded. |
| |
| ## Derivation Graph |
| |
| A derivation graph is a directed acyclic binary graph whose vertices are |
| incompatibilities, with edges to each derived incompatibility from both of its |
| causes. This means that all internal vertices are derived incompatibilities, and |
| all leaf vertices are external incompatibilities. The derivation graph *for an |
| incompatibility* is the graph that contains that incompatibility's causes, their |
| causes, and so on transitively. We refer to that incompatibility as the "root" |
| of the derivation graph. |
| |
| > **Note:** if you're unfamiliar with graph theory, check out the |
| > [Wikipedia page][graphs] on the subject. If you don't know a specific bit of |
| > terminology, check out [this glossary][graph terminology]. |
| |
| [graphs]: https://en.wikipedia.org/wiki/Graph_(discrete_mathematics) |
| [graph terminology]: https://en.wikipedia.org/wiki/Glossary_of_graph_theory_terms |
| |
| A derivation graph represents a proof that the terms in its root incompatibility |
| are in fact incompatible. Because all derived incompatibilities track their |
| causes, we can find a derivation graph for any of them and thereby prove it. In |
| particular, when Pubgrub determines that no solution can be found, it uses the |
| derivation graph for the incompatibility `{root any}` to |
| [explain to the user](#error-reporting) why no versions of the root package can |
| be selected and thus why version solving failed. |
| |
| Here's an example of a derivation graph: |
| |
| ``` |
| ┌1───────────────────────────┐ ┌2───────────────────────────┐ |
| │{foo ^1.0.0, not bar ^2.0.0}│ │{bar ^2.0.0, not baz ^3.0.0}│ |
| └─────────────┬──────────────┘ └──────────────┬─────────────┘ |
| │ ┌────────────────────────┘ |
| ▼ ▼ |
| ┌3────────────┴──────┴───────┐ ┌4───────────────────────────┐ |
| │{foo ^1.0.0, not baz ^3.0.0}│ │{root 1.0.0, not foo ^1.0.0}│ |
| └─────────────┬──────────────┘ └──────────────┬─────────────┘ |
| │ ┌────────────────────────┘ |
| ▼ ▼ |
| ┌5───────────┴──────┴──────┐ ┌6───────────────────────────┐ |
| │{root any, not baz ^3.0.0}│ │{root 1.0.0, not baz ^1.0.0}│ |
| └────────────┬─────────────┘ └──────────────┬─────────────┘ |
| │ ┌───────────────────────────┘ |
| ▼ ▼ |
| ┌7────┴───┴──┐ |
| │{root 1.0.0}│ |
| └────────────┘ |
| ``` |
| |
| This represents the following proof (with numbers corresponding to the |
| incompatibilities above): |
| |
| 1. Because `foo ^1.0.0` depends on `bar ^2.0.0` |
| 2. and `bar ^2.0.0` depends on `baz ^3.0.0`, |
| 3. `foo ^1.0.0` requires `baz ^3.0.0`. |
| 4. And, because `root` depends on `foo ^1.0.0`, |
| 5. `root` requires `baz ^3.0.0`. |
| 6. So, because `root` depends on `baz ^1.0.0`, |
| 7. `root` isn't valid and version solving has failed. |
| |
| # The Algorithm |
| |
| The core of Pubgrub works as follows: |
| |
| * Begin by adding an incompatibility indicating that the current version of the |
| root package must be selected (for example, `{not root 1.0.0}`). Note that |
| although there's only one version of the root package, this is just an |
| incompatibility, not an assignment. |
| |
| * Let `next` be the name of the root package. |
| |
| * In a loop: |
| |
| * Perform [unit propagation](#unit-propagation) on `next` to find new |
| derivations. |
| |
| * If this causes an incompatibility to be satisfied by the partial solution, |
| we have a conflict. Unit propagation will try to |
| [resolve the conflict](#conflict-resolution). If this fails, version |
| solving has failed and [an error should be reported](#error-reporting). |
| |
| * Once there are no more derivations to be found, |
| [make a decision](#decision-making) and set `next` to the package name |
| returned by the decision-making process. Note that the first decision will |
| always select the single available version of the root package. |
| |
| * Decision making may determine that there's no more work to do, in which |
| case version solving is done and the partial solution represents a total |
| solution. |
| |
| ## Unit Propagation |
| |
| Unit propagation combines the partial solution with the known incompatibilities |
| to derive new assignments. Given an incompatibility, if the partial solution is |
| inconclusive for one term *t* in that incompatibility and satisfies the rest, |
| then *t* must be contradicted in order for the incompatibility to be |
| contradicted. Thus, we add *not t* to the partial solution as a derivation. |
| |
| When looking for incompatibilities that have a single inconclusive term, we may |
| also find an incompatibility that's satisfied by the partial solution. If we do, |
| we know the partial solution can't produce a valid solution, so we go to |
| [conflict resolution](#conflict-resolution) to try to resolve the conflict. This |
| either throws an error, or jumps back in the partial solution and returns a new |
| incompatibility that represents the root cause of the conflict which we use to |
| continue unit propagation. |
| |
| While we could iterate over every incompatibility over and over until we can't |
| find any more derivations, this isn't efficient when many of them represent |
| dependencies of packages that are currently irrelevant. Instead, we index them |
| by the names of the packages they refer to and only iterate over those that |
| refer to the most recently-decided package or new derivations that have been |
| added during the current propagation session. |
| |
| The unit propagation algorithm takes a package name and works as follows: |
| |
| * Let `changed` be a set containing the input package name. |
| |
| * While `changed` isn't empty: |
| |
| * Remove an element from `changed`. Call it `package`. |
| |
| * For each `incompatibility` that refers to `package` from newest to oldest |
| (since conflict resolution tends to produce more general incompatibilities |
| later on): |
| |
| * If `incompatibility` is satisfied by the partial solution: |
| |
| * Run [conflict resolution](#conflict-resolution) with `incompatibility`. |
| If this succeeds, it returns an incompatibility that's guaranteed to be |
| almost satisfied by the partial solution. Call this incompatibility's |
| unsatisfied term `term`. |
| * Add `not term` to the partial solution with `incompatibility` as its |
| cause. |
| * Replace `changed` with a set containing only `term`'s package name. |
| |
| * Otherwise, if the partial solution almost satisfies `incompatibility`: |
| |
| * Call `incompatibility`'s unsatisfied term `term`. |
| * Add `not term` to the partial solution with `incompatibility` as its |
| cause. |
| * Add `term`'s package name to `changed`. |
| |
| ## Conflict Resolution |
| |
| When an incompatibility is satisfied by the partial solution, that indicates |
| that the partial solution's decisions aren't a subset of any total solution. The |
| process of returning the partial solution to a state where the incompatibility |
| is no longer satisfied is known as conflict resolution. |
| |
| Following CDCL and Answer Set Solving, Pubgrub's conflict resolution includes |
| determining the root cause of a conflict and using that to avoid satisfying the |
| same incompatibility for the same reason in the future. This makes Pubgrub |
| substantially more efficient in real-world cases, since it avoids re-exploring |
| parts of the solution space that are known not to work. |
| |
| The core of conflict resolution is based on the rule of [resolution][]: given |
| `a or b` and `not a or c`, you can derive `b or c`. This means that given |
| incompatibilities `{t, q}` and `{not t, r}`, we can derive the incompatibility |
| `{q, r}`—if this is satisfied, one of the existing incompatibilities will also |
| be satisfied. |
| |
| [Resolution]: https://en.wikipedia.org/wiki/Resolution_(logic) |
| |
| In fact, we can generalize this: given *any* incompatibilities `{t1, q}` and |
| `{t2, r}`, we can derive `{q, r, t1 ∪ t2}`, since either `t1` or `t2` is true in |
| every solution in which `t1 ∪ t2` is true. This reduces to `{q, r}` in any case |
| where `not t2 ⊆ t1` (that is, where `not t2` satisfies `t1`), including the case |
| above where `t1 = t` and `t2 = not t`. |
| |
| We use this to describe the notion of a "prior cause" of a conflicting |
| incompatibility—another incompatibility that's one step closer to the root |
| cause. We find a prior cause by finding the earliest assignment that fully |
| satisfies the conflicting incompatibility, then applying the generalized |
| resolution above to that assignment's cause and the conflicting incompatibility. |
| This produces a new incompatibility which is our prior cause. |
| |
| We then find the root cause by applying that procedure repeatedly until the |
| satisfying assignment is either a decision or the only assignment at its |
| decision level that's relevant to the conflict. In the former case, there is no |
| underlying cause; in the latter, we've moved far enough back that we can |
| backtrack the partial solution and be guaranteed to derive new assignments. |
| |
| Putting this all together, we get a conflict resolution algorithm. It takes as |
| input an `incompatibility` that's satisfied by the partial solution, and returns |
| another `incompatibility` that represents the root cause of the conflict. As a |
| side effect, it backtracks the partial solution to get rid of the incompatible |
| decisions. It works as follows: |
| |
| * In a loop: |
| |
| * If `incompatibility` contains no terms, or if it contains a single positive |
| term that refers to the root package version, that indicates that the root |
| package can't be selected and thus that version solving has failed. |
| [Report an error](#error-reporting) with `incompatibility` as the root |
| incompatibility. |
| |
| * Find the earliest assignment in the partial solution such that |
| `incompatibility` is satisfied by the partial solution up to and including |
| that assignment. Call this `satisfier`, and call the term in `incompatibility` |
| that refers to the same package `term`. |
| |
| * Find the earliest assignment in the partial solution *before* `satisfier` such |
| that `incompatibility` is satisfied by the partial solution up to and |
| including that assignment plus `satisfier`. Call this `previousSatisfier`. |
| |
| * Note: `satisfier` may not satisfy `term` on its own. For example, if term |
| is `foo >=1.0.0 <2.0.0`, it may be satisfied by |
| `{foo >=1.0.0, foo <2.0.0}` but not by either assignment individually. If |
| this is the case, `previousSatisfier` may refer to the same package as |
| `satisfier`. |
| |
| * Let `previousSatisfierLevel` be `previousSatisfier`'s decision level, or |
| decision level 1 if there is no `previousSatisfier`. |
| |
| * Note: decision level 1 is the level where the root package was selected. |
| It's safe to go back to decision level 0, but stopping at 1 tends to |
| produce better error messages, because references to the root package end |
| up closer to the final conclusion that no solution exists. |
| |
| * If `satisfier` is a decision or if `previousSatisfierLevel` is different |
| than `satisfier`'s decision level: |
| |
| * If `incompatibility` is different than the original input, add it to the |
| solver's incompatibility set. (If the conflicting incompatibility was |
| added lazily during [decision making](#decision-making), it may not have a |
| distinct root cause.) |
| |
| * Backtrack by removing all assignments whose decision level is greater than |
| `previousSatisfierLevel` from the partial solution. |
| |
| * Return `incompatibility`. |
| |
| * Otherwise, let `priorCause` be the union of the terms in incompatibility and |
| the terms in `satisfier`'s cause, minus the terms referring to `satisfier`'s |
| package. |
| |
| * Note: this corresponds to the derived incompatibility `{q, r}` in the |
| example above. |
| |
| * If `satisfier` doesn't satisfy `term`, add `not (satisfier \ term)` to |
| `priorCause`. |
| |
| * Note: `not (satisfier \ term)` corresponds to `t1 ∪ t2` above with |
| `term = t1` and `satisfier = not t2`, by the identity `(Sᶜ \ T)ᶜ = S ∪ T`. |
| |
| * Set `incompatibility` to `priorCause`. |
| |
| ## Decision Making |
| |
| Decision making is the process of speculatively choosing an individual package |
| version in hope that it will be part of a total solution and ensuring that that |
| package's dependencies are properly handled. There's some flexibility in exactly |
| which package version is selected; any version that meets the following criteria |
| is valid: |
| |
| * The partial solution contains a positive derivation for that package. |
| * The partial solution *doesn't* contain a decision for that package. |
| * The package version matches all assignments in the partial solution. |
| |
| Pub chooses the latest matching version of the package with the fewest versions |
| that match the outstanding constraint. This tends to find conflicts earlier if |
| any exist, since these packages will run out of versions to try more quickly. |
| But there's likely room for improvement in these heuristics. |
| |
| Part of the process of decision making also involves converting packages' |
| dependencies to incompatibilities. This is done lazily when each package version |
| is first chosen to avoid flooding the solver with incompatibilities that are |
| likely to be irrelevant. |
| |
| Pubgrub collapses identical dependencies from adjacent package versions into |
| individual incompatibilities. This substantially reduces the total number of |
| incompatibilities and makes it much easier for Pubgrub to reason about multiple |
| versions of packages at once. For example, rather than representing |
| `foo 1.0.0 depends on bar ^1.0.0` and `foo 1.1.0 depends on bar ^1.0.0` as two |
| separate incompatibilities, they're collapsed together into the single |
| incompatibility `{foo ^1.0.0, not bar ^1.0.0}`. |
| |
| The version ranges of the dependers (`foo` in the example above) always have an |
| inclusive lower bound of the first version that has the dependency, and an |
| exclusive upper bound of the first package that *doesn't* have the dependency. |
| if the last published version of the package has the dependency, the upper bound |
| is omitted (as in `foo >=1.0.0`); similarly, if the first published version of |
| the package has the dependency, the lower bound is omitted (as in `foo <2.0.0`). |
| Expanding the version range in this way makes it more closely match the format |
| users tend to use when authoring dependencies, which makes it easier for Pubgrub |
| to reason efficiently about the relationship between dependers and the packages |
| they depend on. |
| |
| If a package version can't be selected—for example, because it's incompatible |
| with the current version of the underlying programming language—we avoid adding |
| its dependencies at all. Instead, we just add an incompatibility indicating that |
| it (as well as any adjacent versions that are also incompatible) should never be |
| selected. |
| |
| For more detail on how adjacent package versions' dependencies are combined and |
| converted to incompatibilities, see `lib/src/solver/package_lister.dart`. |
| |
| The decision making algorithm works as follows: |
| |
| * Let `package` be a package with a positive derivation but no decision in the |
| partial solution, and let `term` be the intersection of all assignments in the |
| partial solution referring to that package. |
| |
| * Let `version` be a version of `package` that matches `term`. |
| |
| * If there is no such `version`, add an incompatibility `{term}` to the |
| incompatibility set and return `package`'s name. This tells Pubgrub to avoid |
| this range of versions in the future. |
| |
| * Add each `incompatibility` from `version`'s dependencies to the |
| incompatibility set if it's not already there. |
| |
| * Add `version` to the partial solution as a decision, unless this would produce |
| a conflict in any of the new incompatibilities. |
| |
| * Return `package`'s name. |
| |
| ### Error Reporting |
| |
| When version solving has failed, it's important to explain to the user what went |
| wrong so that they can figure out how to fix it. But version solving is |
| complicated—for the same reason that it's difficult for a computer to quickly |
| determine that version solving will fail, it's difficult to straightforwardly |
| explain to the user why it *did* fail. |
| |
| Fortunately, Pubgrub's structure makes it possible to explain even the most |
| tangled failures. This is due once again to its root-cause tracking: because the |
| algorithm derives new incompatibilities every time it encounters a conflict, it |
| naturally generates a chain of derivations that ultimately derives the fact that |
| no solution exists. |
| |
| When [conflict resolution](#conflict-resolution) fails, it produces an |
| incompatibility with a single positive term: the root package. This |
| incompatibility indicates that the root package isn't part of any solution, and |
| thus that no solution exists and version solving has failed. We use the |
| derivation graph for this incompatibility to generate a human-readable |
| explanation of why version solving failed. |
| |
| Most commonly, derivation graphs look like the example |
| [above](#derivation-graph): a linear chain of derived incompatibilities with one |
| external and one derived cause. These derivations can be explained fairly |
| straightforwardly by just describing each external incompatibility followed by |
| the next derived incompatibility. The only nuance is that, in practice, this |
| tends to end up a little verbose. You can skip every other derived |
| incompatibility without losing clarity. For example, instead of |
| |
| > ... And, because `root` depends on `foo ^1.0.0`, `root` requires `baz ^3.0.0`. |
| > So, because `root` depends on `baz ^1.0.0`, `root` isn't valid and version |
| > solving has failed. |
| |
| you would emit: |
| |
| > ... So, because `root` depends on both `foo ^1.0.0` and `baz ^3.0.0`, `root` |
| > isn't valid and version solving has failed. |
| |
| However, it's possible for derivation graphs to be more complex. A derived |
| incompatibility may be caused by multiple incompatibilities that are also |
| derived: |
| |
| ``` |
| ┌───┐ ┌───┐ ┌───┐ ┌───┐ |
| │ │ │ │ │ │ │ │ |
| └─┬─┘ └─┬─┘ └─┬─┘ └─┬─┘ |
| └▶┐ ┌◀┘ └▶┐ ┌◀┘ |
| ┌┴─┴┐ ┌┴─┴┐ |
| │ │ │ │ |
| └─┬─┘ └─┬─┘ |
| └──▶─┐ ┌─◀──┘ |
| ┌┴─┴┐ |
| │ │ |
| └───┘ |
| ``` |
| |
| The same incompatibility may even cause multiple derived incompatibilities: |
| |
| ``` |
| ┌───┐ ┌───┐ |
| │ │ │ │ |
| └─┬─┘ └─┬─┘ |
| └▶┐ ┌◀┘ |
| ┌───┐ ┌┴─┴┐ ┌───┐ |
| │ │ │ │ │ │ |
| └─┬─┘ └┬─┬┘ └─┬─┘ |
| └▶┐ ┌◀┘ └▶┐ ┌◀┘ |
| ┌┴─┴┐ ┌┴─┴┐ |
| │ │ │ │ |
| └─┬─┘ └─┬─┘ |
| └─▶┐ ┌◀─┘ |
| ┌┴─┴┐ |
| │ │ |
| └───┘ |
| ``` |
| |
| In these cases, a naïvely linear explanation won't be clear. We need to refer to |
| previous derivations that may not be physically nearby. We use line numbers to |
| do this, but we only number incompatibilities that we *know* will need to be |
| referred to later on. In the simple linear case, we don't include line numbers |
| at all. |
| |
| Before running the error reporting algorithm proper, walk the derivation graph |
| and record how many outgoing edges each derived incompatibility has–that is, how |
| many different incompatibilities it causes. |
| |
| The error reporting algorithm takes as input a derived `incompatibility` and |
| writes lines of output (which may have associated numbers). Each line describes |
| a single derived incompatibility and indicates why it's true. It works as |
| follows: |
| |
| 1. If `incompatibility` is caused by two other derived incompatibilities: |
| |
| 1. If both causes already have line numbers: |
| |
| * Write "Because `cause1` (`cause1.line`) and `cause2` (`cause2.line`), |
| `incompatibility`." |
| |
| 2. Otherwise, if only one cause has a line number: |
| |
| * Recursively run error reporting on the cause without a line number. |
| |
| * Call the cause with the line number `cause`. |
| |
| * Write "And because `cause` (`cause.line`), `incompatibility`." |
| |
| 3. Otherwise (when neither has a line number): |
| |
| 1. If at least one cause's incompatibility is caused by two external |
| incompatibilities: |
| |
| * Call this cause `simple` and the other cause `complex`. The |
| `simple` cause can be described in a single line, which is short |
| enough that we don't need to use a line number to refer back to |
| `complex`. |
| |
| * Recursively run error reporting on `complex`. |
| |
| * Recursively run error reporting on `simple`. |
| |
| * Write "Thus, `incompatibility`." |
| |
| 2. Otherwise: |
| |
| * Recursively run error reporting on the first cause, and give the |
| final line a line number if it doesn't have one already. Set this |
| as the first cause's line number. |
| |
| * Write a blank line. This helps visually indicate that we're |
| starting a new line of derivation. |
| |
| * Recursively run error reporting on the second cause, and add a line |
| number to the final line. Associate this line number with the first |
| cause. |
| |
| * Write "And because `cause1` (`cause1.line`), `incompatibility`." |
| |
| 2. Otherwise, if only one of `incompatibility`'s causes is another derived |
| incompatibility: |
| |
| * Call the derived cause `derived` and the external cause `external`. |
| |
| 1. If `derived` already has a line number: |
| |
| * Write "Because `external` and `derived` (`derived.line`), |
| `incompatibility`." |
| |
| 2. Otherwise, if `derived` is itself caused by exactly one derived |
| incompatibility and that incompatibility doesn't have a line number: |
| |
| * Call `derived`'s derived cause `priorDerived` and its external cause |
| `priorExternal`. |
| |
| * Recursively run error reporting on `priorDerived`. |
| |
| * Write "And because `priorExternal` and `external`, |
| `incompatibility`." |
| |
| 3. Otherwise: |
| |
| * Recursively run error reporting on `derived`. |
| |
| * Write "And because `external`, `incompatibility`." |
| |
| 3. Otherwise (when both of `incompatibility`'s causes are external |
| incompatibilities): |
| |
| * Write "Because `cause1` and `cause2`, `incompatibility`." |
| |
| * Finally, if `incompatibility` causes two or more incompatibilities, give the |
| line that was just written a line number. Set this as `incompatibility`'s line |
| number. |
| |
| Note that the text in the "Write" lines above is meant as a suggestion rather |
| than a prescription. It's up to each implementation to determine the best way to |
| convert each incompatibility to a human-readable string representation in a way |
| that makes sense for that package manager's particular domain. |
| |
| # Examples |
| |
| ## No Conflicts |
| |
| First, let's look at a simple case where no actual conflicts occur to get a |
| sense of how unit propagation and decision making operate. Given the following |
| packages: |
| |
| * `root 1.0.0` depends on `foo ^1.0.0`. |
| * `foo 1.0.0` depends on `bar ^1.0.0`. |
| * `bar 1.0.0` and `2.0.0` have no dependencies. |
| |
| Pubgrub goes through the following steps. The table below shows each step in the |
| algorithm where the state changes, either by adding an assignment to the partial |
| solution or by adding an incompatibility to the incompatibility set. |
| |
| | Step | Value | Type | Where it was added | Cause | Decision level | |
| | ---- | ----- | ---- | ------------------ | ----- | -------------- | |
| | 1 | `root 1.0.0` | decision | top level | | 0 | |
| | 2 | `{root 1.0.0, not foo ^1.0.0}` | incompatibility | top level | | | |
| | 3 | `foo ^1.0.0` | derivation | unit propagation | step 2 | 0 | |
| | 4 | `{foo any, not bar ^1.0.0}` | incompatibility | decision making | | | |
| | 5 | `foo 1.0.0` | decision | decision making | | 1 | |
| | 6 | `bar ^1.0.0` | derivation | unit propagation | step 4 | 1 | |
| | 7 | `bar 1.0.0` | decision | decision making | | 2 | |
| |
| In steps 1 and 2, Pubgrub adds the information about the root package. This |
| gives it a place to start its derivations. It then moves to unit propagation in |
| step 3, where it sees that `root 1.0.0` is selected, which means that the |
| incompatibility `{root 1.0.0, not foo ^1.0.0}` is almost satisfied. It adds the |
| inverse of the unsatisfied term, `foo ^1.0.0`, to the partial solution as a |
| derivation. |
| |
| Note in step 7 that Pubgrub chooses `bar 1.0.0` rather than `bar 2.0.0`. This is |
| because it knows that the partial solution contains `bar ^1.0.0`, which |
| `bar 2.0.0` not compatible with. |
| |
| Once the algorithm is done, we look at the decisions to see which package |
| versions are selected: `root 1.0.0`, `foo 1.0.0`, and `bar 1.0.0`. |
| |
| ## Avoiding Conflict During Decision Making |
| |
| In this example, decision making examines a package version that would cause a |
| conflict and chooses not to select it. Given the following packages: |
| |
| * `root 1.0.0` depends on `foo ^1.0.0` and `bar ^1.0.0`. |
| * `foo 1.1.0` depends on `bar ^2.0.0`. |
| * `foo 1.0.0` has no dependencies. |
| * `bar 1.0.0`, `1.1.0`, and `2.0.0` have no dependencies. |
| |
| Pubgrub goes through the following steps: |
| |
| | Step | Value | Type | Where it was added | Cause | Decision level | |
| | ---- | ----- | ---- | ------------------ | ----- | -------------- | |
| | 1 | `root 1.0.0` | decision | top level | | 0 | |
| | 2 | `{root 1.0.0, not foo ^1.0.0}` | incompatibility | top level | | | |
| | 3 | `{root 1.0.0, not bar ^1.0.0}` | incompatibility | top level | | | |
| | 4 | `foo ^1.0.0` | derivation | unit propagation | step 2 | 0 | |
| | 5 | `bar ^1.0.0` | derivation | unit propagation | step 3 | 0 | |
| | 6 | `{foo >=1.1.0, not bar ^2.0.0}` | incompatibility | decision making | | | |
| | 7 | `not foo >=1.1.0` | derivation | unit propagation | step 6 | 0 | |
| | 8 | `foo 1.0.0` | decision | decision making | | 1 | |
| | 9 | `bar 1.1.0` | decision | decision making | | 2 | |
| |
| In step 6, the decision making process considers `foo 1.1.0` by adding its |
| dependency as the incompatibility `{foo >=1.1.0, not bar ^2.0.0}`. However, if |
| `foo 1.1.0` were selected, this incompatibility would be satisfied: `foo 1.1.0` |
| satisfies `foo >=1.1.0`, and `bar ^1.0.0` from step 5 satisfies |
| `not bar ^2.0.0`. So decision making ends without selecting a version, and unit |
| propagation is run again. |
| |
| Unit propagation determines that the new incompatibility, |
| `{foo >=1.1.0, not bar ^2.0.0}`, is almost satisfied (again because `bar ^1.0.0` |
| satisfies `not bar ^2.0.0`). Thus it's able to deduce `not foo >=1.1.0` in step |
| 7, which lets the next iteration of decision making choose `foo 1.0.0` which is |
| compatible with `root`'s constraint on `bar`. |
| |
| ## Performing Conflict Resolution |
| |
| This example shows full conflict resolution in action. Given the following |
| packages: |
| |
| * `root 1.0.0` depends on `foo >=1.0.0`. |
| * `foo 2.0.0` depends on `bar ^1.0.0`. |
| * `foo 1.0.0` has no dependencies. |
| * `bar 1.0.0` depends on `foo ^1.0.0`. |
| |
| Pubgrub goes through the following steps: |
| |
| | Step | Value | Type | Where it was added | Cause | Decision level | |
| | ---- | ----- | ---- | ------------------ | ----- | -------------- | |
| | 1 | `root 1.0.0` | decision | top level | | 0 | |
| | 2 | `{root 1.0.0, not foo >=1.0.0}` | incompatibility | top level | | | |
| | 3 | `foo >=1.0.0` | derivation | unit propagation | step 2 | 0 | |
| | 4 | `{foo >=2.0.0, not bar ^1.0.0}` | incompatibility | decision making | | | |
| | 5 | `foo 2.0.0` | decision | decision making | | 1 | |
| | 6 | `bar ^1.0.0` | derivation | unit propagation | step 4 | 1 | |
| | 7 | `{bar any, not foo ^1.0.0}` | incompatibility | decision making | | | |
| |
| The incompatibility added at step 7 is satisfied by the partial assignment: `bar |
| any` is satisfied by `bar ^1.0.0` from step 6, and `not foo ^1.0.0` is satisfied |
| by `foo 2.0.0` from step 5. This causes Pubgrub to enter conflict resolution, |
| where it iteratively works towards the root cause of the conflict: |
| |
| | Step | Incompatibility | Term | Satisfier | Satisfier Cause | Previous Satisfier | |
| | ---- | --------------- | ---- | --------- | --------------- | ------------------ | |
| | 8 | `{bar any, not foo ^1.0.0}` | `bar any` | `bar ^1.0.0` from step 6 | `{foo >=2.0.0, not bar ^1.0.0}` | `foo 2.0.0` from step 5 | |
| | 9 | `{foo >=2.0.0}` | `foo >=1.0.0` | `foo 2.0.0` from step 5 | | | |
| |
| In step 9, we merge the two incompatibilities `{bar any, not foo ^1.0.0}` and |
| `{foo >=2.0.0, not bar ^1.0.0}` as described in |
| [conflict resolution](#conflict-resolution), to produce |
| `{not foo ^1.0.0, foo >=2.0.0, bar any ∪ not bar ^1.0.0}`. Since |
| `not not bar ^1.0.0 = bar ^1.0.0` satisfies `bar any`, this simplifies to |
| `{not foo ^1.0.0, foo >=2.0.0}`. We can then take the intersection of the two |
| `foo` terms to get `{foo >=2.0.0}`. |
| |
| Now Pubgrub has learned that, no matter which other package versions are |
| selected, `foo 2.0.0` is never going to be a valid choice because of its |
| dependency on `bar`. Because there's no previous satisfier in step 9, it |
| backtracks all the way to level 0 and continues the main loop with the new |
| incompatibility: |
| |
| | Step | Value | Type | Where it was added | Cause | Decision level | |
| | ---- | ----- | ---- | ------------------ | ----- | -------------- | |
| | 10 | `{foo >=2.0.0}` | incompatibility | conflict resolution | | | |
| | 11 | `not foo >=2.0.0` | derivation | unit propagation | step 10 | 0 | |
| | 12 | `foo 1.0.0` | decision | decision making | | 1 | |
| |
| Given this new incompatibility, Pubgrub knows to avoid selecting `foo 2.0.0` and |
| selects the correction version, `foo 1.0.0`, instead. Because it backtracked, |
| all decisions previously made at decision levels higher than 0 are discarded, |
| and the solution is `root 1.0.0` and `foo 1.0.0`. |
| |
| ## Conflict Resolution With a Partial Satisfier |
| |
| In this example, we see a more complex example of conflict resolution where the |
| term in question isn't totally satisfied by a single satisfier. Given the |
| following packages: |
| |
| * `root 1.0.0` depends on `foo ^1.0.0` and `target ^2.0.0`. |
| * `foo 1.1.0` depends on `left ^1.0.0` and `right ^1.0.0`. |
| * `foo 1.0.0` has no dependencies. |
| * `left 1.0.0` depends on `shared >=1.0.0`. |
| * `right 1.0.0` depends on `shared <2.0.0`. |
| * `shared 2.0.0` has no dependencies. |
| * `shared 1.0.0` depends on `target ^1.0.0`. |
| * `target 2.0.0` and `1.0.0` have no dependencies. |
| |
| `foo 1.1.0` transitively depends on a version of `target` that's not |
| compatible with `root`'s constraint. However, this dependency only exists |
| because of both `left` *and* `right`—either alone would allow a version of |
| `shared` without a problematic dependency to be selected. |
| |
| Pubgrub goes through the following steps: |
| |
| | Step | Value | Type | Where it was added | Cause | Decision level | |
| | ---- | ----- | ---- | ------------------ | ----- | -------------- | |
| | 1 | `root 1.0.0` | decision | top level | | 0 | |
| | 2 | `{root 1.0.0, not foo ^1.0.0}` | incompatibility | top level | | | |
| | 3 | `{root 1.0.0, not target ^2.0.0}` | incompatibility | top level | | | |
| | 4 | `foo ^1.0.0` | derivation | unit propagation | step 2 | 0 | |
| | 5 | `target ^2.0.0` | derivation | unit propagation | step 3 | 0 | |
| | 6 | `{foo >=1.1.0, not left ^1.0.0}` | incompatibility | decision making | | | |
| | 7 | `{foo >=1.1.0, not right ^1.0.0}` | incompatibility | decision making | | | |
| | 8 | `target 2.0.0` | decision | decision making | | 1 | |
| | 9 | `foo 1.1.0` | decision | decision making | | 2 | |
| | 10 | `left ^1.0.0` | derivation | unit propagation | step 6 | 2 | |
| | 11 | `right ^1.0.0` | derivation | unit propagation | step 7 | 2 | |
| | 12 | `{right any, not shared <2.0.0}` | incompatibility | decision making | | | |
| | 13 | `right 1.0.0` | decision | decision making | | 3 | |
| | 14 | `shared <2.0.0` | derivation | unit propagation | step 12 | 3 | |
| | 15 | `{left any, not shared >=1.0.0}` | incompatibility | decision making | | | |
| | 16 | `left 1.0.0` | decision | decision making | | 4 | |
| | 17 | `shared >=1.0.0` | derivation | unit propagation | step 15 | 4 | |
| | 18 | `{shared ^1.0.0, not target ^1.0.0}` | incompatibility | decision making | | | |
| |
| The incompatibility at step 18 is in conflict: `not target ^1.0.0` is satisfied |
| by `target ^2.0.0` from step 5, and `shared ^1.0.0` is *jointly* satisfied by |
| `shared <2.0.0` from step 14 and `shared >=1.0.0` from step 17. However, because |
| the satisfier and the previous satisfier have different decision levels, |
| conflict resolution has no root cause to find and just backtracks to decision |
| level 3, where it can make a new derivation: |
| |
| | Step | Value | Type | Where it was added | Cause | Decision level | |
| | ---- | ----- | ---- | ------------------ | ----- | -------------- | |
| | 19 | `not shared ^1.0.0` | derivation | unit propagation | step 18 | 3 | |
| |
| But this derivation causes a new conflict, which needs to be resolved: |
| |
| | Step | Incompatibility | Term | Satisfier | Satisfier Cause | Previous Satisfier | |
| | ---- | --------------- | ---- | --------- | --------------- | ------------------ | |
| | 20 | `{left any, not shared >=1.0.0}` | `not shared >=1.0.0` | `not shared ^1.0.0` from step 19 | `{shared ^1.0.0, not target ^1.0.0}` | `shared <2.0.0` from step 14 | |
| | 21 | `{left any, not target ^1.0.0, not shared >=2.0.0}` | `not shared >=2.0.0` | `shared <2.0.0` from step 14 | `{right any, not shared <2.0.0}` | `left ^1.0.0` from step 10 | |
| |
| Once again, we merge two incompatibilities, but this time we aren't able to |
| simplify the result. |
| `{left any, not target ^1.0.0, not shared >=1.0.0 ∪ shared ^1.0.0}` becomes |
| `{left any, not target ^1.0.0, not shared >=2.0.0}`. |
| |
| We once again stop conflict resolution and start backtracking, because the |
| satisfier (`shared <2.0.0`) and the previous satisfier (`left ^1.0.0`) have |
| different decision levels. This pattern happens frequently in conflict |
| resolution: Pubgrub finds the root cause of one conflict, backtracks a little |
| bit, and sees another related conflict that allows it to determine a more |
| broadly-applicable root cause. In this case, we backtrack to decision level 2, |
| where `left ^1.0.0` was derived: |
| |
| | Step | Value | Type | Where it was added | Cause | Decision level | |
| | ---- | ----- | ---- | ------------------ | ----- | -------------- | |
| | 22 | `{left any, not target ^1.0.0, not shared >=2.0.0}` | incompatibility | conflict resolution | | | |
| | 23 | `shared >=2.0.0` | derivation | unit propagation | step 22 | 2 | |
| |
| And we re-enter conflict resolution: |
| |
| | Step | Incompatibility | Term | Satisfier | Satisfier Cause | Previous Satisfier | |
| | ---- | --------------- | ---- | --------- | --------------- | ------------------ | |
| | 24 | `{right any, not shared <2.0.0}` | `not shared <2.0.0` | `shared >=2.0.0` from step 23 | `{left any, not target ^1.0.0, not shared >=2.0.0}` | `right ^1.0.0` from step 11 | |
| | 25 | `{left any, right any, not target ^1.0.0}` | `right any` | `right ^1.0.0` from step 11 | `{foo >=1.1.0, not right ^1.0.0}` | `left ^1.0.0` from step 10 | |
| | 26 | `{left any, foo >=1.1.0, not target ^1.0.0}` | `left any` | `left ^1.0.0` from step 10 | `{foo >=1.1.0, not left ^1.0.0}` | `foo 1.1.0` from step 9 | |
| | 27 | `{foo >=1.1.0, not target ^1.0.0}` | `foo >=1.1.0` | `foo 1.1.0` from step 9 | | `target ^2.0.0` from step 5 | |
| |
| Pubgrub has figured out that `foo 1.1.0` transitively depends on `target |
| ^1.0.0`, even though that dependency goes through `left`, `right`, and `shared`. |
| From here it backjumps to decision level 0, where `target ^2.0.0` was derived, |
| and quickly finds the correct solution: |
| |
| | Step | Value | Type | Where it was added | Cause | Decision level | |
| | ---- | ----- | ---- | ------------------ | ----- | -------------- | |
| | 28 | `{foo >=1.1.0, not target ^1.0.0}` | incompatibility | conflict resolution | | | |
| | 29 | `not foo >=1.1.0` | derivation | unit propagation | step 28 | 0 | |
| | 30 | `foo 1.0.0` | decision | decision making | | 1 | |
| | 31 | `target 2.0.0` | decision | decision making | | 2 | |
| |
| This produces the correct solution: `root 1.0.0`, `foo 1.0.0`, and |
| `target 2.0.0`. |
| |
| ## Linear Error Reporting |
| |
| This example's dependency graph doesn't have a valid solution. It shows how |
| error reporting works when the derivation graph is straightforwardly linear. |
| Given the following packages: |
| |
| * `root 1.0.0` depends on `foo ^1.0.0` and `baz ^1.0.0`. |
| * `foo 1.0.0` depends on `bar ^2.0.0`. |
| * `bar 2.0.0` depends on `baz ^3.0.0`. |
| * `baz 1.0.0` and `3.0.0` have no dependencies. |
| |
| `root` transitively depends on a version of `baz` that's not compatible with |
| `root`'s constraint. |
| |
| Pubgrub goes through the following steps: |
| |
| | Step | Value | Type | Where it was added | Cause | Decision level | |
| | ---- | ----- | ---- | ------------------ | ----- | -------------- | |
| | 1 | `root 1.0.0` | decision | top level | | 0 | |
| | 2 | `{root 1.0.0, not foo ^1.0.0}` | incompatibility | top level | | | |
| | 3 | `{root 1.0.0, not baz ^1.0.0}` | incompatibility | top level | | | |
| | 4 | `foo ^1.0.0` | derivation | unit propagation | step 2 | 0 | |
| | 5 | `baz ^1.0.0` | derivation | unit propagation | step 3 | 0 | |
| | 6 | `{foo any, not bar ^2.0.0}` | incompatibility | decision making | | | |
| | 7 | `foo 1.0.0` | decision | decision making | | 1 | |
| | 8 | `bar ^2.0.0` | derivation | unit propagation | step 6 | 1 | |
| | 9 | `{bar any, not baz ^3.0.0}` | incompatibility | decision making | | | |
| |
| The incompatibility added at step 10 is in conflict: `bar any` is satisfied by |
| `bar ^2.0.0` from step 8, and `not baz ^3.0.0` is satisfied by `baz ^1.0.0` from |
| step 5. Because these two satisfiers have different decision levels, conflict |
| resolution backtracks to level 0 where it can make a new derivation: |
| |
| | Step | Incompatibility | Term | Satisfier | Cause | Decision Level | |
| | ---- | --------------- | ---- | --------- | ----- | ------------------ | |
| | 10 | `not bar any` | derivation | unit propagation | step 9 | 0 | |
| |
| This derivation causes a new conflict, which needs to be resolved: |
| |
| | Step | Incompatibility | Term | Satisfier | Satisfier Cause | Previous Satisfier | |
| | ---- | --------------- | ---- | --------- | --------------- | ------------------ | |
| | 11 | `{foo any, not bar ^2.0.0}` | `not bar ^2.0.0` | `not bar any` from step 10 | `{bar any, not baz ^3.0.0}` | `foo ^1.0.0` from step 4 | |
| | 12 | `{foo any, not baz ^3.0.0}` | `not baz ^3.0.0` | `baz ^1.0.0` from step 5 | `{root 1.0.0, not baz ^1.0.0}` | `foo ^1.0.0` from step 4 | |
| | 13 | `{foo any, root 1.0.0}` | `foo any` | `foo ^1.0.0` from step 4 | `{root 1.0.0, not foo ^1.0.0}` | `root 1.0.0` from step 1 | |
| | 14 | `{root 1.0.0}` | | | | | |
| |
| By deriving the incompatibility `{root 1.0.0}`, we've determined that no |
| solution can exist and thus that version solving has failed. Our next task is to |
| construct a derivation graph for `{root 1.0.0}`. Each derived incompatibility's |
| causes are the incompatibility that came before it in the conflict resolution |
| table (`{foo any, root 1.0.0}` for the root incompatibility) and that |
| incompatibility's satisfier cause (`{root 1.0.0, not foo ^1.0.0}` for the root |
| incompatibility). |
| |
| This gives us the following derivation graph, with each incompatibility's step |
| number indicated: |
| |
| ``` |
| ┌6────────────────────────┐ ┌9────────────────────────┐ |
| │{foo any, not bar ^2.0.0}│ │{bar any, not baz ^3.0.0}│ |
| └────────────┬────────────┘ └────────────┬────────────┘ |
| │ ┌─────────────────────┘ |
| ▼ ▼ |
| ┌12──────────┴──────┴─────┐ ┌3───────────────────────────┐ |
| │{foo any, not baz ^3.0.0}│ │{root 1.0.0, not baz ^1.0.0}│ |
| └────────────┬────────────┘ └─────────────┬──────────────┘ |
| │ ┌───────────────────────┘ |
| ▼ ▼ |
| ┌13────────┴────┴─────┐ ┌2───────────────────────────┐ |
| │{foo any, root 1.0.0}│ │{root 1.0.0, not foo ^1.0.0}│ |
| └──────────┬──────────┘ └─────────────┬──────────────┘ |
| │ ┌────────────────────────┘ |
| ▼ ▼ |
| ┌14───┴───┴──┐ |
| │{root 1.0.0}│ |
| └────────────┘ |
| ``` |
| |
| We run the [error reporting](#error-reporting) algorithm on this graph starting |
| with the root incompatibility, `{root 1.0.0}`. Because this algorithm does a |
| depth-first traversal of the graph, it starts by printing the outermost external |
| incompatibilities and works its way towards the root. Here's what it prints, |
| with the step of the algorithm that prints each line indicated: |
| |
| | Message | Algorithm Step | Line | |
| | ------- | -------------- | ---- | |
| | Because every version of `foo` depends on `bar ^2.0.0` which depends on `baz ^3.0.0`, every version of `foo` requires `baz ^3.0.0`. | 3 | | |
| | So, because `root` depends on both `baz ^1.0.0` and `foo ^1.0.0`, version solving failed. | 2.ii | | |
| |
| There are a couple things worth noting about this output: |
| |
| * Pub's implementation of error reporting has some special cases to make output |
| more human-friendly: |
| |
| * When we're talking about every version of a package, we explicitly write |
| "every version of `foo`" rather than "`foo any`". |
| |
| * In the first line, instead of writing "every version of `foo` depends on |
| `bar ^2.0.0` and every version of `bar` depends on `baz ^3.0.0`", we write |
| "every version of `foo` depends on `bar ^2.0.0` which depends on |
| `baz ^3.0.0`". |
| |
| * In the second line, instead of writing "`root` depends on `baz ^1.0.0` and |
| `root` depends on `foo ^1.0.0`", we write "`root` depends on both |
| `baz ^1.0.0` and `foo ^1.0.0`". |
| |
| * We omit the version number for the entrypoint package `root`. |
| |
| * Instead of writing "And" for the final line, we write "So," to help indicate |
| that it's a conclusion. |
| |
| * Instead of writing "`root` is forbidden", we write "version solving failed". |
| |
| * The second line collapses together the explanations of two incompatibilities |
| (`{foo any, root 1.0.0}` and `{root 1.0.0}`), as described in step 2.ii. We |
| never explicitly explain that every version of `foo` is incompatible with |
| `root`, but the output is still clear. |
| |
| ## Branching Error Reporting |
| |
| This example fails for a reason that's too complex to explain in a linear chain |
| of reasoning. It shows how error reporting works when it has to refer back to a |
| previous derivation. Given the following packages: |
| |
| * `root 1.0.0` depends on `foo ^1.0.0`. |
| * `foo 1.0.0` depends on `a ^1.0.0` and `b ^1.0.0`. |
| * `foo 1.1.0` depends on `x ^1.0.0` and `y ^1.0.0`. |
| * `a 1.0.0` depends on `b ^2.0.0`. |
| * `b 1.0.0` and `2.0.0` have no dependencies. |
| * `x 1.0.0` depends on `y ^2.0.0`. |
| * `y 1.0.0` and `2.0.0` have no dependencies. |
| |
| Neither version of `foo` can be selected due to their conflicting direct and |
| transitive dependencies on `b` and `y`, which means version solving fails. |
| |
| Pubgrub goes through the following steps: |
| |
| | Step | Value | Type | Where it was added | Cause | Decision level | |
| | ---- | ----- | ---- | ------------------ | ----- | -------------- | |
| | 1 | `root 1.0.0` | decision | top level | | 0 | |
| | 2 | `{root 1.0.0, not foo ^1.0.0}` | incompatibility | top level | | | |
| | 3 | `foo ^1.0.0` | derivation | unit propagation | step 2 | 0 | |
| | 4 | `foo 1.1.0` | decision | decision making | | 1 | |
| | 5 | `{foo >=1.1.0, not y ^1.0.0}` | incompatibility | decision making | | | |
| | 6 | `{foo >=1.1.0, not x ^1.0.0}` | incompatibility | decision making | | | |
| | 7 | `y ^1.0.0` | derivation | unit propagation | step 5 | 1 | |
| | 8 | `x ^1.0.0` | derivation | unit propagation | step 6 | 1 | |
| | 9 | `{x any, not y ^2.0.0}` | incompatibility | decision making | | | |
| |
| This incompatibility is in conflict, so we enter conflict resolution: |
| |
| | Step | Incompatibility | Term | Satisfier | Satisfier Cause | Previous Satisfier | |
| | ---- | --------------- | ---- | --------- | --------------- | ------------------ | |
| | 10 | `{x any, not y ^2.0.0}` | `x any` | `x ^1.0.0` from step 8 | `{foo >=1.1.0, not x ^1.0.0}` | `y ^1.0.0` from step 7 | |
| | 11 | `{foo >=1.1.0, not y ^2.0.0}` | `not y ^2.0.0` | `y ^1.0.0` from step 7 | `{foo >=1.1.0, not y ^1.0.0}` | `foo 1.1.0` from step 4 | |
| | 12 | `{foo >=1.1.0}` | `foo >=1.1.0` | `foo 1.1.0` from step 4 | `{root 1.0.0, not foo ^1.0.0}` | | |
| |
| We then backtrack to decision level 0, since there is no previous satisfier: |
| |
| | Step | Value | Type | Where it was added | Cause | Decision level | |
| | ---- | ----- | ---- | ------------------ | ----- | -------------- | |
| | 13 | `{foo >=1.1.0}` | incompatibility | conflict resolution | | | |
| | 14 | `not foo >=1.1.0` | derivation | unit propagation | step 13 | 0 | |
| | 15 | `{foo <1.1.0, not b ^1.0.0}` | incompatibility | decision making | | | |
| | 16 | `{foo <1.1.0, not a ^1.0.0}` | incompatibility | decision making | | | |
| | 17 | `foo 1.0.0` | decision | decision making | | 1 | |
| | 18 | `b ^1.0.0` | derivation | unit propagation | step 15 | 1 | |
| | 19 | `a ^1.0.0` | derivation | unit propagation | step 16 | 1 | |
| | 20 | `{a any, not b ^2.0.0}` | incompatibility | decision making | | | |
| |
| We've found another conflicting incompatibility, so we'll go back into conflict |
| resolution: |
| |
| | Step | Incompatibility | Term | Satisfier | Satisfier Cause | Previous Satisfier | |
| | ---- | --------------- | ---- | --------- | --------------- | ------------------ | |
| | 21 | `{a any, not b ^2.0.0}` | `a any` | `a ^1.0.0` from step 19 | `{foo <1.1.0, not a ^1.0.0}` | `b ^1.0.0` from step 18 | |
| | 22 | `{foo <1.1.0, not b ^2.0.0}` | `not b ^2.0.0` | `b ^1.0.0` from step 18 | `{foo >=1.1.0, not b ^1.0.0}` | `not foo >=1.0.0` from step 14 | |
| |
| We now backtrack to decision level 0 where the previous satisfier was derived: |
| |
| | Step | Value | Type | Where it was added | Cause | Decision level | |
| | ---- | ----- | ---- | ------------------ | ----- | -------------- | |
| | 23 | `{foo <1.1.0, not b ^2.0.0}` | incompatibility | conflict resolution | | | |
| | 24 | `b ^2.0.0` | derivation | unit propagation | step 23 | 0 | |
| |
| But this produces another conflict, this time in the incompatibility from line |
| 15: |
| |
| | Step | Incompatibility | Term | Satisfier | Satisfier Cause | Previous Satisfier | |
| | ---- | --------------- | ---- | --------- | --------------- | ------------------ | |
| | 25 | `{foo <1.1.0, not b ^1.0.0}` | `not b ^1.0.0` | `b ^2.0.0` from step 24 | `{foo <1.1.0, not b ^2.0.0}` | `not foo >=1.1.0` from step 14 | |
| | 26 | `{foo <1.1.0}` | `foo <1.1.0` | `not foo >=1.1.0` from step 14 | `{foo >=1.1.0}` | `foo ^1.0.0` from step 3 | |
| | 27 | `{foo any}` | `foo any` | `foo ^1.0.0` from step 3 | `{root 1.0.0, not foo ^1.0.0}` | | |
| | 28 | `{root 1.0.0}` | | | | | |
| |
| This produces a more complex derivation graph than the previous example: |
| |
| ``` |
| ┌20───────────────────┐ ┌16────────────────────────┐ |
| │{a any, not b ^2.0.0}│ │{foo <1.1.0, not a ^1.0.0}│ |
| └──────────┬──────────┘ └────────────┬─────────────┘ |
| │ ┌─────────────────────┘ |
| ▼ ▼ |
| ┌22──────────┴──────┴──────┐ ┌15────────────────────────┐ |
| │{foo <1.1.0, not b ^2.0.0}│ │{foo <1.1.0, not b ^1.0.0}│ |
| └────────────┬─────────────┘ └────────────┬─────────────┘ |
| │ ┌───────────────────────┘ |
| ▼ ▼ |
| ┌26─────┴────┴───┐ ┌9────────────────────┐ ┌6──────────────────────────┐ |
| │{not foo <1.1.0}│ │{x any, not y ^2.0.0}│ │{foo >=1.1.0, not x ^1.0.0}│ |
| └───────┬────────┘ └──────────┬──────────┘ └─────────────┬─────────────┘ |
| │ │ ┌──────────────────────┘ |
| │ ▼ ▼ |
| │ ┌11───────────┴──────┴──────┐ ┌5──────────────────────────┐ |
| │ │{foo >=1.1.0, not y ^2.0.0}│ │{foo >=1.1.0, not y ^1.0.0}│ |
| │ └─────────────┬─────────────┘ └─────────────┬─────────────┘ |
| │ │ ┌────────────────────────┘ |
| ▼ ▼ ▼ |
| ┌27────┴──────┐ ┌12──────┴────┴───┐ |
| │{not foo any}├◀─────┤{not foo >=1.1.0}│ |
| └──────┬──────┘ └─────────────────┘ |
| ▼ |
| ┌28────┴─────┐ ┌2───────────────────────────┐ |
| │{root 1.0.0}├◀─┤{root 1.0.0, not foo ^1.0.0}│ |
| └────────────┘ └────────────────────────────┘ |
| ``` |
| |
| We run the [error reporting](#error-reporting) algorithm on this graph: |
| |
| | Message | Algorithm Step | Line | |
| | ------- | -------------- | ---- | |
| | Because `foo <1.1.0` depends on `a ^1.0.0` which depends on `b ^2.0.0`, `foo <1.1.0` requires `b ^2.0.0`. | 3 | | |
| | So, because `foo <1.1.0` depends on `b ^1.0.0`, `foo <1.1.0` is forbidden. | 2.iii | 1 | |
| | | | |
| | Because `foo >=1.1.0` depends on `x ^1.0.0` which depends on `y ^2.0.0`, `foo >=1.1.0` requires `y ^2.0.0`. | 3 | | |
| | And because `foo >=1.1.0` depends on `y ^1.0.0`, `foo >=1.1.0` is forbidden. | 2.iii | | |
| | And because `foo <1.1.0` is forbidden (1), `foo` is forbidden. | 1.ii | | |
| | So, because `root` depends on `foo ^1.0.0`, version solving failed. | 2.iii | | |
| |
| Because the derivation graph is non-linear–the incompatibility `{not foo any}` |
| is caused by two derived incompatibilities–we can't just explain everything in a |
| single sequence like we did in the last example. We first explain why |
| `foo <1.1.0` is forbidden, giving the conclusion an explicit line number so that |
| we can refer back to it later on. Then we explain why `foo >=1.1.0` is forbidden |
| before finally concluding that version solving has failed. |
| |
| # Differences From CDCL and Answer Set Solving |
| |
| Although Pubgrub is based on CDCL and answer set solving, it differs from the |
| standard algorithms for those techniques in a number of important ways. These |
| differences make it more efficient for version solving in particular and |
| simplify away some of the complexity inherent in the general-purpose algorithms. |
| |
| ## Version Ranges |
| |
| The original algorithms work exclusively on atomic boolean variables that must |
| each be assigned either "true" or "false" in the solution. In package terms, |
| these would correspond to individual package versions, so dependencies would |
| have to be represented as: |
| |
| (foo 1.0.0 or foo 1.0.1 or ...) → (bar 1.0.0 or bar 1.0.1 or ...) |
| |
| This would add a lot of overhead in translating dependencies from version ranges |
| to concrete sets of individual versions. What's more, we'd have to try to |
| reverse that conversion when displaying messages to users, since it's much more |
| natural to think of packages in terms of version ranges. |
| |
| So instead of operating on individual versions, Pubgrub uses as its logical |
| terms `PackageName`s that may be either `PackageId`s (representing individual |
| versions) or `PackageRange`s (representing ranges of allowed versions). The |
| dependency above is represented much more naturally as: |
| |
| foo ^1.0.0 → bar ^1.0.0 |
| |
| ## Implicit Mutual Exclusivity |
| |
| In the original algorithms, all relationships between variables must be |
| expressed as explicit formulas. A crucial feature of package solving is that the |
| solution must contain at most one package version with a given name, but |
| representing that in pure boolean logic would require a separate formula for |
| each pair of versions for each package. This would mean an up-front cost of |
| O(n²) in the number of versions per package. |
| |
| To avoid that overhead, the mutual exclusivity of different versions of the same |
| package (as well as packages with the same name from different sources) is built |
| into Pubgrub. For example, it considers `foo ^1.0.0` and `foo ^2.0.0` to be |
| contradictory even though it doesn't have an explicit formula saying so. |
| |
| ## Lazy Formulas |
| |
| The original algorithms are written with the assumption that all formulas |
| defining the relationships between variables are available throughout the |
| algorithm. However, when doing version solving, it's impractical to eagerly list |
| all dependencies of every package. What's more, the set of packages that may be |
| relevant can't be known in advance. |
| |
| Instead of listing all formulas immediately, Pubgrub adds only the formulas that |
| are relevant to individual package versions, and then only when those versions |
| are candidates for selection. Because those formulas always involve a package |
| being selected, they're guaranteed not to contradict the existing set of |
| selected packages. |
| |
| ## No Unfounded Set Detection |
| |
| Answer set solving has a notion of "unfounded sets": sets of variables whose |
| formulas reference one another in a cycle. A naïve answer set solving algorithm |
| may end up marking these variables as true even when that's not necessary, |
| producing a non-minimal solution. To avoid this, the algorithm presented in |
| Gebser *et al* adds explicit formulas that force these variable to be false if |
| they aren't required by some formula outside the cycle. |
| |
| This adds a lot of complexity to the algorithm which turns out to be unnecessary |
| for version solving. Pubgrub avoids selecting package versions in unfounded sets |
| by only choosing versions for packages that are known to have outstanding |
| dependencies. |