Why compiler error[E0492]: statics cannot refer to interior mutable data

const CONST: AtomicIsize = AtomicIsize::new(0);
// ^ compiles, storing in one place doesn't affect loading elsewhere
static STATIC: AtomicIsize = AtomicIsize::new(0);
// ^ compiles, value stored in one place can be loaded elsewhere
const CONST_REF: &AtomicIsize = &AtomicIsize::new(0);
// ^ error[E0492]: constants cannot refer to interior mutable data
// makes sense
static STATIC_REF: &AtomicIsize = &AtomicIsize::new(0);
// ^ error[E0492]: statics cannot refer to interior mutable data
// why?

Could someone explain?

Obligatory did you read E0492 - Error codes index?

Note that constants in Rust aren't stored anywhere, but rather they are evaluated at compile time and the result is copied to the locations the constant is used. If I were to refer to CONST multiple times, all invocations would result in different instances of AtomicIsize. Static variables on the other hand do have a single location in memory that is referred to whenever the static variable is used somewhere.

3 Likes

Yes, I did.

But it only explains why consts shouldn't refer to interior mutable data, not statics

Wha't's more, the following works perfectly:

static STATIC: AtomicIsize = AtomicIsize::new(0);
static STATIC_REF_TO_STATIC: &AtomicIsize = &STATIC;

while STATIC_REF_TO_STATIC actually, still refers to interior mutable data.

I know, and that's exactly why I don't understand the reason of statics not being allowed to directly refer to interior mutable data,but is somehow allowed to indirectly do so.

Maybe it's some kind of compiler bug?

I believe STATIC_REF doesn't compile, because it essentially creates CONST_REF first, saving it to a global memory location referred to by the static variable.

Is there any doc describing this sort of behavior?

Static promotion forbids interior mutability like that.

Although the case in OP is not real static promotion, the relevant doc should be const-eval: GitHub - rust-lang/const-eval: home for proposals in and around compile-time function evaluation

4 Likes

The reference has a section on static items:

The static initializer is a constant expression evaluated at compile time. Static initializers may refer to other statics.

I thought about it again, and found that according to this explanation, the following shouldn't compile:

static STATIC: AtomicIsize = AtomicIsize::new(0);
static STATIC_REF_TO_STATIC: &AtomicIsize = &STATIC;

because it would be first creating CONST_REF_TO_STATIC, which is a const referring to interior mutable data, that should never be allowed.

What do you think about the above example?

It creates a reference to a global variable, not a reference to a constant with interior mutability. STATIC creates a constant AtomicIsize, which is fine since it is not borrowed, unlike &AtomicIsize.

So you mean by rewriting the illegal:

static STATIC_REF: &AtomicIsize = &AtomicIsize::new(0);

to the legal:

static STATIC: AtomicIsize = AtomicIsize::new(0);
static STATIC_REF: &AtomicIsize = &STATIC;

Only in the latter case is an intermediate static created?

I wonder what's the merit of making the two not to be considered equivalent?

Yes.

I assume static variables don't get that kind of extra treatment because the added complexity during compilation wouldn't be worth the effort? But I'm not a compiler engineer. In my mental model of static variables in Rust, they are just constants expressions with a single location in memory. So all the restrictions of const evaluation apply to them. I personally would find it a lot weirder if

static STATIC_REF: &AtomicIsize = &AtomicIsize::new(0);

would compile but

const CONST_REF: &AtomicIsize = &AtomicIsize::new(0);

wouldn't, just because the compiler is smart enough to add an anonymous static variable somewhere in the former case.

1 Like

I found the related doc:

Constant promotion

Promotion of a value expression to a 'static slot occurs when the expression could be written in a constant and borrowed, and that borrow could be dereferenced where the expression was originally written, without changing the runtime behavior. That is, the promoted expression can be evaluated at compile-time and the resulting value does not contain interior mutability or destructors (these properties are determined based on the value where possible, e.g. &None always has the type &'static Option<_>, as it contains nothing disallowed).

when reading through the information provided by

