Mehdi Akiki
Rust Failure Atlas / Upgrades and compatibility

RFA-081 · Case file with fixtures · Case 53 of 694 · Compiler evidence

Rust 2024: When gen Stops Being a Valid Identifier

Rust 2024 reserves gen for future language syntax. The useful repair is not always a raw identifier: first decide whether compatibility or a clearer domain name is the real requirement.

Reviewed
Rust
Rust 1.98.1
Targets
all targets
Profiles
dev, release, test

Direct answer

What this Rust failure means

Why it happens
The 2024 edition reserves `gen` for future language syntax, so an identifier that was ordinary in an older edition now needs renaming or raw-identifier syntax.
First discriminating check
Compile the smallest occurrence with edition 2024 and decide whether `r#gen` is required for compatibility or a domain name is clearer.

An edition migration can fail before type checking begins. A small example is an identifier named gen:

fn main() {
    let gen = 4;
    assert_eq!(gen, 4);
}

This is ordinary code in Rust 2021. With Rust 1.98.1 and edition 2024, the parser reports expected identifier, found reserved keyword gen. The failing program keeps this exact boundary visible.

The important detail is that the compiler did not discover a new problem with the value. It changed how the same token is classified. gen became reserved in the 2024 edition so future generator syntax can use it without taking an identifier away later.

Editions change parsing without splitting the ecosystem

Rust editions let one package adopt language changes while dependencies stay on another edition. A Rust 2021 library and a Rust 2024 binary can still be linked in the same dependency graph. The edition controls how source code in each crate is interpreted; it does not create a different Rust ABI or package registry.

This explains a migration result that can otherwise look inconsistent. A dependency may still expose an API compiled from old-edition source using a name that is awkward in the new edition. The dependency does not need an immediate rewrite. The new-edition caller may need raw-identifier syntax when it names that item.

The Edition Guide documents both the reservation and the migration lint. The Reference keyword list records that gen is reserved beginning in edition 2024.

There are two valid repairs

The direct compatibility repair is a raw identifier:

let r#gen = 4;
assert_eq!(r#gen, 4);

The r# prefix tells the parser to treat a keyword-shaped token as an identifier. This matters for public APIs, generated bindings, serialized names represented in code, and macros that must preserve an external vocabulary. It is not a strange variable name at the symbol level; it is source syntax for referring to the identifier gen.

For private application code, I usually prefer a domain name such as generation, generator, or generated. The repaired program uses generation. A rename is easier for readers who do not already know raw identifiers, and it avoids spreading escape syntax through logs, documentation, and examples.

Neither repair is universally better. Compatibility is a real constraint. Clarity is also a real constraint. I choose after finding who owns the name.

Why blind replacement is risky

A repository-wide replacement from gen to generation can touch more than Rust identifiers. It may alter JSON field names, SQL columns, protocol values, snapshots, documentation examples, shell variables, or strings consumed by another program. Conversely, inserting r# inside a string produces the wrong external name.

I separate occurrences into four groups:

  1. Rust identifiers owned by this crate.
  2. Public Rust API identifiers consumed by other crates.
  3. Names generated from an external schema or binding tool.
  4. Plain data and text that the Rust parser never interprets.

Only the first three are relevant to the keyword migration, and they may need different repairs. Public APIs may also need a deprecation period rather than an immediate rename.

Macros make the location less obvious

A declarative or procedural macro may generate the token that fails. In that case the underline can appear at the invocation while the name originates in a schema, template, or macro implementation. I inspect expanded code or reduce the invocation before editing the call site.

Token construction deserves special attention. A macro that turns user strings into identifiers must know the target edition and keyword set. Escaping everything is not a complete design because not every generated name should become part of a stable API.

My migration check

I run the edition compatibility lint before changing the manifest, review the suggested edits, then compile the workspace using the target edition. I also search public documentation and downstream examples for the old name. The compiler proves the new source parses; it does not prove an API rename is harmless.

The small lesson is useful beyond this keyword. When an edition migration produces a parser error, I first identify the token classification change. I do not debug types, lifetimes, or Cargo features yet. For gen, the decision is precise: use r#gen where the spelling is an external contract, or choose a clearer owned identifier where it is not.