@@ -116,7 +116,7 @@ impl<DP: DependencyProvider> State<DP> {
116
116
& mut self ,
117
117
package : Id < DP :: P > ,
118
118
) -> Result < SmallVec < ( Id < DP :: P > , IncompDpId < DP > ) > , NoSolutionError < DP > > {
119
- let mut root_causes = SmallVec :: default ( ) ;
119
+ let mut satisfier_causes = SmallVec :: default ( ) ;
120
120
self . unit_propagation_buffer . clear ( ) ;
121
121
self . unit_propagation_buffer . push ( package) ;
122
122
while let Some ( current_package) = self . unit_propagation_buffer . pop ( ) {
@@ -169,12 +169,11 @@ impl<DP: DependencyProvider> State<DP> {
169
169
}
170
170
}
171
171
if let Some ( incompat_id) = conflict_id {
172
- let ( package_almost, root_cause) =
173
- self . conflict_resolution ( incompat_id)
174
- . map_err ( |terminal_incompat_id| {
175
- self . build_derivation_tree ( terminal_incompat_id)
176
- } ) ?;
177
- root_causes. push ( ( package, root_cause) ) ;
172
+ let ( package_almost, root_cause) = self
173
+ . conflict_resolution ( incompat_id, & mut satisfier_causes)
174
+ . map_err ( |terminal_incompat_id| {
175
+ self . build_derivation_tree ( terminal_incompat_id)
176
+ } ) ?;
178
177
self . unit_propagation_buffer . clear ( ) ;
179
178
self . unit_propagation_buffer . push ( package_almost) ;
180
179
// Add to the partial solution with incompat as cause.
@@ -190,16 +189,46 @@ impl<DP: DependencyProvider> State<DP> {
190
189
}
191
190
}
192
191
// If there are no more changed packages, unit propagation is done.
193
- Ok ( root_causes )
192
+ Ok ( satisfier_causes )
194
193
}
195
194
196
195
/// Return the root cause or the terminal incompatibility.
197
196
/// CF <https://github.com/dart-lang/pub/blob/master/doc/solver.md#unit-propagation>
197
+ ///
198
+ /// Usually by the time we have a conflict `unit_propagation` has done a lot of work.
199
+ /// So the actual conflict we find is important, but not particularly actionable.
200
+ /// It says something like "the dependency on package X and the dependency on package Y are incompatible".
201
+ /// To make it actionable we want to track it back to decisions that made the dependency required.
202
+ /// "The decision on B is incompatible with the decision on C,
203
+ /// because unit propagation from just those decisions will lead to the conflict about X and Y"
204
+ /// is much more actionable, backtrack until one of those decisions can be revisited.
205
+ /// To make a practical, we really only need one of the terms to be a decision.
206
+ /// We may as well leave the other terms general. Something like
207
+ /// "the dependency on the package X is incompatible with the decision on C" tends to work out pretty well.
208
+ /// Then if A turns out to also have a dependency on X the resulting root cause is still useful.
209
+ /// Of course, this is more heuristics than science. If the output is too general, then `unit_propagation` will
210
+ /// handle the confusion by calling us again with the next most specific conflict it comes across.
211
+ /// If the output is to specific, then the outer `solver` loop will eventually end up calling us again
212
+ /// until all possibilities are enumerated.
213
+ ///
214
+ /// This function combines incompatibilities with things that make the problem inevitable to end up with a
215
+ /// more useful incompatibility. For the correctness of the PubGrub algorithm only the final output is required.
216
+ /// By banning the final output, unit propagation will prevent the intermediate steps from occurring again,
217
+ /// at least prevent the exact same way. However, the statistics collected for `prioritize`may want
218
+ /// to analyze those intermediate steps. For example we might start with "there is no version 1 of Z",
219
+ /// and `conflict_resolution` may be able to determine that "that was inevitable when we picked version 1 of X"
220
+ /// which was inevitable when picked W and ... and version 1 of B, which was depended on by version 1 of A.
221
+ /// Therefore the root cause may simplify all the way down to "we cannot pick version 1 of A".
222
+ /// This will prevent us going down this path again. However when we start looking at version 2 of A,
223
+ /// and discover that it depends on version 2 of B, we will want to prioritize the chain of intermediate steps
224
+ /// to confirm if it has a problem with the same shape.
225
+ /// The `satisfier_causes` argument keeps track of these intermediate steps so that the caller can use.
198
226
#[ allow( clippy:: type_complexity) ]
199
227
#[ cold]
200
228
fn conflict_resolution (
201
229
& mut self ,
202
230
incompatibility : IncompDpId < DP > ,
231
+ satisfier_causes : & mut SmallVec < ( Id < DP :: P > , IncompDpId < DP > ) > ,
203
232
) -> Result < ( Id < DP :: P > , IncompDpId < DP > ) , IncompDpId < DP > > {
204
233
let mut current_incompat_id = incompatibility;
205
234
let mut current_incompat_changed = false ;
@@ -223,6 +252,7 @@ impl<DP: DependencyProvider> State<DP> {
223
252
previous_satisfier_level,
224
253
) ;
225
254
log:: info!( "backtrack to {:?}" , previous_satisfier_level) ;
255
+ satisfier_causes. push ( ( package, current_incompat_id) ) ;
226
256
return Ok ( ( package, current_incompat_id) ) ;
227
257
}
228
258
SatisfierSearch :: SameDecisionLevels { satisfier_cause } => {
@@ -234,6 +264,7 @@ impl<DP: DependencyProvider> State<DP> {
234
264
) ;
235
265
log:: info!( "prior cause: {}" , prior_cause. display( & self . package_store) ) ;
236
266
current_incompat_id = self . incompatibility_store . alloc ( prior_cause) ;
267
+ satisfier_causes. push ( ( package, current_incompat_id) ) ;
237
268
current_incompat_changed = true ;
238
269
}
239
270
}
0 commit comments