November 28, 2024·4 min read·engineeringrustconstreferenceslibraries

Rust 1.83 permits mutation inside constant evaluation

Mutable references and additional library operations can now participate in compile-time computation. The result still has to satisfy the rules that keep constant values independent of mutable global state.

A computation can mutate temporary state while still producing an immutable final result. That distinction is central to Rust 1.83, released on November 28. The release permits mutable references and additional operations in constant contexts, allowing more ordinary algorithms to run during compilation without making the resulting constant an arbitrary mutable object.

The new capability concerns the evaluation process and the value it eventually produces as separate things. A helper can borrow local state mutably, update it, and return a result. The final constant must still satisfy restrictions on references to mutable memory.

I find this separation useful because it preserves familiar algorithm structure. A compile-time builder should not need an unrelated implementation solely because its intermediate state changes. Rust can permit that local work while continuing to reject results whose meaning would depend on mutable global contents.

Constant contexts require evaluation during compilation

The announcement identifies the contexts involved. They include initializers for constants and statics, array lengths, enum discriminants, and const generic arguments. A const fn can participate in those computations. These are places where the compiler must determine a value, rather than merely optimize an ordinary runtime expression if convenient.

The distinction has a long history. RFC 911, the original const-function proposal, dates to 2015 and motivates constructors for statics whose representations should remain private. Without callable constructors in constant contexts, a library could be pressured into exposing fields so another declaration could initialize them directly.

That early proposal was deliberately limited. It did not attempt to provide general compile-time evaluation immediately. Reading its restrictions as the current language would be misleading, but its motivation remains relevant. Constant evaluation should allow an abstraction's own operations to construct a value instead of requiring callers to duplicate its representation.

Rust 1.83 extends the operations those computations can use. The benefit is not simply replacing a number with a longer constant expression. It is allowing reusable functions and standard-library methods to participate in construction while respecting their own invariants.

Mutable borrowing can stay inside the computation

The release example uses a const fn that increments an integer through a mutable reference. A constant initializer creates a mutable local, calls the helper with a mutable borrow, and returns the updated integer. The borrow serves the computation without becoming part of the final constant value.

That lets code organize work through helper functions instead of repeatedly returning every intermediate value. The mutation is confined to state the evaluator is constructing. Its eventual result is an integer whose value does not change when the program runs.

Interior mutability is also available during evaluation. The announcement demonstrates creating an UnsafeCell, updating its contents through a raw pointer, and consuming it to obtain the final integer. The example illustrates evaluator capability, while its unsafe operation still needs to satisfy the usual requirements of that operation.

The release expressly rejects retaining a mutable reference such as a reference to a temporary integer as the final constant. Enabling mutable access inside an evaluation is not permission to expose that access indefinitely through the completed constant.

This boundary keeps the resulting object distinct from a runtime variable whose contents can change. The evaluator can use mutation to calculate a value while still checking whether the completed value is suitable for its constant context.

References to statics have a separate rule

Before this release, constant contexts other than static initializers could not reference static items. Rust 1.83 permits references to immutable statics. The announcement demonstrates a constant reference to an immutable static integer.

Reading the value of a mutable or interior-mutable static remains forbidden during constant evaluation. A constant's final value also cannot contain a reference to such a static. Otherwise evaluating or interpreting that constant could depend on global memory that changes during execution.

The announcement explicitly connects this restriction to patterns, which may dereference references when interpreting a constant. Keeping the value stable throughout execution requires more than calculating its initial address. It also requires preventing ordinary references from making mutable global contents part of the constant's meaning.

A raw pointer to a mutable static is a permitted distinct case. The announcement shows constructing one with &raw mut. A raw pointer records a location without granting safe reference access to its changing contents. This exception should not be confused with retaining a mutable reference into the evaluator's temporary state.

The difference follows the operations each type permits. A reference makes stronger validity and access guarantees. A raw pointer does not make dereferencing the target safe merely because the pointer itself was constructed in a constant context.

Library stabilization makes mutation useful

The release also marks many existing methods usable in const contexts. The list includes mutable slice access and splitting, Option::as_mut, Option::take, Option::replace, and mem::replace. Language support for a mutable borrow is more useful when the ordinary library operations around that borrow can participate too.

For example, an evaluator may need to update one part of a slice rather than reconstruct an entire aggregate for each step. Mutable access methods let that algorithm retain its local structure. Their const stabilization changes where they can run, rather than inventing a second set of slice semantics.

The announcement includes into_inner operations for several cell types. Consuming an intermediate cell can produce a final value without retaining the cell's access mechanism. That fits the distinction between mutation during evaluation and the permitted shape of the final constant.

Floating-point classification and bit-conversion methods also gain const support. These extend the computations available to a constant initializer independently of mutable borrowing. The release is therefore a collection of related evaluator and library changes, not a claim that every runtime function is now callable during compilation.

I appreciate the continuity with the original constructor motivation. Library abstractions are more useful when they do not force callers to abandon them at compile time. Rust 1.83 lets more computations use local mutation and existing operations, while the final-value checks preserve the boundary around mutable references and global state.