# Version-based visibility to make the language&ecosystem more stable

**URL:** <https://internals.rust-lang.org/t/version-based-visibility-to-make-the-language-ecosystem-more-stable/17124>\
**Category:** language design\
**Created:** [August 3, 2022, 11:09am UTC](https://internals.rust-lang.org/t/version-based-visibility-to-make-the-language-ecosystem-more-stable/17124 "2022-08-03T11:09:38Z")\
**Posts on this page:** 14\
**Page:** 1

<div class="post-metadata">

**Author:** ![Kixunil](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/kixunil/32/4110_2.png) [@Kixunil](https://internals.rust-lang.org/u/Kixunil)\
**Post date:** [August 3, 2022, 11:09am UTC](https://internals.rust-lang.org/t/version-based-visibility-to-make-the-language-ecosystem-more-stable/17124/1 "2022-08-03T11:09:38Z")

</div>

I found two sources of potential API instability that could be solved with similar approach.

## Case 1: added trait method causes breakage

```rust
// in crate A
trait Foo {
    fn do_stuff(&self);
}

// in crate B
trait Bar {
    fn do_something_else(&self);
}

// in crate C, which depends on A and B
use a::Foo;
use b::Bar;

x.do_stuff();
x.do_something_else();

```

Now if either A adds default `fn Foo::do_something_else` or B adds `fn Bar::do_stuff` C breaks due to ambiguity. This is considered minor and can be resolved using UFCS however the breakage is still annoying and UFCS is ugly.

This is even worse for `core` where e.g. `TryFrom`/`TryInto` could not have been added to prelude in old editions because people wrote their own traits to work on stable. Now people have to jump whole edition to have it in prelude. Since `core` is used by everything the likelihood of breakage is high.

## Case 2: functionality got moved to a feature

Crate A has a default feature `foo` which is undesirable in crate B, so crate B specifies `default-features = false }` Crate A wants to make some dependency optional but this dependency was previously used by B. Making the dependency optional but default doesn't work, so the only solution is for A to issue breaking release.

It seems this is most often experienced when crates make `std` optional but there are other cases too.

## Additional annoyance

I work with the newest compiler to have better error messages etc but want to support older MSRV. After I write the code I test with older MSRV and find out I have to rewrite it because I misremembered which features are available in the given MSRV.

This is unrelated but happens to be helped with by the solution!

## Solution

The solution for both would be to make the new crate versions somehow act as if they were older.

### For case 1

Add attribute `#[since = "$version"]` to newly added items in the dependency (A, B in the example). Then if a consumer crate (C in the example) specifies older version of dependency in its `Cargo.toml` those items become invisible. Attempt to use them will cause compilation error with helpful message suggesting to bump the version. In case of `std`&co `stable` attribute is reused and combined with `rust-version` field in `Cargo.toml`. This behavior is only enabled with option `hide-unrequested-features = true` (feel free to bikeshed) to avoid breaking existing things.

This helps with the annoyance above at least when it comes to libs API.

### For case 2

Add optional `default-before` field to features:

```toml
[features]
foo = { default-before = "1.4", deps = ["bar"] }

```

If a crate depends on version 1.3 it will see `foo` feature as always on even when `default-features` is `false`. To truly remove it the crate has to depend on `1.4` or higher.

I imagine this could also reduce many discussions around "this is minor change but let's do crater run; oh breaks x crates, is it too many?" and enable faster library development.

Can you see any way this could be improved even more? Would it be worth adding?

---

<div class="post-metadata">

**Author:** ![197g](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/197g/32/7276_2.png) [@197g](https://internals.rust-lang.org/u/197g)\
**Post date:** [August 3, 2022, 12:00pm UTC](https://internals.rust-lang.org/t/version-based-visibility-to-make-the-language-ecosystem-more-stable/17124/2 "2022-08-03T12:00:30Z")

</div>

I would _love_ the integration of version and function resolution. Better backwards compatibility and a mechanism for actually utilizing the `rust-version` attribute for validating against the feature-set are both good reasons on their own.

The biggest downside is that same story, though. This silences some forward compatibility issues but doesn't help resolve them. Bumping rust-version to larger versions is something that _should_ be trivial. However, this feature allows `std` to silently become incompatible with such upgrades. Consider the motivating example of an added `TryInto`. We've moved the point of failure from the release of Rust `1.70` to the bump _to_ `1.70` in the crate. A compilation breakage that could have been previously considered to cause too much ecosystem breakage could slip through the current crater checks—afterall all the crates which pin their compatibilty still work? But it will take the exact same total toll and dev churn when the ecosystem tries to upgrade beyond the rust version that now contains the changes, the effect is just delayed and stretched to a longer timeframe.

---

<div class="post-metadata">

**Author:** ![Kixunil](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/kixunil/32/4110_2.png) [@Kixunil](https://internals.rust-lang.org/u/Kixunil)\
**Post date:** [August 3, 2022, 12:15pm UTC](https://internals.rust-lang.org/t/version-based-visibility-to-make-the-language-ecosystem-more-stable/17124/3 "2022-08-03T12:15:22Z")

</div>

Yeah, I was thinking about forward compatibility a bit. I think the lint should still be available but I'm not sure if it should be `allow` or `warn` by default.

---

<div class="post-metadata">

**Author:** ![epage](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/epage/32/3171_2.png) [@epage](https://internals.rust-lang.org/u/epage)\
**Post date:** [August 3, 2022, 1:22pm UTC](https://internals.rust-lang.org/t/version-based-visibility-to-make-the-language-ecosystem-more-stable/17124/4 "2022-08-03T13:22:18Z")

</div>

> Add attribute `#[since = "$version"]` to newly added items in the dependency

What I'd like to see

- Expose the `stable` attribute used in the stdlib
- Warn if an item is newer than the bottom of the version requirement (or `rust-version` for stdlib)

> Add optional `default-before` field to features:

There have been several directions people have gone with migrating features

- [Cargo feature migrations by Ericson2314 · Pull Request #3146 · rust-lang/rfcs · GitHub](https://github.com/rust-lang/rfcs/pull/3146)
- [RFC 3283: Backward compatible default features by tbu- · Pull Request #3283 · rust-lang/rfcs · GitHub](https://github.com/rust-lang/rfcs/pull/3283)
- [Ability to disable individual default features · Issue #3126 · rust-lang/cargo · GitHub](https://github.com/rust-lang/cargo/issues/3126#issuecomment-1190785350)

`default-before` proposal is similar to RFV 3146 in that it makes decisions on behavior based on your version req. I'm a bit concerned that changing a version req to a newer, compatible version would be breaking someone's code.

---

<div class="post-metadata">

**Author:** ![mathstuf](https://avatars.discourse-cdn.com/v4/letter/m/958977/32.png) [@mathstuf](https://internals.rust-lang.org/u/mathstuf)\
**Post date:** [August 3, 2022, 2:12pm UTC](https://internals.rust-lang.org/t/version-based-visibility-to-make-the-language-ecosystem-more-stable/17124/5 "2022-08-03T14:12:58Z")

</div>

One question I have is: what happens when resolution ends up forcing something newer anyways?

Say you have crate A that does this and adds some symbol X with `#[since = "1.4"]`. This conflicts with crate B's X in some way, but it has `a = "^1.3"`. Crate C uses X and has `a = "^1.4"`. When compiling A, is X included or not? If it is not, C fails. If it is, B needs some way to hide certain symbols when looking into A based on _its_ requirements (this would require some more information on the command line when using A's `.rlib`).

Whatever the solution, I suspect this needs a `Cargo.lock` update to say something like `"a 1.4.0 (as of 1.3.0)"` or something in the dependency list (obvious bikeshedding opportunity, but I think this info would then be required in the file _somewhere_).

---

<div class="post-metadata">

**Author:** ![Kixunil](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/kixunil/32/4110_2.png) [@Kixunil](https://internals.rust-lang.org/u/Kixunil)\
**Post date:** [August 3, 2022, 3:00pm UTC](https://internals.rust-lang.org/t/version-based-visibility-to-make-the-language-ecosystem-more-stable/17124/6 "2022-08-03T15:00:16Z")

</div>

Great that people are thinking about those! Will check them out.

> I'm a bit concerned that changing a version req to a newer, compatible version would be breaking someone's code.

How could it? If you change it for your own crate you may get compile error but that's because you made an explicit change. You have to either update the code or roll back. Nothing else is affected.

---

<div class="post-metadata">

**Author:** ![Kixunil](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/kixunil/32/4110_2.png) [@Kixunil](https://internals.rust-lang.org/u/Kixunil)\
**Post date:** [August 3, 2022, 3:08pm UTC](https://internals.rust-lang.org/t/version-based-visibility-to-make-the-language-ecosystem-more-stable/17124/7 "2022-08-03T15:08:36Z")

</div>

In your example B doesn't see the symbol X but C does. It could have interesting interactions with re-exports but I think they are solvable. For `*` imports just exclude the new items, ~~for added struct fields, enum variants and (trait) methods just make them inaccessible from C when going through B but accessible directly.~~ Actually, I think only `*` imports are an issue and excluding looks simple.

> B needs some way to hide certain symbols when looking into A based on _its_ requirements (this would require some more information on the command line when using A's `.rlib`)

Yep, adding an argument for this seems fine to me.

> `Cargo.lock` update

Does `cargo` guarantee it only reads `Cargo.lock` in some circumstances? I don't remember such guarantee and if it doesn't it can just read `Cargo.toml` to get the required information.

---

<div class="post-metadata">

**Author:** ![epage](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/epage/32/3171_2.png) [@epage](https://internals.rust-lang.org/u/epage)\
**Post date:** [August 3, 2022, 3:17pm UTC](https://internals.rust-lang.org/t/version-based-visibility-to-make-the-language-ecosystem-more-stable/17124/8 "2022-08-03T15:17:55Z")

</div>

> [@Kixunil](#):
>
> How could it? If you change it for your own crate you may get compile error but that's because you made an explicit change. You have to either update the code or roll back. Nothing else is affected.

As of today, upgrading version reqs that are semver compatible is a safe operation (pending regressions). I am very loathe for us to deviate from that.

Besides just the breaking, the rest of the story isn't that great. The upgrade instructions need to be inferred from reading your dependency's `Cargo.toml`. We could make `cargo upgrade` handle that for you but you shouldn't have to use the tool to do upgrades and you shouldn't have to dig into source or even changelogs to upgrade within semver compatible versions.

---

<div class="post-metadata">

**Author:** ![Kixunil](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/kixunil/32/4110_2.png) [@Kixunil](https://internals.rust-lang.org/u/Kixunil)\
**Post date:** [August 3, 2022, 4:32pm UTC](https://internals.rust-lang.org/t/version-based-visibility-to-make-the-language-ecosystem-more-stable/17124/9 "2022-08-03T16:32:40Z")

</div>

Upgrading being "safe" depends entirely on whether the dependency copied its policy from `std` ("A bunch of really smart people behind `std` chose this policy so it must be good, right?"). Per Rust policy adding a default method to a trait is minor change so breakage absolutely can already happen! Even worse, it doesn't even require `Cargo.toml` update, it could be update of `Cargo.lock` out of habit or because one wants to use new stuff in an unrelated crate that depends on the same crate. My proposal strictly improves this situation by limiting the breakage to explicit `Cargo.toml` update.

Updating `Cargo.toml` should be only done when one actually needs to use a feature. Updating it without a reason is harmful. If one's going to touch the code to use a new feature anyway, resolving the breakage should not be a big issue. Note that these breakages are very rare anyway and also trivial to fix: just change call to UFCS. Using an official tool doesn't seem bad, `cargo` itself is an official tool.

Of course there might be crates that hate breakages and would bump major even in case of added method. I strongly believe this would be much worse than changing a few calls. I already experience major PITA when dealing with dependencies that change major versions. Libcpocalypse comes to mind as an infamous example of widespread breakage. If the language can be modified to reduce these cases even imperfectly it's much better than the alternative.

Finally, I think that additions to `std` not being breaking at all (at lesat if you have `rust-version` specified) could be a great additional argument towards convincing Linux distros to make an exception for Rust and update a bit faster (they could even make `rust-version` mandatory in their own packages to make it nearly perfect). Given recent MSRV discussions, this is also great and worth it.

---

<div class="post-metadata">

**Author:** ![Nemo157](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/nemo157/32/11585_2.png) [@Nemo157](https://internals.rust-lang.org/u/Nemo157)\
**Post date:** [August 3, 2022, 7:07pm UTC](https://internals.rust-lang.org/t/version-based-visibility-to-make-the-language-ecosystem-more-stable/17124/10 "2022-08-03T19:07:22Z")

</div>

I don't see any reason this would need to be in the `Cargo.lock`. It seems to be possible simply as an additional flag giving the version requirement of all dependencies to the crate when it's building.

I assume this is going to only be supported when using a simple `^` version requirement? It likely isn't possible for `#[stable]` attributes to work correctly across major versions if you have a version requirement like `> 1.1.2, < 3.0.0`

---

<div class="post-metadata">

**Author:** ![epage](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/epage/32/3171_2.png) [@epage](https://internals.rust-lang.org/u/epage)\
**Post date:** [August 3, 2022, 7:12pm UTC](https://internals.rust-lang.org/t/version-based-visibility-to-make-the-language-ecosystem-more-stable/17124/11 "2022-08-03T19:12:15Z")

</div>

The likelihood of a minor incompatibility breaking someone is relatively low such that people regularly update all dependencies.

As for not touching `Cargo.toml`, you might want to catch up on the [cargo upgrade discussion](https://internals.rust-lang.org/t/feedback-on-cargo-upgrade-to-prepare-it-for-merging/17101) as we are looking to make `cargo update --save` to update all version requirements.

---

<div class="post-metadata">

**Author:** ![8573](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/8573/32/4913_2.png) [@8573](https://internals.rust-lang.org/u/8573)\
**Post date:** [August 3, 2022, 9:23pm UTC](https://internals.rust-lang.org/t/version-based-visibility-to-make-the-language-ecosystem-more-stable/17124/12 "2022-08-03T21:23:56Z")

</div>

> [@197g](#):
>
> This silences some forward compatibility issues but doesn't help resolve them. Bumping rust-version to larger versions is something that _should_ be trivial. However, this feature allows `std` to silently become incompatible with such upgrades. Consider the motivating example of an added `TryInto`. We've moved the point of failure from the release of Rust `1.70` to the bump _to_ `1.70` in the crate. A compilation breakage that could have been previously considered to cause too much ecosystem breakage could slip through the current crater checks—afterall all the crates which pin their compatibilty still work?

How much would this be mitigated by testing crates, whether in their CI or in Crater, both in an MSRV mode and in a mode that uses latest stable or newer?

---

<div class="post-metadata">

**Author:** ![Kixunil](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/kixunil/32/4110_2.png) [@Kixunil](https://internals.rust-lang.org/u/Kixunil)\
**Post date:** [August 4, 2022, 11:55am UTC](https://internals.rust-lang.org/t/version-based-visibility-to-make-the-language-ecosystem-more-stable/17124/13 "2022-08-04T11:55:26Z")

</div>

For `> x, < y` supplying `x` should be correct.

@8573 there would have to be some special lint level and you'd have to set it properly but it should completely resolve the issue.

---

<div class="post-metadata">

**Author:** ![system](https://sea2.discourse-cdn.com/flex002/user_avatar/internals.rust-lang.org/system/32/14092_2.png) [@system](https://internals.rust-lang.org/u/system)\
**Post date:** [November 2, 2022, 11:55am UTC](https://internals.rust-lang.org/t/version-based-visibility-to-make-the-language-ecosystem-more-stable/17124/14 "2022-11-02T11:55:51Z")

</div>

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.
