RFA-078 · Case file with fixtures · Case 50 of 694 · Compiler evidence
E0015: Why a Rust Constant Cannot Call an Ordinary Function
Const-callability is an API promise checked separately from ordinary execution. Mark a pure helper const only when all operations support const evaluation, or move the computation to runtime.
- Reviewed
- Rust
- Rust 1.98.1
- Targets
- all targets
- Profiles
- dev, release, test
Direct answer
What this Rust failure means
- Why it happens
- Const contexts accept only operations in the stable const subset, and an ordinary function does not promise that its body remains valid for compile-time interpretation.
- First discriminating check
- Mark the smallest pure helper `const fn` and let the compiler validate each operation in its body before widening the compile-time API.
The compiler does not inspect an ordinary function, notice that it returns three, and silently treat it as a const function:
fn read_default() -> u32 {
3
}
const DEFAULT: u32 = read_default();
Rust 1.98.1 reports E0015: a non-const function cannot be called in constants. The failing fixture shows that the function body itself contains no obviously dynamic operation.
The missing part is an explicit API contract. Ordinary functions may evolve to allocate, perform I/O, call virtual methods, or use operations unavailable to compile-time evaluation. Const callers need a promise that the body stays inside the supported const subset.
const fn is callable in two execution contexts
The direct repair is:
const fn read_default() -> u32 {
3
}
The function can now be called during required constant evaluation and at runtime. The repaired fixture builds the constant and verifies its value on Rust 1.98.1.
Marking a function const does not force every call to run at compile time. It permits const-context calls. An ordinary runtime call remains an ordinary call and can be optimized according to normal rules.
The Reference section on const functions defines this dual use and notes that the const qualifier is part of what callers may rely on.
Every operation in the body must be const-valid
Adding const can move the diagnostic into the function body. A called method, trait operation, allocation, destructor, pointer operation, or control-flow feature may not be supported in const evaluation for the selected toolchain.
I mark the smallest helper const and let the compiler reveal the first unsupported operation. Then I decide whether to:
- Use a const-capable primitive operation.
- Split runtime-only work from pure calculation.
- Store precomputed data directly.
- Keep the entire value runtime-initialized.
Reimplementing a complex standard operation only to force const evaluation can add more maintenance than the startup work it saves.
Rust version is part of const compatibility
The const-capable subset grows over time. A method accepted by Rust 1.98 may not compile under a library's lower MSRV. Conversely, an old workaround may become unnecessary on a newer toolchain.
For a published crate, I verify const code using the declared MSRV, not only the newest compiler. Making an existing public function const can be a compatible improvement, but using newly const-stabilized operations can raise the effective toolchain requirement if not guarded.
Static lazy initialization solves a different problem
If a value requires runtime work but should be created once, synchronization-backed lazy initialization may be appropriate. It runs during program execution, can allocate or read the environment, and may report runtime failure.
That is not equivalent to const. A constant can be embedded wherever used and needs no one-time initialization state. A lazy static has an address and runtime lifecycle. I choose based on what the value needs, not only on avoiding E0015.
Macros do not automatically make calls const
A macro can expand into const-compatible expressions, but calling an ordinary helper inside the expansion remains an ordinary call. Likewise, inlining attributes affect optimization and do not grant const-callability. The restriction is semantic, not based on whether the compiler could theoretically calculate the result.
Const promises constrain future implementation changes
Once downstream code uses a public function in array lengths, constant generics, patterns, or static initialization, removing const breaks that code. I therefore treat pub const fn as a real stability promise.
A function likely to need logging, allocation, dynamic dispatch, or platform queries later may be a poor const API even if today's body is simple. A narrow calculation helper can preserve the stable part while runtime wrappers evolve.
My first check
For E0015, I separate two questions:
- Could this exact body be evaluated on the current toolchain?
- Should callers be allowed to rely on compile-time evaluation as part of the API?
If both are yes, I add const and verify the MSRV. If the value is naturally runtime data, I move its initialization out of the const context rather than fighting the evaluator.
The official E0015 explanation shows allowed constant functions and blocks. The core lesson is that a predictable result is not enough: const-callability must be declared and every operation must satisfy the selected compiler's compile-time execution rules.