Type inference breakage in 1.80 has not been handled well

That would be a good idea. The crate is supposed to compile without those APIs, that's what the option means, so this would both significantly reduce breakage in future, and better test the package.rust-version actually holds.

In my experience, C++ code stays on older version usually either because a newer compiler is not yet available for the target, or because updating the toolchain is a lot of work and nobody has time to do it.

Rust having one reference compiler that works for most target avoids the first problem, but every time code does not compile with new version that did compile with the previous one, for whatever reason, like just happened with 1.80, creates the second problem. It will make people hesitant to update their toolchain, especially on bigger projects, because there any breakage disrupts work of a lot of people. Therefore anything that prevents such breakage improves adoption of new features.

There is a difference between attaching this to minimum supported rust version and to edition though. Minimum supported rust version is the right choice—the crate is intended to work without those APIs anyway—while edition is the wrong choice—editions are mainly about language changes and interface changes can be independent of those.

8 Likes

It's an interesting point that it resembles edition in some way, however it's not the same thing.

Edition forces MSRV upgrade and all modifications required to make code work (e.g. rename keywords), this doesn't. Also as @CAD97 pointed out requiring edition slow down adoption of new trait impls even though that shouldn't be needed because cod can be written forward-compatibly - that was the entire justification for this break being acceptable.

However unlike the mentioned proposal, I do not want it based on package.rust-version because that prevents crates from using conditional compilation to optionally expand their features based on which Rust version is available. It needs to be a separate field.

4 Likes

+1 to that. package.rust-version is implicitly package.min-supported-rust-version. Inference should be gated via something like package.max-known-rust-version.

6 Likes

Good point about breaks discouraging updates causing crates to want even stricter MSRV. I just don't agree that rust-version is the right choice even though the edition isn't either because many crates #[cfg] the rust version and there's even WIP to have native version-based #[cfg].

That being said it might make sense for libraries if libraries were capable of annotating their APIs because you can't #[cfg] on a library version even though I'd very much like it to be possible because it could increase the span of supported dependencies preventing problems like mentioned in https://www.phoronix.com/news/Debian-Orphans-Bcachefs-Tools

1 Like

There's maybe a space here to pull language-version-dependent features into cargo+rustc proper?

I'm imagining something like in Cargo.toml:

[bikeshed-rustc-version-dependent-features]
uses_const_generics = "1.51"

And then being able to #[cfg(feature = "uses_const_generics")] in rust code.

This makes rustc-version-dependent features more declarative (avoiding the need for build scripts, and perhaps even the rustversion crate at all, and allowing the resolver to give much better and earlier errors about unsupported versions), and gives cargo the knowledge to configure rustc with what trait universes should be in scope based on declarative feature data rather than requiring a hard-coded max-known-rust-version.

(The UI surface of this feature, and whether it needs an opt-in/out because people may still be using rustversion or similar, are very open to discussion, but the core idea may be worth considering)

1 Like

A maximum known rust version is implicit in the timestamps of publication of the crate and rustc. I guess this could be tricky to try to rely on in vendoring and general non-cargo cases, but maybe a technique to keep in the back pocket in case it’s useful in the future.

1 Like

This would imply that as soon as rustc 1.100 comes out, you can no longer submit crates to crates.io with a MKRV of 1.99, which means maintainers have to test with latest-stable immediately. Even the mobile OS vendors give developers a few months before they require everyone to use the latest tools.

1 Like

Likely better than using the timestamp is just to use the version of cargo that uploaded the package. Cargo verifies that (at least the default configuration of) the crate builds when packaging it, and while possible it's unusual to use the MSRV toolchain to do the publish instead of stable. (At least if the package conditionally utilizes newer toolchain functionality, as if it doesn't, using a rustup override for the project so that you always test using the MSRV isn't completely unreasonable. But does give up perf improvements.)

3 Likes

What you're proposing doesn't solve the problem and the problem you're solving is being worked on already - there's a rust-version-based cfg directly that doesn't need any annoying cargo configuration. However it as stalled by people demanding another loosely-related feature for no obvious technical reason.

The issue here is not matter of using cfg - you can already do that. It's the matter of not having to defensively write UFCS everywhere because a new compiler version might break the code. And if you know certainly that some version is going to break the code, you only write UFCS for the offending piece of code and them bump maximum known Rust version.

It's exciting to hear that people are already working on rust-version-based-cfg!

What I was proposing here was a way of inferring rather than needing to manually maintain the maximum known Rust version.

Let's imagine that we know effectively "This crate requires at least rust 1.60, but has features that may be enabled by rust 1.70 and by rust 1.75" (which may be known because it's hard-coded in a Cargo.toml, or may be inferred by rustc by parsing cfg blocks, or some other means), we can decide:

  • If the cargo/rust version is 1.59, this crate is unsupported.
  • If the cargo/rust version is 1.60..=1.69, rustc should bring into scope trait implementations known in rust 1.60 and none which are newer (because maybe 1.68 introduced a conflicting trait, but this code was written with 1.60 in mind).
  • If the cargo/rust version is 1.70..=1.74, bring into scope trait implementations known in rust 1.70 but none newer.
  • If the cargo/rust version is >=1.75, bring into scope trait implementations known in rust 1.75 but none newer.

If we can know "what versions of rust was the crate written knowing about", we can treat the bottom of each of those ranges as the maximum known Rust version as a side effect of knowing what versions were known about at time of writing those version-dependent features, rather than needing to maintain a separate and manually maintained MaxSRV.

1 Like

Wouldn’t a very similar strategy allow to remove the orphan rule? This code is known to compile with version 1.5 of libA and version 2.18 of libB, so bring only trait implementation that do exist in those versions. So it’s fine if you do implement a foreign trait of libA to a foreign type of libB, because upgrading them will not make new trait implementation visible.

Unfortunately, while name resolution is a purely local problem, impl resolution is a global problem. Due to how monomorphization​ works, all code needs to agree on what impl is selected for <Type as Trait>. And since code is allowed to rely on the specific impl refinement of a concrete type, this is a soundness property.

This could theoretically be addressed — in effect, monomorphizing generics not only on the types but also on the caller-selected satisfying impls for any bounds, desugaring bounded quantification — but especially once blanket implementations get introduced things can easily get confusing[1].


  1. A significant portion of the worst surprises (e.g. removing a trivially satisfied trait bound changing behavior) can be avoided by forbidding scoped impls from being blanket impls, but other pitfalls still exist, with the worst ones sharing elements with the pitfalls around unsound specialization potentially on lifetimes. ↩︎

3 Likes