Closed malaire closed 4 years ago
Thanks for the creation of the issue. I suppose you figured out that the binary tree displayed in the debug output for priorCause is the chain of derivation leading to the actual incompatibility creating the issue.
Yes it's the "Derivation Graph" mentioned at PubGrub documentation[1]. I'm still reading this while comparing it to the implementation.
While current debug output is quite complete, it would be more useful if it used exact same terms as the PubGrub documentation. For example terms changed
, package
, incompatibility
etc. in "Unit Propagation" section.
[1] https://github.com/dart-lang/pub/blob/master/doc/solver.md
PubGrub documentation says that
The process of returning the partial solution to a state where the incompatibility is no longer satisfied is known as conflict resolution.
So error here is in conflictResolution
which doesn't return correct value. I see it has some notes about things which are guaranteed, but those are not checked, so adding more error reporting there would be first step to do.
Adding this debug check makes Example 5bis fail earlier, and also makes Examples 3 and 4 fail: https://github.com/malaire/elm-pubgrub/commit/02dc6fc403974fef10f36b9eccc67ba9ab7f6955
p.s. I havn't yet learned how to make pull requests in GitHub.
:)
Why can't official guide tell simple solution for making PR, but instead has longwinded 8-step process which requires base branches, head forks and compare branches?
Anyway, here's the PR (https://github.com/mpizenberg/elm-pubgrub/pull/2), and I think I'll continue debugging with Example 3 which now fails also and is simpler than 5bis.
So I've done a full re-read of the PubGrub algorithm. The last step of the conflict resolution is to return the current incompatibility of the loop and to backtrack the partial solution. That incompatibility has been obtained through successive "priorCause" computations and should be referred to as the "root cause" to stick to PubGrub namings. If we believe PubGrub unit propagation explanation, the returned root cause should be guaranted to almost satisfy the backtracked partial solution. I'll probably want to verify why that is, but let's assume that is indeed the case (they should know ^^). In that case, these are the things that could go wrong.
not any == none
right? (and what does not none
mean? is it the same than any
?) There may be comparisons out there that return false for these and should return true. All functions returning a Term should do so in a normalized way (only use none
?).I'll want to check those things manually and maybe add unit tests. There are also few remarks:
About {q, r, t1 U t2}
: Documentation says
In fact, we can generalize this: given any incompatibilities
{t1, q}
and{t2, r}
, we can derive{q, r, t1 ∪ t2}
, since eithert1
ort2
is true in every solution in whicht1 ∪ t2
is true. This reduces to{q, r}
in any case wherenot t2 ⊆ t1
(that is, wherenot t2
satisfiest1
), including the case above wheret1 = t
andt2 = not t
.
The way I understand this is that {q, r, t1 ∪ t2}
is just part of the explanation - it's never actually used in the algorithm. Instead in algorithm {q, r}
is used, but only when not t2
satisfies t1
.
Yes it is actually used, that's the else
block here: https://github.com/mpizenberg/elm-pubgrub/blob/master/src/PubGrub.elm#L331
It's just that the naming of the algorithm calls it "priorCause" even before it is actually a correct prior cause, which I called newIncompat
later because I was just out of names and didn't fully understand it yet: https://github.com/mpizenberg/elm-pubgrub/blob/master/src/PubGrub.elm#L346
Ah, true. priorCause
is initially set to {q, r}
, but the third term can be added a bit later.
Although the added term isn't t1 ∪ t2
but corresponding not (not t2 \ t1)
.
Note:
not (satisfier \ term)
corresponds tot1 ∪ t2
above withterm = t1
andsatisfier = not t2
, by the identity(Sᶜ \ T)ᶜ = S ∪ T
.
p.s. I'll need to brush up set theory.
But elm-pubgrub
is adding the union instead of not (satisfier \ term)
as algorithm says?
The logic is explained just above I believe this is correct
To expand a bit the explanation. I do not have the \
Set operator but I have the union and intersection operations on ranges Sets, so with A
for satisfier and B
for term we have:
A \ B == intersection A (not B)
and
union (not A) (B) == not (intersection A (not B))
so
not (A \ B) == union (not A) B
Yes, even the algorithm description says that they correspond to each other. But algorithm description clearly says that the added term is not (satisfier \ term)
and not the union. So elm-pubgrub
is wrong here.
From https://github.com/dart-lang/pub/blob/master/doc/solver.md#conflict-resolution:
- If
satisfier
doesn't satisfyterm
, addnot (satisfier \ term)
topriorCause
.
- Note:
not (satisfier \ term)
corresponds tot1 ∪ t2
above withterm = t1
andsatisfier = not t2
, by the identity(Sᶜ \ T)ᶜ = S ∪ T
.
This is the second place I've seen where elm-pubgrub
deliberately implements the algorithm incorrectly.
I see no reason to debug elm-pubgrub
further as it doesn't even attempt to implement Pubgrub correctly.
I'm sorry if this frustrates you but I'm trying to do the correct implementation (with the time I can put in it) and to strictly follow the PubGrub algorithm, the explanation above that I'm re-adding below seems correct to me so I am actually adding not (satisfier \ term)
. I cannot just invent out of fin air the \
operator, and if I'd have to add it, I'd just define it as below. If you've seen another place delibarately different than PubGrub, please do tell.
A \ B == intersection A (not B)
and
union (not A) (B) == not (intersection A (not B))
so
not (A \ B) == union (not A) B
If you've seen another place delibarately different than PubGrub, please do tell.
The other place is about expanding/merging version ranges: https://github.com/mpizenberg/elm-pubgrub/issues/3#issuecomment-639600678
... the explanation above that I'm re-adding below seems correct to me so I am actually adding
not (satisfier \ term)
...
For one thing you are adding potentially POSITIVE term while algorithm clearly says to add NEGATIVE term.
Also doing things differently than what is said in algorithm makes debugging a lot harder as it's not possible to compare the behavior of your implementation to the exact steps mentioned in documentation.
For one thing you are adding potentially POSITIVE term while algorithm clearly says to add NEGATIVE term.
This isn't sayed anywhere, or it may be your interpretation that differs from mine. I don't see a reason for satisfier or term to be positive terms. Maybe that's my interpretation that's wrong and that's why having a second view is great. satisfier
and term
could very well be negative terms, and in that case not (satisfier \ term)
is positive.
As for the equivalent code in the actual dart pub code base, it turns out the Set complement operator is called difference
there and is implemented as follows (exactly how I did).
https://github.com/dart-lang/pub/blob/master/lib/src/solver/term.dart#L149
Also doing things differently than what is said in algorithm makes debugging a lot harder as it's not possible to compare the behavior of your implementation to the exact steps mentioned in documentation.
Sorry about that, I thought the comment just above in the code would suffice to follow easily. Obviously things need to be different between an imperative description and a functional implementation. I didn't thought that line would be much trouble compared to the structural changes to the algorithm required by the Elm language.
The other place is about expanding/merging version ranges: #3 (comment)
I can understand if you consider this deliberately different. Actually the description of the PubGrub algorithm leaves a lot of grey areas where I just have to make some choices, like version choosing heuristics and other things. It also indicates some things as being uneeded for correctness but good for performances. I interpreted this explanation as such one of those performance-only improvements, especially given that no such collapse is part of the algorithm steps below the textual explanation.
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}.
Since I have a limited time implementing all this, I've reduced things to what needs to be done, and added some TODOS in the code base for those as in that example you point out:
https://github.com/mpizenberg/elm-pubgrub/blob/master/src/Incompatibility.elm#L190
But again, if this is a misinterpretation on my part, you're right and that's why I've added your point to the things to check in this issue (see "absence of smart merging" there: https://github.com/mpizenberg/elm-pubgrub/issues/1#issuecomment-640913440)
Anyway, I just thought that your comment was a bit harsh and wanted to let you know that it hurt me. If you do not feel like helping anymore I totally understand.
When I mentioned following the steps, I meant the steps in Examples section.
But again, if this is a misinterpretation on my part, you're right and that's why I've added your point to the things to check in this issue (see "absence of smart merging" there: #1 (comment))
Can you confirm whether relation-checking at any point has access to the list of all available versions? Because if not, then current implementation of elm-pubgrub
can't work correctly, as I said in the other thread (https://github.com/mpizenberg/elm-pubgrub/issues/3#issuecomment-639641827), and so this clear bug should be at top of the list of things to check and not the last item to check.
As for the equivalent code in the actual dart pub code base, it turns out the Set complement operator is called difference there and is implemented as follows (exactly how I did).
I was wrong here then. I thought it was important when algorithm description clearly made the point that different term needs to be used instead of union.
Can you confirm whether relation-checking at any point has access to the list of all available versions? Because if not, then current implementation of elm-pubgrub can't work correctly, as I said in the other thread (#3 (comment)).
It doesn't. And it appears that pub also doesn't have access at that time:
https://github.com/dart-lang/pub/blob/master/lib/src/solver/version_solver.dart#L156
Then I suppose this means that incompatibilities should be more descriptive and collapsing is needed? Merging a new incompatibility (Incompatibility.merge
and in PubGrub.backtrack
that should use Incompatibility.merge
) is a place where this could be done I suppose. I didn't take the time yet to check that since I wanted to first get a better overview of the algorithm.
I'll take some time next weekend on specifically this issue https://github.com/mpizenberg/elm-pubgrub/issues/3#issuecomment-639641827 that you pointed out.
I'll expand on my understanding of bar any
vs. bar 1.0.0
issue if you didn't understand what I think is wrong here.
Basic question in conflict resolution is whether certain term satisfies another term.
IN ALGORITHM
In Example 3 the question is whether bar ^1.0.0
satisfies bar any
.
In other words the question is: If bar is 1.0.0 <= x < 2.0.0
is true, does that mean bar is any version
is always true? Clearly answer is YES, because if bar
is a version within range 1.0.0 <= x < 2.0.0
then bar
does have a version.
In terms of sets we can say that bar ^1.0.0
is SUBSET of bar any
.
IN ELM-PUBGRUB
elm-pubgrub
uses bar 1.0.0
instead of bar any
. Let's see how this works out:
The question now is whether bar ^1.0.0
satisfies bar 1.0.0
.
In other words the question is: If bar is 1.0.0 <= x < 2.0.0
is true, does that mean bar is version 1.0.0
is always true? Clearly answer is NO, because if bar
is a version within range 1.0.0 <= x < 2.0.0
then bar
could be for example 1.5.0
which is not 1.0.0
.
In terms of sets we can say that bar ^1.0.0
is SUPERSET of bar 1.0.0
.
THE BUG
So by deciding to use bar 1.0.0
instead of bar any
, elm-pubgrub
arrives at completely wrong conclusion IF it only uses set operations to check for set relations.
HOWEVER if elm-pubgrub
uses list of available versions when checking set relations, then and only then, can it conclude that in Example 3 bar ^1.0.0
actually is same as bar 1.0.0
because there is only one version of bar
, and so answer is YES.
And it appears that pub also doesn't have access at that time:
pub doesn't need to because it would only be needed when not merging/expanding version ranges.
Note that bar ^1.0.0
above comes from dependencies, so it's range both in pub
and elm-pubgrub
.
So stated another way:
in pub
:
termFromDepencencies
: bar ^1.0.0
termFromPickedVersion
: bar any
in elm-pubgrub
:
termFromDepencencies
: bar ^1.0.0
termFromPickedVersion
: bar 1.0.0
Problem:
However you implement relations-checking, it must conclude that in context of Example 3, termFromDepencencies
satisfies termFromPickedVersion
.
In case of pub
this can be done using only set operations, without context.
In case of elm-pubgrub
this can't be done using only set operations, without context, but needs to use context (i.e. the set of available versions).
p.s. It's not actually relevant that range is any
. This issue would arise also with smaller range than any
which is still superset of ^1.0.0
.
You could also say that pub
takes the context of other available versions into account when merging/expanding picked version to a range.
elm-pubgrub
doesn't take this context into account at that point, so it must take that context into account at some other point. Currently it doesn't, which is why relation checking, and in extension conflict resolution, doesn't work.
While this was from Example 3, I believe it's highly likely that this is also behind this issue of Example 5bis failing.
I'm pretty confident I've identified the issue. It has to do with prior cause computation but not what we thought on collapsing incompatibilities.
First the issue you raised is actually not a problem it turns out. In fact the step-by-step examples are a little bit idealized to be more easily readable. In practice, there is another kind of incompatibility in the algorithm that is left out of the explanations for matters of simplicity. These are "noVersion" incompatibilities and are mentioned in the detailed algorithm in the following decision making step:
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.
In pub code this appears here: https://github.com/dart-lang/pub/blob/master/lib/src/solver/version_solver.dart#L380. In elm-pubgrub this corresponds to the Unknown
variant of an incompatibility kind that should be renamed to NoVersion
(or better, there should be some changes in the Incompatibility
type).
What's interesting is that a non-optimal incompatibility (such as { bar 1, foo not (1 <=v < 2) }
) will later trigger search for a version of bar in the range 1.0.1 <= v < 2
that is the rest of the search space. At the time of picking a version, which has access to packages and versions it will realize that no such version exist. As such, a new incompatibility will be generated of the form { bar : the rest of the search space }
. This is what happens in the debug logs here:
bar: 1.0.0 <= v < 2.0.0, foo: Not ( 1.0.0 <= v < 2.0.0 ) <<< derived from:
bar: 1.0.0, foo: Not ( 1.0.0 <= v < 2.0.0 ) <<< from dependency of bar at version 1.0.0
bar: 1.0.1 <= v < 2.0.0 <<< from unknown reason ...: ""
Now onto the issue.
Remember the question whether or not the satisfier term has to be added to the initialization of the prior cause with both incompatibilities without their terms related to satisfier package? It turns out the same should be done for any package. Let me develop that. In example 5bis, after getting through the transitive bar
dependency, the algorithm realizes that it will not be possible. At that point the following detailed logs for prior cause computation give:
previousLevel == satisfierLevel: ""
satisfier bar Not ( 2.0.0 ): ""
cause
bar: 2.0.0, baz: Not ( 3.0.0 <= v < 4.0.0 ) <<< from dependency of bar at version 2.0.0: ""
incompat
bar: 2.0.1 <= v < 3.0.0, baz: Not ( 2.0.0 <= v < 3.0.0 ) <<< derived from:
bar: 2.1.0, baz: Not ( 2.0.0 <= v < 3.0.0 ) <<< from dependency of bar at version 2.1.0
bar: [ 2.0.1, 2.1.0 [ [ 2.1.1, 3.0.0 [ <<< from unknown reason ...: ""
priorCause
bar: 2.0.0 <= v < 3.0.0, baz: Not ( ∅ ) <<< derived from:
bar: 2.0.0, baz: Not ( 3.0.0 <= v < 4.0.0 ) <<< from dependency of bar at version 2.0.0
bar: 2.0.1 <= v < 3.0.0, baz: Not ( 2.0.0 <= v < 3.0.0 ) <<< derived from:
bar: 2.1.0, baz: Not ( 2.0.0 <= v < 3.0.0 ) <<< from dependency of bar at version 2.1.0
bar: [ 2.0.1, 2.1.0 [ [ 2.1.1, 3.0.0 [ <<< from unknown reason ...: ""
Here the satisfier package is bar
so the rest of the two incompatibilities used for priorCause
initialization are
{ baz: Not ( 3.0.0 <= v < 4.0.0 ) }
and
{ baz: Not ( 2.0.0 <= v < 3.0.0 ) }
And the union of those to terms (logical "or") gives "not none". Actually, it turns out "not none" is always satisfied. Indeed it is satisfied if no version is selected (because it's negative) and it is satisfied if any version is selected. Instead, just like for the satisfier term that we do not add when not t1 is included in t2, we should not add a term to an incompatibility when the union is "not none". This is actually exactly the same thing, just expressed differently (here we could say that not (not (3 <= v < 4))
which is 3 <= v < 4
is included in not (2 <= v < 3)
).
Because we do not do that, we have the incompatibility {bar: 2.0.0 <= v < 3.0.0, baz: Not ( ∅ )}
in our set instead of just {bar: 2.0.0 <= v < 3.0.0}
. And that incompatibility propagates later to {root: 1.0.0, baz: Not ( ∅ )}
just before reaching the impossible state. Indeed the incompatibility {root: 1.0.0, baz: Not ( ∅ )}
is a disguised terminal case, that would have been detected by the first condition of conflictResolution
if it was in its canonical form {root: 1.0.0}
.
if Dict.isEmpty (Incompatibility.asDict incompat) || Incompatibility.singlePositive root incompat then
I'm very confident that this will solve that issue. I just started to add notes to the code for places that need to change. Next weekend I can't work on this, the weekend after it all should be solved. Then there will only be error reporting left :)
Actually the fix is pretty trivial, I just replaced the Incompatibility.fuse
code by
fuse : String -> Term -> Term -> Incompatibility -> Incompatibility
fuse name t1 t2 incompatibility =
let
termUnion =
Term.union t1 t2
in
if termUnion == Term.Negative Range.none then
incompatibility
else
insert name (Term.union t1 t2) incompatibility
And it works on example 5bis, i.e. it stops and identify that root package can't be selected before reaching any impossible state.
Also PS, there was a bug in Term.union
. It was returning a positive term instead of a negative term when we have one of each. I've fixed it by union t1 t2 = not ( intersection (not t1) (not t2))
. I'll also fix that properly in git in two weekends.
wow, this pubgrub is really tricky endevour. I guess this should not be too suprising as it is NP-hard. Is very impressive the progress made so far :+1:
I should just stick with the math I know. This algorithm is far too complex for me.
Creating an issue for this for notes and discussion.
Example 5bis fails in
PubGrub.unitPropagationLoop
byPartialSolution.relation priorCause updatedModel.partialSolution
returningSatisfies
instead of expectedAlmostSatisfies
.Here
partialSolution
isand
priorCause
isManual checking confirms that
partialSolution
indeed satisfiespriorCause
, so one of these is wrong.