blob: 30605db7d3a0b1c0dc24514a87afa3ef79fe2d38 [file] [log] [blame] [view]
* [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.