Introduction to Rust

Rust Editions

A Rust Edition (2015, 2018, 2021, 2024) is an opt-in milestone configured per crate in Cargo.toml that allows the Rust language to introduce new keywords, refine grammar rules, and update default preludes without ever breaking existing code written for earlier editions.

Why Editions Exist: Stability Without Stagnation

In most programming languages, adding a new keyword (such as async, await, or gen) creates an unavoidable dilemma: any existing program that used async or gen as a variable or function name immediately stops compiling when the compiler updates.

Rust solves this by separating compiler versions (1.0 through 1.85+, released every six weeks) from language editions (released roughly every three years):

  • A crate declares which edition it uses via edition = "2024" in Cargo.toml.
  • New keywords, syntax changes, and prelude additions that could break older code are enabled only for crates that opt into the new edition.
  • A single modern rustc compiler supports all editions simultaneously.

Seamless Cross-Edition Interoperability

Unlike Python 2 vs Python 3—where libraries written in different versions could not easily be imported into the same program—Rust editions affect only how surface syntax is parsed and lowered to HIR. Inside rustc, every edition compiles down to the exact same Mid-Level IR (MIR) and binary ABI:

Crate A (edition = "2015") ──┐
Crate B (edition = "2018") ──┼──> Common HIR / MIR inside `rustc` ──> Single Linked Binary
Crate C (edition = "2021") ──┤
Your App (edition = "2024") ─┘

Your application running on the 2024 Edition can depend on crates from crates.io written in the 2015, 2018, 2021, or 2024 editions without any friction or wrapper code.

Timeline of Rust Editions

Edition Stabilized In Key Changes Introduced
Rust 2015 Rust 1.0 (May 2015) Baseline stable language; default edition when running rustc directly without --edition
Rust 2018 Rust 1.31 (Dec 2018) Reserved async and await keywords; simplified module system (no extern crate needed); Non-Lexical Lifetimes (NLL); dyn Trait syntax
Rust 2021 Rust 1.56 (Oct 2021) Disjoint field capture in closures (|| self.x); [a, b, c].into_iter() iterates by value; TryFrom/TryInto/FromIterator added to prelude; or_patterns in macros
Rust 2024 Rust 1.85 (Feb 2025) Future and IntoFuture added to prelude; RPIT (-> impl Trait) captures all in-scope lifetimes by default (+ use<...> syntax); refined temporary drop order in tail expressions and if let; unsafe extern blocks and unsafe(no_mangle) attributes; reserved gen keyword

Highlights of the Rust 2024 Edition

Setting edition = "2024" in Cargo.toml enables the latest language ergonomics and safety refinements:

1. Future and IntoFuture in the Prelude

You can write functions returning impl Future<Output = T> directly without importing std::future::Future:

fn compute_async() -> impl Future<Output = u32> {
    async { 10 + 32 }
}

fn main() {
    let _future = compute_async();
    println!("Created Future using 2024 prelude");
}
Created Future using 2024 prelude

2. Explicit unsafe extern Blocks

In the 2024 edition, declaring foreign C functions requires writing unsafe extern "C" { ... }, making it explicit that declaring a foreign function signature is a promise to the compiler that the C signature is accurate:

unsafe extern "C" {
    fn abs(input: i32) -> i32;
}

fn main() {
    let val = unsafe { abs(-42) };
    println!("abs(-42) = {val}");
}
abs(-42) = 42

3. gen Reserved as a Keyword

To prepare for future generator blocks (gen { yield 1; }), gen is a reserved keyword in the 2024 edition. If you need to call a function named gen from an older library, use a raw identifier (r#gen).

Migrating a Project to a New Edition (cargo fix --edition)

Upgrading an existing crate from an older edition (such as 2021) to 2024 is automated by Cargo:

# 1. Automatically rewrite any syntax that changes meaning in the new edition:
cargo fix --edition

# 2. Update `Cargo.toml` to `edition = "2024"`
# 3. Verify the project compiles and passes tests:
cargo test

cargo fix --edition inspects your code and applies mechanical updates (such as converting extern "C" to unsafe extern "C" or escaping gen identifiers as r#gen) so your code's behavior remains identical under the new edition.