Skip to content

Commit f2d4a62

Browse files
authored
Add section about current PubGrub limitations (pubgrub-rs#3)
* Intro to PubGrub limitations * Tweak limitations intro * Prepare section for optional dependencies * Complete the optional_deps section * Update PackageName in the code examples for optional deps * Start explanation for multiple versions * Typos and consistency * Less ambiguous proxi example * Change solution to multiple example * Complete the multiple_versions section * Add a section about continuous version space * Complete the prerelease section * Typos and adjustments to continuous_versions * Typos and adjustments to intro * Typos and adjustments to multiple_versions * Proxy and proxies * Typos and text adjustments * Joking ;) * Mention dart prerelease issue * Adjust section on pre-releases
1 parent 2c029ea commit f2d4a62

File tree

6 files changed

+660
-0
lines changed

6 files changed

+660
-0
lines changed

src/SUMMARY.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@
88
- [Strategical decision making in a DependencyProvider](./pubgrub_crate/strategy.md)
99
- [Solution and error reporting](./pubgrub_crate/solution.md)
1010
- [Writing your own error reporting logic](./pubgrub_crate/custom_report.md)
11+
- [Advanced usage and limitations](./limitations/intro.md)
12+
- [Optional dependencies](./limitations/optional_deps.md)
13+
- [Allowing multiple versions of a package](./limitations/multiple_versions.md)
14+
- [Versions in a continuous space](./limitations/continuous_versions.md)
15+
- [Pre-release versions](./limitations/prerelease_versions.md)
1116
- [Internals of the PubGrub algorithm](./internals/intro.md)
1217
- [Overview of the algorithm](./internals/overview.md)
1318
- [Terms](./internals/terms.md)
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
# Versions in a continuous space
2+
3+
The current design of pubgrub exposes a `Version` trait demanding two properties, (1) that there exists a lowest version, and (2) that versions live in a discrete space where the successor of each version is known.
4+
So versions are basically isomorph with N^n, where N is the set of natural numbers.
5+
6+
## The successor design
7+
8+
There is a good reason why we started with the successor design for the `Version` trait.
9+
When building knowledge about the dependency system, pubgrub needs to compare sets of versions, and to perform common set operations such as intersection, union, inclusion, comparison for equality etc.
10+
In particular, given two sets of versions S1 and S2, it needs to be able to answer if S1 is a subset of S2 (S1 ⊂ S2).
11+
And we know that S1 ⊂ S2 if and only if S1 ∩ S2 == S1.
12+
So checking for subsets can be done by checking for the equality between two sets of versions.
13+
Therefore, **sets of versions need to have unique canonical representations to be comparable**.
14+
15+
We have the interesting property that we require `Version` to have a total order.
16+
As a consequence, the most adequate way to represent sets of versions with a total order, is to use a sequence of non intersecting segments, such as `[0, 3] ∪ ]5, 9[ ∪ [42, +∞[`.
17+
18+
> Notation: we note segments with close or open brackets depending on if the value at the frontier is included or excluded of the interval.
19+
> It is also common to use a parenthesis for open brackets.
20+
> So `[0, 14[` is equivalent to `[0, 14)` in that other notation.
21+
22+
The previous set is for example composed of three segments,
23+
- the closed segment [0, 3] containing versions 0, 1, 2 and 3,
24+
- the open segment ]5, 9[ containing versions 6, 7 and 8,
25+
- the semi-open segment [42, +∞[ containing all numbers above 42.
26+
27+
For the initial design, we did not want to have to deal with close or open brackets on both interval bounds.
28+
Since we have a lowest version, the left bracket of segments must be closed to be able to contain that lowest version.
29+
And since `Version` does not impose any upper bound, we need to use open brackets on the right side of segments.
30+
So our previous set thus becomes: `[0, ?[ ∪ [?, 9[ ∪ [42, +∞[`.
31+
But the question now is what do we use in place of the 3 in the first segment and in place of the 5 in the second segment.
32+
This is the reason why we require the `bump()` method on the `Version` trait.
33+
If we know the next version, we can replace 3 by bump(3) == 4, and 5 by bump(5) == 6.
34+
We finally get the following representation `[0, 4[ ∪ [6, 9[ ∪ [42, +∞[`.
35+
And so the `Range` type is defined as follows.
36+
37+
```rust
38+
pub struct Range<V: Version> {
39+
segments: Vec<Interval<V>>,
40+
}
41+
type Interval<V> = (V, Option<V>);
42+
// set = [0, 4[ ∪ [6, 9[ ∪ [42, +∞[
43+
let set = vec![(0, Some(4)), (6, Some(9)), (42, None)];
44+
```
45+
46+
## The bounded interval design
47+
48+
We may want however to have versions live in a continuous space.
49+
For example, if we want to use fractions, we can always build a new fraction between two others.
50+
As such it is impossible to define the successor of a fraction version.
51+
52+
We are currently investigating the use of bounded intervals to enable continuous spaces for versions.
53+
If it happens, this will only be in the next major release of pubgrub, probably 0.3.
54+
The current experiments look like follows.
55+
56+
```rust
57+
/// New trait for versions.
58+
/// Bound is core::ops::Bound.
59+
pub trait Version: Clone + Ord + Debug + Display {
60+
/// Returns the minimum version.
61+
fn minimum() -> Bound<Self>;
62+
/// Returns the maximum version.
63+
fn maximum() -> Bound<Self>;
64+
}
65+
66+
/// An interval is a bounded domain containing all values
67+
/// between its starting and ending bounds.
68+
/// RangeBounds is core::ops::RangeBounds.
69+
pub trait Interval<V>: RangeBounds<V> + Debug + Clone + Eq + PartialEq {
70+
/// Create an interval from its starting and ending bounds.
71+
/// It's the caller responsability to order them correctly.
72+
fn new(start_bound: Bound<V>, end_bound: Bound<V>) -> Self;
73+
}
74+
75+
/// The new Range type is composed of bounded intervals.
76+
pub struct Range<I> {
77+
segments: Vec<I>,
78+
}
79+
```
80+
81+
It is certain though that the flexibility of enabling usage of continuous spaces will come at a performance price.
82+
We just have to evaluate how much it costs and if it is worth sharing a single implementation, or having both a discrete and a continuous implementation.

src/limitations/intro.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
# Advanced usage and limitations
2+
3+
By design, the current implementation of PubGrub is rather well suited to handle a dependency system with the following constraints:
4+
5+
1. Packages are uniquely identified.
6+
2. Versions are in a discrete set, with a total order.
7+
3. The successor of a given version is always uniquely defined.
8+
4. Dependencies of a package version are fixed.
9+
5. Exactly one version must be selected per package depended on.
10+
11+
The fact that packages are uniquely identified (1) is perhaps the only constraint that makes sense for all common dependency systems.
12+
But for the rest of the constraints, they are all inadequate for some common real-world dependency systems.
13+
For example, it's possible to have dependency systems where order is not required for versions (2).
14+
In such systems, dependencies must be specified with exact sets of compatible versions, and bounded ranges make no sense.
15+
Being able to uniquely define the successor of any version (3) is also a constraint that is not a natural fit if versions have a system of pre-releases.
16+
Indeed, what is the successor of `2.0.0-alpha`?
17+
We can't tell if that is `2.0.0` or `2.0.0-beta` or `2.0.0-whatever`.
18+
Having fixed dependencies (4) is also not followed in programming languages allowing optional dependencies.
19+
In Rust packages, optional dependencies are called "features" for example.
20+
Finally, restricting solutions to only one version per package (5) is also too constraining for dependency systems allowing breaking changes.
21+
In cases where packages A and B both depend on different ranges of package C, we sometimes want to be able to have a solution where two versions of C are present, and let the compiler decide if their usages of C in the code are compatible.
22+
23+
In the following subsections, we try to show how we can circumvent those limitations with clever usage of dependency providers.
Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
# Allowing multiple versions of a package
2+
3+
One of the main goals of PubGrub is to pick one version per package depended on, under the assumption that at most one version per package is allowed.
4+
Enforcing this assumption has two advantages.
5+
First, it prevents data and functions interactions of the same library at different, potentially incompatible versions.
6+
Second, it reduces the code size and therefore also the disk usage and the compilation times.
7+
8+
However, under some circonstances, we may relax the "single version allowed" constraint.
9+
Imagine that our package "a" depends on "b" and "c", and "b" depends on "d" @ 1, and "c" depends on "d" @ 2, with versions 1 and 2 of "d" incompatible with each other.
10+
With the default rules of PubGrub, that system has no solution and we cannot build "a".
11+
Yet, if our usages of "b" and "c" are independant with regard to "d", we could in theory build "b" with "d" @ 1 and build "c" with "d" @ 2.
12+
13+
Such situation is sufficiently common that most package managers allow multiple versions of a package.
14+
In Rust for example, multiple versions are allowed as long as they cross a major frontier, so 0.7 and 0.8 or 2.0 and 3.0.
15+
So the question now is can we circumvent this fundamental restriction of PubGrub?
16+
The short answer is yes, with buckets and proxies.
17+
18+
## Buckets
19+
20+
We saw that implementing optional dependencies required the creation of on-demand feature packages, which are virtual packages created for each optional feature.
21+
In order to allow for multiple versions of the same package to coexist, we are also going to play smart with packages.
22+
Indeed, if we want two versions to coexist, there is only one possibility, which is that they come from different packages.
23+
And so we introduce the concept of buckets.
24+
A package bucket is a set of versions of the same package that cannot coexist in the solution of a dependency system, basically just like before.
25+
So for Rust crates, we can define one bucket per major frontier, such as one bucket for 0.1, one for 0.2, ..., one for 1.0, one for 2.0, etc.
26+
As such, versions 0.7.0 and 0.7.3 would be in the same bucket, but 0.7.0 and 0.8.0 would be in different buckets and could coexist.
27+
28+
Are buckets enought? Not quite, since once we introduce buckets, we also need a way to depend on multiple buckets alternatively.
29+
Indeed, most packages should have dependencies on a single bucket, because it doesn't make sense to depend on potentially incompatible versions at the same time.
30+
But rarely, dependencies are written such that they can depend on multiple buckets, such as if one write `v >= 2.0`.
31+
Then, any version from the 2.0 bucket would satisfy it, as well as any version from the 3.0 or any other later bucket.
32+
Thus, we cannot write "a depends on b in bucket 2.0".
33+
So how do we write dependencies of "a"?
34+
That's where we introduce the concept of proxies.
35+
36+
## Proxies
37+
38+
A proxy package is an intermediate on-demand package, placed just between one package and one of its dependencies.
39+
So if we need to express that package "a" has a dependency on package "b" for different buckets, we create the intermediate proxy package "a->b".
40+
Then we can say instead that package "a" depends on any version of the proxy package "a->b".
41+
And then, we create one proxy version for each bucket.
42+
So if there exists the following five buckets for "b", 0.1, 0.2, 1.0, 2.0, 3.0, we create five corresponding versions for the proxy package "a->b".
43+
And since our package "a" depends on any version of the proxy package "a->b", it will be satisfied as soon as any bucket of "b" has a version picked.
44+
45+
## Example
46+
47+
We will consider versions in the form `major.minor` with a major component starting at 1, and a minor component starting at 0.
48+
The smallest version is 1.0, and each major component represents a bucket.
49+
50+
> Note that we could start versions at 0.0, but since different dependency system tends to interpret versions before 1.0 differently, we will simply avoid that problem by saying versions start at 1.0.
51+
52+
For convenience, we will use a string notation for proxies and buckets.
53+
Buckets will be indicated by a "#", so "a#1" is the 1.0 bucket of package "a", and "a#2" is the 2.0 bucket of package "a".
54+
And we will use "@" to denote exact versions, so "a" @ 1.0 means package "a" at version 1.0.
55+
Proxies will be represented by the arrow "->" as previously mentioned.
56+
Since a proxy is tied to a specific version of the initial package, we also use the "@" symbol in the name of the proxy package.
57+
For example, if "a" @ 1.4 depends on "b", we create the proxy package "a#[email protected]>b".
58+
It's a bit of a mouthful, but once you learn how to read it, it makes sense.
59+
60+
> Note that we might be tempted to just remove the version part of the proxy, so "a#1->b" instead of "a#[email protected]>b".
61+
> However, we might have "a" @ 1.4 depending on "b" in range `v >= 2.2` and have "a" @ 1.5 depending on "b" in range `v >= 2.6`.
62+
> Both dependencies would map to the same "a#1->b" proxy package, but we would not know what to put in the dependency of "a#1->b" to the "b#2" bucket.
63+
> Should it be "2.2 <= v < 3.0" as in "a" @ 1.4, or should it be "2.6 <= v < 3.0" as in "a" @ 1.5?
64+
> That is why each proxy package is tied to an exact version of the source package.
65+
66+
Let's consider the following example, with "a" being our root package.
67+
- "a" @ 1.4 depends on "b" in range `1.1 <= v < 2.9`
68+
- "b" @ 1.3 depends on "c" at version `1.1`
69+
- "b" @ 2.7 depends on "d" at version `3.1`
70+
71+
Using buckets and proxies, we can rewrite this dependency system as follows.
72+
- "a#1" @ 1.4 depends on "a#[email protected]>b" at any version (we use the proxy).
73+
- "a#[email protected]>b" proxy exists in two versions, one per bucket of "b".
74+
- "a#[email protected]>b" @ 1.0 depends on "b#1" in range `1.1 <= v < 2.0` (the first bucket intersected with the dependency range).
75+
- "a#[email protected]>b" @ 2.0 depends on "b#2" in range `2.0 <= v < 2.9` (the second bucket intersected with the dependency range).
76+
- "b#1" @ 1.3 depends on "c#1" at version `1.1`.
77+
- "b#2" @ 2.7 depends on "d#3" at version `3.1`.
78+
79+
There are potentially two solutions to that system.
80+
The one with the newest versions is the following.
81+
- "a#1" @ 1.4
82+
- "a#[email protected]>b" @ 2.0
83+
- "b#2" @ 2.7
84+
- "d#3" @ 3.1
85+
86+
Finally, if we want to express the solution in terms of the original packages, we just have to remove the proxy packages from the solution.
87+
88+
## Example implementation
89+
90+
A complete example implementation of this extension allowing multiple versions is available in the [`allow-multiple-versions` crate of the `advanced_dependency_providers` repository][multiple-versions-crate].
91+
In that example, packages are of the type `String` and versions of the type `SemanticVersion` defined in pubgrub, which does not account for pre-releases, just the (Major, Minor, Patch) format of versions.
92+
93+
[multiple-versions-crate]: https://github.com/pubgrub-rs/advanced_dependency_providers/tree/main/allow-multiple-versions
94+
95+
### Defining an index of packages
96+
97+
Inside the `index.rs` module, we define a very basic `Index`, holding all packages known, as well as a helper function `add_deps` easing the writing of tests.
98+
99+
```rust
100+
/// Each package is identified by its name.
101+
pub type PackageName = String;
102+
/// Alias for dependencies.
103+
pub type Deps = Map<PackageName, Range<SemVer>>;
104+
105+
/// Global registry of known packages.
106+
pub struct Index {
107+
/// Specify dependencies of each package version.
108+
pub packages: Map<PackageName, BTreeMap<SemVer, Deps>>,
109+
}
110+
111+
// Initialize an empty index.
112+
let mut index = Index::new();
113+
// Add package "a" to the index at version 1.0.0 with no dependency.
114+
index.add_deps::<R>("a", (1, 0, 0), &[]);
115+
// Add package "a" to the index at version 2.0.0 with a dependency to "b" at versions >= 1.0.0.
116+
index.add_deps("a", (2, 0, 0), &[("b", (1, 0, 0)..)]);
117+
```
118+
119+
### Implementing a dependency provider for the index
120+
121+
Since our `Index` is ready, we now have to implement the `DependencyProvider` trait for it.
122+
As explained previously, we'll need to differenciate packages representing buckets and proxies, so we define the following new `Package` type.
123+
124+
```rust
125+
/// A package is either a bucket, or a proxy between a source and a target package.
126+
pub enum Package {
127+
/// "a#1"
128+
Bucket(Bucket),
129+
/// source -> target
130+
Proxy {
131+
source: (Bucket, SemVer),
132+
target: String,
133+
},
134+
}
135+
136+
/// A bucket corresponds to a given package, and match versions
137+
/// in a range identified by their major component.
138+
pub struct Bucket {
139+
pub name: String, // package name
140+
pub bucket: u32, // 1 maps to the range 1.0.0 <= v < 2.0.0
141+
}
142+
```
143+
144+
In order to implement the first required method, `choose_package_version`, we simply reuse the `choose_package_with_fewest_versions` helper function provided by pubgrub.
145+
That one requires a list of available versions for each package, so we have to create that list.
146+
As explained previously, listing the existing (virtual) versions depend on if the package is a bucket or a proxy.
147+
For a bucket package, we simply need to retrieve the original versions and filter out those outside of the bucket.
148+
149+
```rust
150+
match package {
151+
Package::Bucket(p) => {
152+
let bucket_range = Range::between((p.bucket, 0, 0), (p.bucket + 1, 0, 0));
153+
self.available_versions(&p.name)
154+
.filter(|v| bucket_range.contains(*v))
155+
}
156+
...
157+
```
158+
159+
If the package is a proxy however, there is one version per bucket in the target of the proxy.
160+
161+
```rust
162+
match package {
163+
Package::Proxy { target, .. } => {
164+
bucket_versions(self.available_versions(&target))
165+
}
166+
...
167+
}
168+
169+
/// Take a list of versions, and output a list of the corresponding bucket versions.
170+
/// So [1.1, 1.2, 2.3] -> [1.0, 2.0]
171+
fn bucket_versions(
172+
versions: impl Iterator<Item = SemVer>
173+
) -> impl Iterator<Item = SemVer> { ... }
174+
```
175+
176+
Additionally, we can filter out buckets that are outside of the dependency range in the original dependency leading to that proxy package.
177+
Otherwise it will add wastefull computation to the solver, but we'll leave that out of this walkthrough.
178+
179+
The `get_dependencies` method is slightly hairier to implement, so instead of all the code, we will just show the structure of the function in the happy path, with its comments.
180+
181+
```rust
182+
fn get_dependencies(
183+
&self,
184+
package: &Package,
185+
version: &SemVer,
186+
) -> Result<Dependencies<Package, SemVer>, ...> {
187+
let all_versions = self.packages.get(package.pkg_name());
188+
...
189+
match package {
190+
Package::Bucket(pkg) => {
191+
// If this is a bucket, we convert each original dependency into
192+
// either a dependency to a bucket package if the range is fully contained within one bucket,
193+
// or a dependency to a proxy package at any version otherwise.
194+
let deps = all_versions.get(version);
195+
...
196+
let pkg_deps = deps.iter().map(|(name, range)| {
197+
if let Some(bucket) = single_bucket_spanned(range) {
198+
...
199+
(Package::Bucket(bucket_dep), range.clone())
200+
} else {
201+
...
202+
(proxy, Range::any())
203+
}
204+
})
205+
.collect();
206+
Ok(Dependencies::Known(pkg_deps))
207+
}
208+
Package::Proxy { source, target } => {
209+
// If this is a proxy package, it depends on a single bucket package, the target,
210+
// at a range of versions corresponding to the bucket range of the version asked,
211+
// intersected with the original dependency range.
212+
let deps = all_versions.get(&source.1);
213+
...
214+
let mut bucket_dep = Map::default();
215+
bucket_dep.insert(
216+
Package::Bucket(Bucket {
217+
name: target.clone(),
218+
bucket: target_bucket,
219+
}),
220+
bucket_range.intersection(target_range),
221+
);
222+
Ok(Dependencies::Known(bucket_dep))
223+
}
224+
}
225+
}
226+
227+
/// If the range is fully contained within one bucket,
228+
/// this returns that bucket identifier, otherwise, it returns None.
229+
fn single_bucket_spanned(range: &Range<SemVer>) -> Option<u32> { ... }
230+
```
231+
232+
That's all!
233+
The implementation also contains tests, with helper functions to build them.
234+
Here is the test corresponding to the example we presented above.
235+
236+
```rust
237+
#[test]
238+
/// Example in guide.
239+
fn success_when_simple_version() {
240+
let mut index = Index::new();
241+
index.add_deps("a", (1, 4, 0), &[("b", (1, 1, 0)..(2, 9, 0))]);
242+
index.add_deps("b", (1, 3, 0), &[("c", (1, 1, 0)..(1, 1, 1))]);
243+
index.add_deps("b", (2, 7, 0), &[("d", (3, 1, 0)..(3, 1, 1))]);
244+
index.add_deps::<R>("c", (1, 1, 0), &[]);
245+
index.add_deps::<R>("d", (3, 1, 0), &[]);
246+
assert_map_eq(
247+
&resolve(&index, "a#1", (1, 4, 0)).unwrap(),
248+
&select(&[("a#1", (1, 4, 0)), ("b#2", (2, 7, 0)), ("d#3", (3, 1, 0))]),
249+
);
250+
}
251+
```
252+
253+
Implementing a dependency provider allowing both optional dependencies and multiple versions per package is left to the reader as an exercise (I've always wanted to say that).

0 commit comments

Comments
 (0)