Skip to content

Commit fb86625

Browse files
committed
Upgrade the version solver's algorithm
This introduces a new algorithm I call "Pubgrub" which is based on cutting-edge techniques for NP-hard search problems. It should substantially reduce the number of cases where the version solver goes exponential, and make it much easier to provide thorough error messages when no solution is available. This commit adds the core of the algorithm, but it's not yet feature- complete with the old version solver. I intend to add support for features like locked dependencies, downgrading, and so on in follow-up CLs. It also doesn't have any kind of error handling yet. As such, pub's test suite is not expected to pass. See #912
1 parent 0b9f548 commit fb86625

15 files changed

+1900
-1345
lines changed

doc/solver.md

+710
Large diffs are not rendered by default.

lib/src/package_name.dart

+27-6
Original file line numberDiff line numberDiff line change
@@ -48,12 +48,6 @@ abstract class PackageName {
4848
description = null,
4949
isMagic = true;
5050

51-
String toString() {
52-
if (isRoot) return "$name (root)";
53-
if (isMagic) return name;
54-
return "$name from $source";
55-
}
56-
5751
/// Returns a [PackageRef] with this one's [name], [source], and
5852
/// [description].
5953
PackageRef toRef() => isMagic
@@ -82,6 +76,9 @@ abstract class PackageName {
8276
source.hashCode ^
8377
source.hashDescription(description);
8478
}
79+
80+
/// Like [toString], but leaves off some information for extra terseness.
81+
String toTerseString();
8582
}
8683

8784
/// A reference to a [Package], but not any particular version(s) of it.
@@ -98,6 +95,18 @@ class PackageRef extends PackageName {
9895
/// Creates a reference to a magic package (see [isMagic]).
9996
PackageRef.magic(String name) : super._magic(name);
10097

98+
String toString() {
99+
if (isRoot) return "$name (root)";
100+
if (isMagic) return name;
101+
return "$name from $source";
102+
}
103+
104+
String toTerseString() {
105+
if (isMagic) return name;
106+
if (isRoot || source.name != 'hosted') return "$name";
107+
return "$name from $source";
108+
}
109+
101110
bool operator ==(other) => other is PackageRef && samePackage(other);
102111
}
103112

@@ -146,6 +155,12 @@ class PackageId extends PackageName {
146155
if (isMagic) return name;
147156
return "$name $version from $source";
148157
}
158+
159+
String toTerseString() {
160+
if (isMagic) return name;
161+
if (isRoot || source.name == 'hosted') return "$name $version";
162+
return "$name $version from $source";
163+
}
149164
}
150165

151166
/// A reference to a constrained range of versions of one package.
@@ -214,6 +229,12 @@ class PackageRange extends PackageName {
214229
return "$prefix ($description)";
215230
}
216231

