December 6, 2018·6 min read·engineeringrusteditionsborrowing

Rust 2018 changes source rules without splitting dependencies

Rust 1.31 ships the 2018 edition with per-crate opt-in, a more precise borrow checker, and revised module paths. Existing dependencies can remain on Rust 2015.

Changing a language's source rules usually creates pressure to migrate everything that depends on them. A library ecosystem makes that pressure difficult to manage because unrelated maintainers do not upgrade together. Rust 2018 addresses the problem by selecting source rules per crate. A program can combine crates using the existing edition with crates using the new one.

Today's Rust 1.31 and Rust 2018 announcement makes that compatibility model available on stable Rust. The release also brings non-lexical lifetimes to the new edition and revises module paths. These changes reduce several forms of source work while preserving a route for code that stays on Rust 2015.

I find the independent migration model especially valuable. Rust's semantics make it attractive for systems work, but adoption becomes harder when a local improvement requires coordinating every library in a dependency graph. An edition should let a maintainer make a bounded source change without manufacturing an ecosystem-wide deadline.

The compiler version and edition answer different questions

Rust 1.31 names a compiler release. Rust 2018 names a selection of source rules and a coordinated set of tooling and documentation improvements. Installing the newer compiler does not silently rewrite an existing crate's edition. A manifest without an edition key continues to select Rust 2015. The package setting makes the choice explicit.

[package]
name = "report"
version = "0.1.0"
edition = "2018"

The example declares the source mode for this package. It does not instruct all dependencies to use the same mode. The release announcement explicitly permits dependencies in either direction between Rust 2015 and Rust 2018 packages. That is the compatibility property which makes incremental adoption possible.

RFC 2052 explains the design motivation. Some improvements need incompatible source changes, such as introducing a keyword that an existing program might already use as an identifier. Edition selection gives those rules a defined boundary while allowing existing source to retain its earlier interpretation.

The model also separates edition-dependent changes from ordinary feature stabilization. A feature that does not require incompatible source rules can be available to earlier editions. An edition is therefore not a gate around every improvement introduced since the preceding edition. A project's compiler requirement and its edition still need to be understood separately.

This matters for a library's consumers. Staying on Rust 2015 does not promise compatibility with every old compiler if the library adopts newly stabilized APIs. Conversely, using a recent compiler does not require selecting Rust 2018. Those are distinct choices even when a project records them during the same maintenance work.

A borrow can end before its enclosing block

Non-lexical lifetimes address cases where the earlier borrow checker requires a reference to remain live longer than its uses demand. RFC 2094 describes lifetimes based on control flow. That gives the compiler more precise information than restricting the relevant region to a lexical block.

The distinction is between the use of a reference and the scope of the value it points to. The original value can remain alive while a particular borrow is no longer needed. Ending that borrow does not destroy the value or grant permission for a dangling reference. It allows subsequent operations once the conflicting use has ended. A small example demonstrates the intended effect in Rust 2018.

fn main() {
    let mut value = 5;
    let borrowed = &value;
    println!("{}", borrowed);

    let changed = &mut value;
    *changed += 1;
    assert_eq!(value, 6);
}

The shared reference is last used by the print operation. The mutable borrow follows that use. A checker tracking the reference's actual use can distinguish these phases without requiring an artificial inner block around the shared borrow.

That does not mean a shared reference can remain usable across conflicting mutation. Moving its use after the mutable borrow changes the constraints. The release announcement shows that such a case still receives an error, with diagnostics identifying the later conflicting use. Greater precision accepts additional valid programs while continuing to reject conflicting access.

The older workaround often involved restructuring scopes solely to shorten the compiler's inferred borrow. Removing that need makes the source more directly reflect the operation. A reader can follow the actual sequence of uses instead of asking why a seemingly unnecessary block surrounds a local reference.

Control flow matters as well as statement order

The RFC also motivates the change with conditional cases. A borrowed result may be used in one match arm while another arm needs to update the underlying collection. A lexical region covering the whole match can be broader than the dependency represented by one path through it.

Control-flow-based reasoning gives the checker a more suitable representation of that question. The relevant uses determine where a borrow must remain valid. That is more precise than treating every point inside one enclosing expression as though it participates in the same access pattern.

The RFC includes limitations as well as improvements. Non-lexical lifetimes should not be summarized as solving every borrow-checking rejection a programmer can produce. In particular, general claims about returning references through complex conditional paths need more evidence than the name of the feature supplies. The released examples establish useful cases without making that unlimited promise.

I prefer this form of ergonomics because it improves the compiler's account of a valid program. It can remove source rearrangements whose only purpose was accommodating an overly broad inferred borrow. The underlying requirement that references remain valid and accesses remain compatible still explains the accepted code.

In Rust 1.31, the announcement restricts the new checker behavior to Rust 2018. It describes making it available to Rust 2015 later as a plan. Rust 2015 packages do not yet receive that planned behavior in this release. Selecting the edition is therefore relevant to reproducing the examples on this release.

Module paths expose their starting point

The edition changes also revise how a programmer refers to external crates and the current crate. The announcement says extern crate is unnecessary in almost all cases. Paths beginning with crate identify the current crate root, while crate names identify external origins in the revised model.

That distinction reduces surprises when moving code into a submodule. A reference whose meaning depends on an implicit root assumption is harder to preserve during refactoring. An explicit origin gives both the reader and the compiler a more direct explanation of the intended lookup.

The filesystem conventions become less restrictive too. A module can use a foo.rs file alongside a foo/ subdirectory holding its children. The release says mod.rs is no longer necessary for that arrangement. This changes the available organization without requiring every existing source tree to be renamed.

Macro imports fit the same direction. A module can import a macro with use, which places the dependency alongside ordinary imported items. The release includes work that has already stabilized in earlier compiler versions as part of its overall edition account. That should not erase those earlier stabilization dates or imply that every listed capability first exists today.

Migration tools support the source transition

The release identifies cargo fix as the mechanism for automatically applying migration changes. Automation is particularly suitable where the compiler can identify a source spelling that must change under the new rules. It can reduce the manual work of updating a crate while preserving the intended program.

An automated source change still has a specific scope. It updates the crate being migrated. The dependency compatibility model is what permits other crates to remain in their own editions. The tool does not need to rewrite the entire ecosystem to make one package's source conform to Rust 2018.

The result is a more manageable maintenance operation. A maintainer can examine the migrated source and its manifest declaration together. Those files explain the crate's selected rules. Other maintainers can choose their own schedule as long as the compiler used for the combined build supports the participating crates.

Tool releases have their own guarantees

Rust 2018 includes Rustfmt 1.0 and stable Clippy availability. The announcement gives the formatter a compatibility promise under its default options. It separately explains that Clippy can add warnings, while builds that turn warnings into errors have chosen a stricter condition. These guarantees differ because formatting output and lint findings serve different purposes.

That distinction matters for automation consuming the tools. A formatting check compares source with a defined rendering. A lint check evaluates code against a collection of rules that may grow. Calling both tools stable does not mean neither can ever report anything new under any configuration.

The edition's practical contribution is a set of explicit boundaries. The package selects its source rules, its dependencies retain their own editions, and the compiler checks borrows using the behavior associated with the selected mode. A maintainer can adopt the revised paths and more precise checker locally while continuing to build against libraries whose authors have not migrated.