Defining the Rust 2024 edition
In December, the Rust project released a call for proposals for inclusion in the 2024 edition. Rust handles backward incompatible changes by using Editions, which permit projects to specify a single stable edition for their code and allow libraries written in different editions to be linked together. Proposals for Rust 2024 are now in, and have until the end of February to be debated and decided on. Once the proposals are accepted, they have until May to be implemented in time for the 2024 edition to be released in the second half of the year.
Accepted proposals
Some proposals have already been accepted, the largest of which is a change to how functions (and methods) that use "Return Position impl Types" (sometimes also called "-> impl Trait") capture lifetime information. While this proposal is slated for inclusion in Rust 2024, it does leave certain questions open, and may end up not being included if those questions cannot be resolved in time. In Rust, one can write functions that return opaque types that can only be used by invoking methods of traits they implement. LWN covered previous work to improve the consistency of this feature late last year. An example of a function that uses Return Position impl Types is the following function definition that returns some type that implements the Foo trait, without committing itself to a specific type:
fn foo<'a, T>(x: &'a T) -> impl Foo { ... }
One wart with this functionality is the rule for how the inferred hidden type of the function incorporates other types from its surroundings. In the given example, the type that foo() returns can reference the type T. In Rust 2021, however, it cannot reference the lifetime 'a. This asymmetry requires users to introduce awkward contortions in order to correctly write functions that capture references and return them as part of an opaque type. This is especially troublesome in the case of trying to convert an asynchronous function to use the impl Trait syntax, because asynchronous functions can capture lifetime information in this way.
The proposal corrects this asymmetry in the 2024 edition by permitting lifetime
information and types to be captured using the same rules.
The discussion about this
feature proposal
included some concerns about whether this could lead to "overcapturing", where a
lifetime parameter that the user does not intend to be referenced is captured
anyway, restricting how the returned value can be used. The proposal ends up
leaving this question open, saying that these changes to the lifetime-capture
rules should not be included in the final
set of revisions for Rust 2024 unless the community can find "some solution
for precise capturing that will allow all code that is allowed under Rust 2021
to be expressed, in some cases with syntactic changes, in Rust 2024.
"
Two other accepted proposals have to deal with how
Cargo, Rust's package manager, handles dependencies.
One proposal changes how packages specify
optional dependencies. Currently, every
optional dependency of a package implicitly creates a Cargo "feature" —
Rust's solution for conditional compilation — with the
same name. The proposal says that this makes it "easy for crate authors
to use the wrong syntax and be met with errors
" and leads to "confusing
choices when cargo add lists features that look the same
",
because the names of optional dependencies appear alongside features that are
defined in the crate.
In Rust 2024, there will be no externally-visible feature with the same name as
the dependency by default. Instead, enabling an optional dependency will be done
using a feature named
"dep:package-name" instead of "package-name".
The other proposal that impacts dependency handling is designed to make it easier for Rust to automatically detect and warn about changes that may break semantic versioning compatibility guarantees. In current editions of Rust, when one crate depends on another, the first crate can expose types from the second crate as part of its API. The following example, taken from the proposal, shows code that re-exposes types from serde and serde_json:
#[derive(Clone, Debug, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
pub struct Diagnostic {
code: String,
message: String,
file: std::path::PathBuf,
span: std::ops::Range<usize>,
}
impl std::str::FromStr for Diagnostic {
type Err = serde_json::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
serde_json::from_str(s)
}
}
If serde_json were to change the definition of serde_json::Error, then the return type of Diagnostic::from_str() would change, which could potentially be a breaking change. The proposal seeks to remedy this by turning an existing warning (lints.rust.exported_private_dependencies) into an error in the 2024 edition, and giving crates a way to mark dependencies that are deliberately exposed by adding a public flag to Cargo dependency declarations.
The last accepted proposal at the time of writing stands out by not modifying the language itself, but the policies around how future editions will maintain backward compatibility. Rust permits users to define syntactic macros (as opposed to procedural macros) that pattern-match given fragments of Rust syntax, and produce modified Rust code. When Rust adds new pieces of syntax, this impacts the meaning of existing macro definitions. This proposal seeks to clarify the policy around when and how the syntactic macro pattern-matching rules are updated to match new syntax, saying that when the syntax of Rust is changed in such a way that the patterns in syntactic macros no longer align with the actual grammar of the language that the project should: add a new pattern which preserves the behavior of the existing pattern; in the next edition, change the behavior of the original pattern to match the underlying grammar of the release of Rust corresponding to the first release of that edition; and have cargo fix replace all instances of the original pattern with the new pattern which preserves the old behavior.
Proposals under discussion
Currently, there is one unaccepted proposal that is in its final comment period before being approved, a proposal to add support to Cargo for respecting the minimum supported Rust version of a crate. Crates can already declare that they need a certain version of Rust using the rust-version field in the Cargo manifest. This proposal adds a new Cargo resolver that prefers versions of dependencies that have compatible rust-version fields.
Another proposal that has not yet been accepted, but that seems likely to be accepted because it has been part of Rust's overarching vision for some time, is a proposal that would reserve the gen keyword. The new keyword would be analogous to the async keyword, but permit the definition of generators — recently renamed in the internal documentation to coroutines to match use of the term outside of the Rust community — instead of asynchronous functions. It is already possible to write coroutines in nightly Rust, and they are part of how the compiler implements asynchronous functions, but they are not yet stabilized. The proposal doesn't suggest getting them stabilized by May, but only reserving the syntax so that automatic upgrades from Rust 2021 to Rust 2024 can update existing programs to avoid clashing with coroutines when they are introduced.
Migrating code from one edition to the next is always supposed to be possible using cargo fix. For newly reserved keywords, this means rewriting programs which use those keywords as identifiers. For example, "let gen = ...;" would be rewritten to "let r#gen = ...;". This uses a seldom-seen feature of Rust known as raw identifiers, which permit using otherwise reserved words as variable or function names by prefixing them with "r#".
The proposal prompted
a lot of debate about the exact place that this keyword would have in the Rust
parser, and whether it should be changed to reflect the new term coroutine,
eventually prompting Oli Scherer — the contributor who submitted
the proposal — to
declare: "This RFC is on hiatus until we have an
implementation and will then mirror the implementation
".
Work on an
implementation is ongoing.
Another proposal intended to make writing asynchronous code slightly smoother is
adding the
Future and
IntoFuture traits to the
prelude — the section of the standard library available without explicitly
importing a library. Alice Cecile
expressed doubts that this was as useful as
some of the existing functionality in the prelude, saying: "Generally I
find that traits are most useful in preludes when you want to implicitly use
their methods
". She observed that the only method that this proposal would
permit the use of is poll(). The proposal remains open for debate.
A proposal that hasn't seen much discussion since November is a suggestion to disallow directly casting function pointers to integers. Several supporters of the proposal noted that there are architectures where pointers to functions are different sizes than pointers to data, and expecting a usize value to contain a function pointer is incorrect. Others were concerned that changing this behavior would be making users jump through extra hoops for little benefit.
The last proposal currently under discussion would change how Rust represents ranges by adding new range types that would implement Copy, allowing them to be duplicated without boilerplate. The current range types do not implement Copy because of how this would interact with their use as iterators. The discussion of this proposal focused on how the introduction of new types might complicate the use of libraries that rely on the old types, for example by using the old types as indexes into collections. Peter Jaszkowiak, the contributor working on this proposal, did not believe this would be a significant problem, but said that he would adapt the proposal to include convenience features to ameliorate this if the language team thought that was warranted.
Conclusion
People who have been paying attention to Rust development since 2021 may find this list of changes for the 2024 edition a bit sparse, since it includes little mention of improvements to asynchronous code, to the standard library, or any of the hundreds of other small improvements Rust contributors have made in that time. These improvements are all already available in the 2021 edition. In Rust, only backward incompatible changes need to wait for the next edition. Therefore, the release of the 2024 edition heralds two things: the 2021 edition becoming more stable — in the sense that new syntax will now go in the 2024 edition — and the chance to fix various small issues that nonetheless hinder the continued evolution of the language, without breaking existing users.