232+
String toTerseString() {
233+
if (isMagic) return name;
234+
if (isRoot || source.name == 'hosted') return "$name $constraint";
235+
return "$name $constraint from $source";
236+
}
237+
217238
/// Returns a new [PackageRange] with [features] merged with [this.features].
218239
PackageRange withFeatures(Map<String, FeatureDependency> features) {
219240
if (features.isEmpty) return this;

lib/src/solver/assignment.dart

+26
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import '../package_name.dart';
6+
import 'incompatibility.dart';
7+
import 'term.dart';
8+
9+
/// A term in a [PartialSolution] that tracks some additional metadata.
10+
class Assignment extends Term {
11+
/// The number of decisions at or before this in the [PartialSolution] that
12+
/// contains it.
13+
final int decisionLevel;
14+
15+
/// The index of this assignment in [PartialSolution.assignments].
16+
final int index;
17+
18+
/// The incompatibility that caused this assignment to be derived, or `null`
19+
/// if it isn't a derivation.
20+
final Incompatibility cause;
21+
22+
Assignment(
23+
PackageName package, bool isPositive, this.decisionLevel, this.index,
24+
{this.cause})
25+
: super(package, isPositive);
26+
}

lib/src/solver/cache.dart

-120
This file was deleted.

lib/src/solver/incompatibility.dart

+97
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
// Copyright (c) 2017, the Dart project authors. Please see the AUTHORS file
2+
// for details. All rights reserved. Use of this source code is governed by a
3+
// BSD-style license that can be found in the LICENSE file.
4+
5+
import '../package_name.dart';
6+
import 'term.dart';
7+
8+
/// A set of mutually-incompatible terms.
9+
///
10+
/// See https://github.com/dart-lang/pub/tree/master/doc/solver.md#incompatibility.
11+
class Incompatibility {
12+
/// The mutually-incompatibile terms.
13+
final List<Term> terms;
14+
15+
/// Creates an incompatibility with [terms].
16+
///
17+
/// This normalized [terms] so that each package has at most one term
18+
/// referring to it.
19+
factory Incompatibility(List<Term> terms) {
20+
if (terms.length == 1 ||
21+
// Short-circuit in the common case of a two-term incompatibility with
22+
// two different packages (for example, a dependency).
23+
(terms.length == 2 &&
24+
terms.first.package.name != terms.last.package.name)) {
25+
return new Incompatibility._(terms);
26+
}
27+
28+
// Coalesce multiple terms about the same package if possible.
29+
var byName = <String, Map<PackageRef, Term>>{};
30+
for (var term in terms) {
31+
var byRef = byName.putIfAbsent(term.package.name, () => {});
32+
var ref = term.package.toRef();
33+
if (byRef.containsKey(ref)) {
34+
byRef[ref] = byRef[ref].intersect(term);
35+
36+
// If we have two terms that refer to the same package but have a null
37+
// intersection, they're mutually exclusive, making this incompatibility
38+
// irrelevant, since we already know that mutually exclusive version
39+
// ranges are incompatible. We should never derive an irrelevant
40+
// incompatibility.
41+
assert(byRef[ref] != null);
42+
} else {
43+
byRef[ref] = term;
44+
}
45+
}
46+
47+
return new Incompatibility._(byName.values.expand((byRef) {
48+
// If there are any positive terms for a given package, we can discard
49+
// any negative terms.
50+
var positiveTerms =
51+
byRef.values.where((term) => term.isPositive).toList();
52+
if (positiveTerms.isNotEmpty) return positiveTerms;
53+
54+
return byRef.values;
55+
}).toList());
56+
}
57+
58+
Incompatibility._(this.terms);
59+
60+
String toString() {
61+
if (terms.length == 1) {
62+
var term = terms.single;
63+
return "${term.package.toTerseString()} is "
64+
"${term.isPositive ? 'forbidden' : 'required'}";
65+
}
66+
67+
if (terms.length == 2) {
68+
var term1 = terms.first;
69+
var term2 = terms.last;
70+
if (term1.isPositive != term2.isPositive) {
71+
var positive = (term1.isPositive ? term1 : term2).package;
72+
var negative = (term1.isPositive ? term2 : term1).package;
73+
return "if ${positive.toTerseString()} then ${negative.toTerseString()}";
74+
} else if (term1.isPositive) {
75+
return "${term1.package.toTerseString()} is incompatible with "
76+
"${term2.package.toTerseString()}";
77+
} else {
78+
return "either ${term1.package.toTerseString()} or "
79+
"${term2.package.toTerseString()}";
80+
}
81+
}
82+
83+
var positive = <String>[];
84+
var negative = <String>[];
85+
for (var term in terms) {
86+
(term.isPositive ? positive : negative).add(term.package.toTerseString());
87+
}
88+
89+
if (positive.isNotEmpty && negative.isNotEmpty) {
90+
return "if ${positive.join(' and ')} then ${negative.join(' and ')}";
91+
} else if (positive.isNotEmpty) {
92+
return "one of ${positive.join(' or ')} must be false";
93+
} else {
94+
return "one of ${negative.join(' or ')} must be true";
95+
}
96+
}
97+
}

0 commit comments

Comments
 (0)