December 19, 2019·5 min read·engineeringrustapicompatibilitylanguage

Rust 1.40 lets public types reserve room to grow

The non_exhaustive attribute prevents downstream code from depending on a complete set of variants or fields. Rust 1.40 also ends borrow-checker migration warnings for the 2015 edition.

A public enum can accidentally promise that its current variants are the only variants it will ever have. Downstream code can exhaustively match them, so adding another variant breaks the consumer's source. Rust 1.40 gives library authors a direct way to decline that promise with #[non_exhaustive].

The December 19 announcement stabilizes the attribute for enums, structures and enum variants. It changes what code outside the defining crate may assume about completeness. The restriction is more specific than making every field private, and more explicit than adding a hidden placeholder variant.

I find this useful because a public type should communicate the actual contract its author intends to maintain. Exhaustive matching is valuable when the cases are closed. An evolving library also needs a way to say that the current set is intentionally open without relying on a naming convention users can ignore.

External matches must allow unknown variants

On an enum, #[non_exhaustive] prevents other crates from exhaustively matching its known variants. A consumer needs an additional wildcard arm to account for variants the library may add later. The release uses atomic ordering as an example of this requirement.

For a smaller library example, the defining crate could expose an extensible status type.

#[non_exhaustive]
pub enum Status {
    Ready,
    Busy,
}

Code in a different crate must preserve a fallback when matching it. Assuming the library crate is named protocol, a consumer could write the following function.

use protocol::Status;

fn can_start(status: Status) -> bool {
    match status {
        Status::Ready => true,
        Status::Busy => false,
        _ => false,
    }
}

The fallback has a real semantic purpose, it defines this consumer's response to future states. In this example it declines to start work for an unfamiliar state. Other applications may require a different policy, but they cannot omit the possibility merely because today's library exposes only two variants.

Inside the defining crate, the design RFC permits exhaustive matching. The library author can therefore receive a compiler error when adding a variant without updating internal handling. The boundary preserves internal completeness checking while preventing downstream code from freezing the public list.

Hidden variants were an incomplete substitute

The RFC describes the previous workaround of adding a variant hidden from generated documentation. Ordinary external crates could still name that variant and include it in an exhaustive match. Hiding it from a page did not remove it from the language's accessible cases.

The standard library could additionally make such a hidden variant unstable, preventing stable code from using it. The RFC explains why that approach did not provide the same mechanism to ordinary library authors. A dedicated attribute gives the intended restriction a language-supported representation.

The workaround could also affect representation by adding an actual variant that the type otherwise did not need. The RFC discusses potential size and optimization costs. Those are motivations for avoiding dummy variants, rather than a promise of a particular size reduction for every enum adopting the new attribute.

The distinction is between a present hidden case and possible future cases. non_exhaustive expresses the latter directly. A library can reserve room for growth without introducing an artificial value solely to make consumers write a fallback.

Introducing the attribute on an existing public type also deserves attention. A consumer that currently uses an exhaustive match will no longer satisfy the new external restriction. This follows from the rule itself, so adding the attribute is not a way to erase compatibility obligations that consumers have already acquired. It is most straightforward when the open-ended contract is established as part of the API's intended design.

Similarly, a wildcard makes an added variant source-compatible with a consumer's match but cannot prove that the fallback implements the best policy for the new case. The consumer may later choose to handle that variant specifically. Compilation compatibility and application behavior remain separate questions.

Structures reserve future fields by restricting construction

On a structure, the attribute prevents construction from outside its defining crate and requires non-exhaustive field patterns. Public fields can remain readable, but external code cannot assume that supplying the current fields completely constructs a value.

This addresses a different compatibility pressure. A configuration structure with public fields is easy to inspect, but an external literal naming every field prevents the library from adding a required field without breaking that literal. Non-exhaustive construction keeps the library responsible for producing complete values.

Consumers can still extract the known public fields using a pattern containing ... That pattern says explicitly that other fields may exist. The same principle applies when the attribute is placed on a particular enum variant, allowing the variant's fields to grow without promising its present shape is complete.

The RFC also discusses structure-update syntax. Outside the defining crate, adding ..Config::default() does not bypass the non-exhaustive construction restriction. The library could later add private fields, so construction remains an operation controlled by the library. A constructor or other library-provided value is the appropriate starting point.

I prefer this explicit boundary to leaving users to infer future intentions from comments. Public readability and public construction are different capabilities. The attribute lets an API preserve the former while retaining control over complete initialization and future additions.

The borrow-checker migration ends for Rust 2015

The release also changes borrow-checker migration warnings into hard errors for the 2015 edition. Rust 1.39 made the corresponding change for Rust 2018. The December announcement explains that the old checker accepted some memory-unsafe programs that the non-lexical-lifetime checker correctly rejects.

Migration mode had downgraded those disagreements to warnings when the old checker accepted the program. Ending that mode therefore can break code that previously compiled with a warning. The stated reason is closing the old soundness holes and enabling removal of the older compiler machinery.

This is separate from the new attribute. An existing program need not adopt non_exhaustive to encounter a borrow-checking error after upgrading. A release assessment should connect the diagnostic to the checker transition instead of assuming every compatibility change comes from the headline API feature.

For public type design, the new mechanism has a narrower purpose. It lets the defining crate preserve room for additional cases or fields while requiring consumers to acknowledge that openness. The compiler can then enforce the boundary the library author intended, with downstream fallback behavior visible in the source.