I can understand why values with destructors are excluded by the promotion rule (in non-'static scopes, destructors are not expected to never be excecuted), but I still don't understand why values with interior mutability are designed to be excluded.

Could someone explain or provide a link?

The UnsafeCell restrictions are there to ensure that the promoted value is truly immutable behind the reference.
src: 1414-rvalue_static_promotion - The Rust RFC Book

If the reference has type &Cell<i32> it is quite clear that the program can easily observe whether two references point to the same memory even without comparing their address: Changes through one reference will affect reads through the other. So, we cannot allow constant references to types that have interior mutability (types that are not Freeze )...
src: const-eval/const.md at master · rust-lang/const-eval · GitHub

But these are reasons for const items. Const item needs the value to be truely immutable anywhere.

For static items in question:

Constants and promoteds are not allowed to read from statics, so their final value does not have have to be const-valid in any meaningful way.
src: const-eval/static.md at master · rust-lang/const-eval · GitHub

i.e. Promoted &AtomicIsize is not allowed in statics.
i.e. Making the Atomic value static, or reference-to-static is allowed:

use core::sync::atomic::AtomicIsize;
static S: AtomicIsize = AtomicIsize::new(0);
static A: &AtomicIsize = &S; // compiles: not promotion, just refer to a static

// static S: &AtomicIsize = &AtomicIsize::new(0); // fail: promotion

Aside: Promotion means

// if this compiles
const X: &'static T = &CONST_EXPR;
// then this compiles
let x: &'static T = &CONST_EXPR;

To make static S: &AtomicIsize = &AtomicIsize::new(0) work, the compiler tries to treat the reference as a static promotion instead of a reference to a static. This is reasonable: AtomicIsize::new(0) is not an explicit static item, so rustc will by default treat it as a constant value

static S: &AtomicIsize = &AtomicIsize::new(0); // fail: promotion

static S: &AtomicIsize = &const { AtomicIsize::new(0) }; // rustc may treat like this
static S: &AtomicIsize = { const A: AtomicIsize = AtomicIsize::new(0); &A }; // rustc may treat like this
static S: &AtomicIsize = { static A: AtomicIsize  = AtomicIsize::new(0); &A }; // not this
2 Likes

I think I had previously missed this point:

This(promotion) is essentially an automatic transformation turning &EXPR into { const _PROMOTED = &EXPR; _PROMOTED } , but only if EXPR qualifies.
from: const-eval/promotion.md at master · rust-lang/const-eval · GitHub

So maybe compiler hint could be improved to advise for an intermediate static? (My fault: I was too preoccupied by the misleading error message that I failed to notice the advice was already there) Saying it doesn't work because statics cannot refer to interior mutable data is misleading.

Simplification of the compiler.

  1. Rust compiler doesn't exist in vacuum. It's goal is to produce some kind of binary that you may actually run.
  2. And it wants to avoid “life before main”.
  3. And that means static should be something that linker may produce.
  4. And what does linker produce on most common platforms? That's simple: precomputed sequence of bytes plus relocations.
  5. What are relocations? Most relocations on most common platforms are designed to massage code into something runnable, but because linkers are designed to support C they also have one common relocation designed to work with globals: address of [global] variable is added the isize data you supply (so efficiently it's address of global plus fixed offset).

And AFAICS this limitation is to both make compiler simpler and also to avoid code like this:

static FOO: (&AtomicIsize, &AtomicIsize) =
  (&AtomicIsize::new(0), &AtomicIsize::new(0));

where author would expect to have reference to one, single, static.

That's very slightly and subtly, but incorrect. See above.

Create a bug for rustc ? The reason for current limitation is clear, but error message may definitely be improved.

“Fun” fact: that’s not 100% true:

const fn add_one(x: i32) -> i32 {
    x + 1
}

// this compiles
const X: &'static i32 = &add_one(0);

// this doesn't compile
let x: &'static i32 = &add_one(0);

Rust Playground

1 Like

Yeah, I'm aware of that. Actually, static promotion is a bit outdated. Currently, the terminologies are

  • Making references have 'static lifetime is called “lifetime extension”.
  • The underlying mechanism of extracting part of some code into a constant is called “promotion”.

src: https://rust-lang.github.io/rfcs/3027-infallible-promotion.html#background-on-promotion-and-lifetime-extension

Const fns that are not marked #[rustc_promotable] are not promotable.

#[rustc_promotable] can't be applied outside of compiler or standard library, so basically, all custom const fns are not promotable.

And the only way to make lifetime extension to the result of them is by wrapping it in const /static initializers.

As a consequence, we only promote code that can never fail to evaluate (see RFC 3027). This ensures that even if promotion happens inside dead code, this will not turn a "runtime error in dead code" (which is not an error at all) into a compile-time error. In particular, we cannot promote calls to arbitrary const fn, as discussed in detail in rust-lang/const-eval#19. Thus, only functions marked #[rustc_promotable] are promotable.

There is one exception to this rule: the bodies of const/static initializers. This code is never compiled, so we do not actually have to evaluate constants that occur in dead code. If we are careful enough during compilation, we can ensure that only constants whose value is actually needed are evaluated. We thus can be more relaxed about promotion; in practice, what this means is that we will promote calls to arbitrary const fn, not just those marked #[rustc_promotable].

See below for another special case in promotion analysis: accesses and references to statics are only promoted inside other statics.

src: const-eval/promotion.md at master · rust-lang/const-eval · GitHub

1 Like