Idea: version aware trait solving

Today, #[stable(…)] or #[unstable(…)] attributes on trait impls are no-op and all trait impls are "insta-stable". However, it often causes type inference regressions.

Here, I propose a way to solve the long-standing issue above in a generic manner, without affecting the semantics of the language.

Consider cost of each trait impl as stabilization version or 0 if no stabilization attribute present. Then define cost of a solution (of a trait resolution) as the maximum of the costs of the used impl s. We want to find a solution with minimal cost solution. If there are multiple solutions with the same minimum cost, then it is an error.

Feedback welcome. Although I'm presenting there in the RFC format, this is just an idea yet and no way I can complete this as a RFC at this moment. Probably I'm missing something big so please pointing out such issues me.

Like the example says, "User code written before 1.35.0". So the compiler needs to know this information in some way — will there be a tag for this in Cargo.toml. intended-rust-version = "1.34"? Or reusing the minimum required version tag from a previous RFC, rust = "1.34"? However, the min version tag was not intended to disallow selective conditional use of newer features.

No, user doesn't specify Rust version. Let me explain by an example.

When a user wrote the code before 1.35.0, std only had:

//! say, std-1.30.0
#[stable(since = "1.0.0")] ... // impl1
// impl2 missing

At this time, the user code is completely correct and no ambiguity are there. The user had no knowledge about impl2 or any future version at this time.

Then, the user wants to compile the code in newer rust version,

//! std-1.35.0
#[stable(since = "1.0.0")] ... // impl1
#[stable(since = "1.35.0")] ... // impl2

Before this RFC, user code cannot be compiled due to ambiguity between impl1 and impl2. User couldn't foresee this in any way.

By this RFC, when rustc-1.35.0 tries to compile the code, two potential solutions are considered: using impl1 and using impl2. The cost of using impl1 is 1.0.0 whereas the cost of using impl2 is 1.35.0. The minimum cost solution is the solution using impl1 with cost 1.0.0. Because there are no other solutions with equal cost, the compiler can infer that the code wants to use impl1.

When code is written is not considered. User can write the same code after 1.35.0 came out. In which case, an otherwise ambiguous code is accepted. Probably a warning should be issued. I'll add this.

So the plan is summarized by "when multiple trait impls apply, choose the one that has been stable the longest"?

For completeness, the reference guide probably needs to consider the case where trait methods are stabilized after the trait impl and when multiple applicable traits are in scope providing the method.

Only std has stable attributes -- what about external conflicts? e.g. Itertools::flatten was fine until Iterator::flatten came along.

1 Like

So the plan is summarized by "when multiple trait impls apply, choose the one that has been stable the longest"?

Yes, it summarizes my idea very well.

For completeness, the reference guide probably needs to consider the case where trait methods are stabilized after the trait impl and when multiple applicable traits are in scope providing the method.

I only considered stabilization attribute on trait impls and stabilization of individual methods are not considered. Probably it can be generalized to considering individual methods but not completely sure on the interaction. I'll add this in unresolved question, however.

In theory stable attributes can be applied to other crates but, in order to be usable, its "version" has to crate version, not rustc version. Is there an existing effort on such generalization of stability attributes? I'm not aware.

Therefore, that is definitely a future possibility but not in the scope of my RFC.

One area where this scoring/prioritization could be useful is for adding array iterators, but this would have to interact with deref method resolution too.

That is, today calling my_array.into_iter() does an auto-deref/ref to call that method on its slice, iterating by reference. (You should just use .iter() instead.) If we add IntoIterator for arrays, that call would now resolve to the new thing, consuming the array and iterating by value. If we could somehow deprioritize the new one, maybe we could avoid any breaking change.