Editions and modern Rust

Rust changes every six weeks without breaking old code, and an edition is how: a crate chooses the rules it is compiled under, and the compiler carries every edition from 2015 to 2024 at once. A new edition may reserve a word, tighten a pattern or require a keyword, and only the crates that opt in see the change.

This lesson compiles the same program under two editions, shows three differences that a 2024 crate notices, and moves a crate from 2021 with the tool that does the mechanical part. Every command below ran on this machine.

What an edition is

The edition is a field in the manifest, and rustc takes it from --edition. It is not a version of the language: a crate of every edition links against the same standard library, and the same compiler builds all of them.

The program is the first one of the course: it prints a number. Both editions build it, which is the promise of the whole system.

fn main() { let total = 40 + 2; println!("{total}"); }

rustc --edition 2015 plain.rs
./plain
rustc --edition 2024 plain.rs
./plain

42
42

Plain code like this has the same meaning in every edition, so both binaries print the same line. The differences appear at the edges: a word that became a keyword, a pattern that became an error, an attribute that needs unsafe. The first program lesson shows the same file under rustc without the flag, which defaults to 2015.

A keyword reserved in 2024

Edition 2024 reserves gen for the generator syntax that is being prepared. A crate of an older edition may still use it as a name, and a 2024 crate may not, which is the smallest possible example of an edition change: a word that becomes unavailable to new code.

The program binds a variable named gen. The 2021 build accepts it, and the 2024 build reports the reserved word and the escape that keeps the program compiling.

fn main() { let gen = 1; println!("{gen}"); }

rustc --edition 2021 gen.rs
./gen
rustc --edition 2024 gen.rs

1
error: expected identifier, found reserved keyword `gen`
--> gen.rs:2:9
|
2 | let gen = 1;
| ^^^ expected identifier, found reserved keyword
|
help: escape `gen` to use it as an identifier
|
2 | let r#gen = 1;
| ++

The raw identifier r#gen is the escape for every keyword, and it is what a macro or a generated name uses when it cannot know the edition of its caller. A human code base renames the variable instead, which is the cheaper fix.

Match ergonomics in 2024

A pattern that matches a reference borrows its contents implicitly, and since 2024 an explicit ref in that position is an error. The rule removes a double borrow that used to be written by hand and read as if it were needed.

The program matches &Option<i32> and writes Some(ref inner). The 2021 build compiles it, and the 2024 build rejects the modifier and prints the line without it as the fix.

fn main() { let value = Some(5); match &value { Some(ref inner) => println!("{inner}"), None => println!("none"), } }

rustc --edition 2021 ergonomics.rs
./ergonomics
rustc --edition 2024 ergonomics.rs

5
error: cannot explicitly borrow within an implicitly-borrowing pattern
--> ergonomics.rs:4:14
|
4 | Some(ref inner) => println!("{inner}"),
| ^^^ explicit `ref` binding modifier not allowed when implicitly borrowing
|
= note: for more information, see <https://doc.rust-lang.org/reference/patterns.html#binding-modes>
note: matching on a reference type with a non-reference pattern implicitly borrows the contents
--> ergonomics.rs:4:9
|
4 | Some(ref inner) => println!("{inner}"),
| ^^^^^^^^^^^^^^^ this non-reference pattern matches on a reference type `&_`
help: remove the unnecessary binding modifier
|
4 - Some(ref inner) => println!("{inner}"),
4 + Some(inner) => println!("{inner}"),
| +

The note explains the mechanism: the pattern already borrowed, so the modifier asked for a second borrow. The fix is the line the compiler prints, and it also compiles in 2021, which is why a code base can be prepared for 2024 before the edition changes. The lesson on patterns covers the binding modes.

Unsafe attributes in 2024

An attribute that changes what the compiler promises about a symbol is now marked unsafe(...): no_mangle, export_name and link_section. The attribute is the programmer's promise that the name or the section is not used by anything else.

The program exports a function with the old form. Edition 2024 reports the attribute and shows the wrapped form in the suggestion.

