Constant promotion with interior mutability exposes whether a function was used
Opened this issue ยท 14 comments
use std::cell::Cell;
const fn make_none<T>() -> Option<T> {
None
}
const A: Option<Cell<i32>> = None;
const B: Option<Cell<i32>> = make_none();
fn works() {
let _: &'static _ = &A;
}
fn fails() {
let _: &'static _ = &B;
}In the above code, I expected works and fails to either both compile or both not compile. Instead, only works compiles, and fails gives the following error:
error[E0716]: temporary value dropped while borrowed
--> src/lib.rs:15:26
|
15 | let _: &'static _ = &B;
| ---------- ^ creates a temporary value which is freed while still in use
| |
| type annotation requires that borrow lasts for `'static`
16 | }
| - temporary value is freed at the end of this statement
For more information about this error, try `rustc --explain E0716`.
That is, for some reason, &A is constant-promoted, but &B isn't.
I'm not sure if this is a bug, but it's at least extremely surprising, and is a semver hazard.
See also rust-lang/unsafe-code-guidelines#493
Meta
Reproducible on the playground with version 1.92.0-nightly (2025-09-20 dd7fda570040e8a736f7)
This is absolutely intended and not avoidable in general (cycle errors and any simple change like adding a condition causing it to stop working, too). We treat functions as abstract boundaries and assume they can return any value of their return type
@oli-obk This is not a duplicate. My reproducer only shows this strange behavior when interior mutability is involved. Without interior mutability, a const item is sufficient to cause the result of a function call to be const promoted.
Also, as far as I can tell, it's specifically a function call that causes this behavior. For example, the following code compiles:
use std::cell::Cell;
const A: Option<Cell<i32>> = {
let mut x = 0;
while x < 10 {
x += 1;
}
let a = None;
x += 1;
if x == 11 { a } else { panic!() }
};
fn works() {
let _: &'static _ = &A;
}And... I just found out that some function calls don't cause this behavior. The following code compiles.
use std::cell::Cell;
const A: Option<Cell<i32>> = {
let a = None;
let b = None;
if make_true() { a } else { b }
};
const fn make_true() -> bool {
true
}
fn works() {
let _: &'static _ = &A;
}If all values of the type are promotable, the value gets promoted. If all code paths produce a promotable value, it's also ok
This is just underdocumented as per #105270
Yeah it's not a duplicate of #60502 but it is expected behavior, in terms of the intent of the implementation.
When promoting &CONST and CONST is defined by calling some function, we definitely don't want to "look into" that function to figure our whether this can be promoted. I hope we agree on that. :)
OTOH, with const C: MyEnum = MyEnum::Variant;, it'd be rather odd to treat &C different from &MyEnum::Variant. So we do look into the value of a const for the purpose of promotion, but we don't look into function calls.
My intuition is that, if two consts have the same value, it doesn't matter how they're created, they ought to be able to be used in the same way ๐
That's a reasonable intuition, but sadly in contradiction with the two examples I sketched above. So something had to give.
When promoting
&CONSTandCONSTis defined by calling some function, we definitely don't want to "look into" that function to figure our whether this can be promoted. I hope we agree on that. :)
This is inconsistent with how the compiler currently checks whether a const can be used in a pattern. As per the reference, the check for StructuralPartialEq is done on the specific value of the const. In particular, it only requires StructuralPartialEq on the active variant of enums. For example, the following code compiles and prints "Is Some", showing that the check is done after executing the initializer of the const:
use std::sync::atomic::AtomicI32;
#[allow(dead_code)]
struct Weird(AtomicI32);
impl PartialEq for Weird {
fn eq(&self, _: &Self) -> bool {
panic!()
}
}
const fn make() -> Option<Weird> {
None
}
const C: Option<Weird> = make();
fn main() {
match Some(Weird(AtomicI32::new(1))) {
C => println!("Is C"),
Some(_) => println!("Is Some"),
}
}When I said "look into a constant/function", I didn't mean "evaluate the constant/function". We cannot evaluate the constant that is considered for promotion, it could depend on generic arguments. What I meant by this term is look at the source code of the constant (and the functions it calls) and draw conclusions from that -- a classical static analysis. That's what we do for constants, but not for the functions it calls.
For patterns, we don't do a static analysis of the source code of the constant (or of anything else). We evaluate the constant. This is because patterns must be monomorphic, i.e. they cannot depend on generic arguments. We have to evaluate them anyway to even do MIR building.
So I think you are drawing a false parallel here.
Cc @rust-lang/lang-docs
This is because patterns must be monomorphic, i.e. they cannot depend on generic arguments.
Depending on what you mean, this might not be true. See #147721.
We fully evaluate consts during pattern lowering. That error you filed shows the const being evaluated. There's a bug here in that some earlier check should have prevented this from even happening for a generic const, but there's no doubt that pattern matching works on the const value whereas promotion works on the const source code (i.e. its MIR body).
We could consider opportunistically using the const value for promotion when it can be computed, but I am not sure that'd be a good idea.