There are a few things I’d like to have clarified to make sure we’re all on the same page.
1. Will each crate specify its own epoch, or will the root project specify the epoch and its dependencies inherit it?
So far, I’m assuming the former, because the latter would imply that libraries would have to be written in such a way that they are compatible with every epoch that they decide to support. If it’s the former, then…
2. What kind of changes can and cannot be made in a epoch?
or the collorary:
2a. If a library updates its epoch, is it (allowed to be) a breaking change for the library (i.e. does the library need to bump its major version)?
If the answer is yes, then library authors need to be aware of this. Bumping the major version means that applications on an older epoch will be able to continue using the older major version of a library without fearing that cargo update
will update the library to a version that is incompatible with the application’s epoch.
I think we already have this issue today with Rust releases: if a library starts using Rust 1.y features without a major version bump, and an application that uses that library is stuck on Rust 1.x, where x < y, then cargo update
will update the library to a version that is not compatible with Rust 1.x and it will not compile. If we eventually logic to cargo update
to not upgrade a library to a version that requires (based on an attribute added in Cargo.toml
, perhaps) a newer version of Rust than the currently installed version, then we could do something similar for epochs to avoid imposing major version bumps on library.
The answer to this question will have an impact on the answer to the next question (specifically scenario 3.2):
3. What is the interaction between crates using different epochs?
There are two scenarios. Let’s suppose application A uses library B.
3.1. Application A uses a newer epoch than library B.
3.2. Application A uses an older epoch than library B.
In scenario 3.1, library B might be using features or syntax that has become deprecated or that changed semantics in the epoch that application A uses. If these changes don’t affect the library’s API (e.g. the proposed change to match
), there’s no issue. But if they do, then in order for this scenario to work, there needs to be an alternate feature or syntax that application A can use in order to use library B.
For example, let’s consider the semantic change to &Trait
. Suppose that library B defines these traits:
pub trait Foo {
pub fn foo(self, x: &Bar);
}
pub trait Bar {}
If application A wants to implement Foo
for one of its types, it will be able to, but it will have to use the new syntax:
impl Foo for MyFoo {
pub fn foo(self, x: &dyn Bar);
}
(If it used &Bar
, it would fail to compile because the declaration wouldn’t be compatible with the trait’s.)
This might be a problem if library B exports a macro that expands to an impl Foo for $x
: presumably, the macro would be written with the old syntax. If you use that macro in application A, which epoch should the expanded code be compiled under? In order to maintain compatibility, we’d have to compile the expanded code with library B’s epoch. I don’t know if that’s easy of even feasible.
Scenario 3.2 is essentially scenario 3.1 backwards, but now instead of worrying about forward compatibility, we’d be worrying about backward compatibility. If new epochs are allowed to introduce features that code using an older epoch cannot consume, then it may force applications to upgrade their epoch in order to keep using the library. Coming back to question 2, if a library updates its epoch and has the consequence of forcing downstream crates to update their epoch, then that’s a breaking change. Now, if the library doesn’t use any of the features that are backwards-incompatible, then it’s not a breaking change. If the library then evolves and adds a new feature that depends on a feature only available in the new epoch, it will force downstream crates to upgrade only if they want to use that feature.