#[no_mangle] pub extern "C" fn triple(value: i32) -> i32 { value * 3 } fn main() { println!("{}", triple(14)); }

rustc --edition 2024 noman.rs

error: unsafe attribute used without unsafe
--> noman.rs:1:3
|
1 | #[no_mangle]
| ^^^^^^^^^ usage of unsafe attribute
|
help: wrap the attribute in `unsafe(...)`
|
1 | #[unsafe(no_mangle)]
| +++++++ +

error: aborting due to 1 previous error

The same form works in 2021 as well, so the wrapped attribute is the one to write while a crate is still on the old edition. The lesson on calling C uses the attribute across a real boundary.

cargo fix --edition

The tool compiles the crate under the next edition with the lints that report what would break, and rewrites the source where the fix is mechanical. It leaves the manifest edition for the programmer, and the output says how many files it changed.

The package starts on 2021 with the pattern from the previous section. The command fixes it, and the manifest is then bumped by hand.

cargo fix --edition --allow-no-vcs
cat src/main.rs

   Migrating Cargo.toml from 2021 edition to 2024
    Checking moving v0.1.0 (/tmp/rw7/L087/moving)
   Migrating src/main.rs from 2021 edition to 2024
       Fixed src/main.rs (1 fix)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.38s
fn main() {
    let value = Some(5);
    match &value {
        Some(inner) => println!("{inner}"),
        None => println!("none"),
    }
}

--allow-no-vcs is needed outside a repository, because the tool rewrites files and wants a way back. After the manifest says edition = "2024", the package prints the same five and is on the new rules. The next lesson reads the old code that needs more than a mechanical fix.

Exercises

  1. Rename the gen variable with a raw identifier and check that the 2024 build passes.
  2. Write the pattern of the second section without the modifier and build it under 2015 and 2021.
  3. Run cargo fix --edition on a crate of your own and list every change with git diff.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What does an edition change?

    Answer

    The rules a crate is compiled under

    One compiler builds every edition, and the edition decides which rules apply.

  2. What does the raw identifier r#gen do?

    Answer

    Lets a keyword be used as a name

    The prefix tells the lexer to read the word as an identifier.

  3. Why is an explicit ref an error in a 2024 match?

    Answer

    The pattern already borrows the value implicitly

    Match ergonomics set the binding mode, so the modifier would add a second borrow.

  4. What does cargo fix --edition leave to the programmer?

    Answer

    The edition field in the manifest

    The tool prepares and fixes the code, and the edition is bumped by hand.

  5. What does this program print under edition 2015 and under edition 2024? fn main() { let count = 5; let doubled = count * 2; println!("{count} {doubled}"); }

    Answer

    5 10

    Plain code has the same meaning in every edition, which is the promise of the system.

  6. What does this program print? fn main() { let value = Some(7); match &value { Some(inner) => println!("{inner}"), None => println!("none"), } }

    Answer

    7

    The pattern borrows the contents and the print uses the number.

  7. What does this program print? fn main() { let number = 42; let text = if number > 0 { "positive" } else { "negative" }; println!("{text}"); }

    Answer

    positive

    An if expression yields a value, and the condition is true.

  8. What does this program print? fn main() { let values = [10, 20, 30]; let slice = &values[1..]; println!("{} {}", slice.len(), slice[0]); }

    Answer

    2 20

    The range starts at the second element, so two are left and the first is twenty.

  9. What is the first line of the compiler output for this program when it is compiled with edition 2024? fn main() { let gen = 1; println!("{gen}"); }

    Answer

    error: expected identifier, found reserved keyword gen

    Edition 2024 reserves the word, and the help offers the raw identifier.

  10. What is the first line of the compiler output for this program when it is compiled with edition 2024? fn main() { let value = Some(5); match &value { Some(ref inner) => println!("{inner}"), None => println!("none"), } }

    Answer

    error: cannot explicitly borrow within an implicitly-borrowing pattern

    The pattern already borrows, so the explicit modifier is refused.

The code on this page was built with rustc 1.96.1 and cargo 1.96.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev