Repository navigation
'cargo check' passes but 'cargo build' fails when there are errors during monomorphization #99682
Description
Consider the following code:
pub struct G;
pub trait IAmArray{
const SIZE: usize;
}
impl<T, const N: usize> IAmArray for [T; N]{
const SIZE: usize = N;
}
impl G{
pub fn i_am_break_on_different_array_sizes<A, B>(_a: A, _b: B)
where A: IAmArray,
B: IAmArray,
{
trait CompileAssert{
const TRIGGER: ();
}
impl<A, B> CompileAssert for (A, B)
where A: IAmArray,
B: IAmArray,
{
const TRIGGER: () = if A::SIZE == B::SIZE {} else {panic!("You must provide arrays of same length")};
}
let _ = <(A, B) as CompileAssert>::TRIGGER;
}
}
fn main() {
G::i_am_break_on_different_array_sizes([0u8;5], [0u32;3]);
}When I do a check-build of this (rustc --emit=metadata), it works fine. But when I do a full build (rustc --emit=link), it fails:
error[E0080]: evaluation of `<([u8; 5], [u32; 3]) as G::i_am_break_on_different_array_sizes::CompileAssert>::TRIGGER` failed
--> test.rs:23:64
|
23 | const TRIGGER: () = if A::SIZE == B::SIZE {} else {panic!("You must provide arrays of same length")};
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the evaluated program panicked at 'You must provide arrays of same length', test.rs:23:64
|
= note: this error originates in the macro `$crate::panic::panic_2015` which comes from the expansion of the macro `panic` (in Nightly builds, run with -Z macro-backtrace for more info)
note: the above error was encountered while instantiating `fn G::i_am_break_on_different_array_sizes::<[u8; 5], [u32; 3]>`
--> test.rs:31:5
|
31 | G::i_am_break_on_different_array_sizes([0u8;5], [0u32;3]);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
error: aborting due to previous error
This is a post-monomorphization error. It is a long-known problem but I could not find an issue tracking it.
It seems worth tracking as it is rather surprising, so if we can find a way to fix it, that would be great. It also leads to issues with Miri such as rust-lang/miri#2423.
All that said, currently this is intended behavior. This RFC gives some relevant background:
cargo check should catch as many errors as possible, but the emphasis of cargo check is on giving a "fast" answer rather than giving a "complete" answer. If you need a complete answer with all possible errors accounted for then you must use cargo build. The rationale for this is that giving a "complete" answer requires (among other things) doing full monomorphization (since some errors, such as those related to associated consts, can only be caught during monomorphization). Monomorphization is expensive: instead of having to check each function only once, each function now has to be checked once for all choices of generic parameters that the crate needs. Given this performance cost and the fact that errors during monomorphization are fairly rare, cargo check favors speed over completeness.
A Rust program must compile with cargo build to be covered by Rust's standard stability guarantee.
This is related to #49292 but not the same -- that issue is about lints that arise pre-monomorphization, during MIR passes that are skipped in check builds. This here is about errors that appear even later during compilation.
Activity
RalfJung commentedon Jul 24, 2022
So what would be a way to fix it? The only option I can think of right now is to perform some kind of "fake monomorphization" with
--emit=metadata, to ensure that we actually evaluate all the constants that will be evaluated in a proper build, but skipping all the actual generation of LLVM IR.mqudsi commentedon Jul 25, 2022
Tangential question: is there an official guarantee as to what
cargo checkis supposed to catch?AngelicosPhosphoros commentedon Aug 26, 2022
I think, almost everyone expect it to catch everything as actual compilation would. This is a whole point of it: check compilation errors without generating code.
[-]'cargo check' passes but 'cargo build' fails[/-][+]'cargo check' passes but 'cargo build' fails when there are errors during monomorphization[/+]RalfJung commentedon Aug 24, 2023
#49292 is a related but different issue -- this one here is about errors due to const-eval running only during monomorphization (which
checkdoesn't run); #49292 is about errors that show up pre-monomorphization but that are emitted by lint passes which are not run duringcheck.RalfJung commentedon Mar 18, 2024
To fix this, we have to at least do something like a "mentioned item" monomorphizing traversal of the collector (also see #122568). Fundamentally there's no way to avoid having to touch the same function multiple times, since it might be instantiated with different types implementing a trait and their associated consts may behave differently.
So the regression for check times is likely going to be gigantic. It will be up to the compiler team to decide if they want to take that or not. (RFC 3477 puts this choice into t-compiler jurisdiction. It even mentions not doing any kind of monomorphization during check builds -- for performance reasons -- as a motivation for why we are okay with check builds passing when full builds fail.)
I think this issue will likely remain open at least until we decide to have "slow" and "fast" check builds. But it's still useful to have this as a central tracking location.
Auto merge of rust-lang#122744 - RalfJung:check-mono-items, r=<try>
RalfJung commentedon Mar 23, 2024
I did an implementation experiment in #122744 (to be able to measure the regression). Some work needs to happen before we can even do the experiment:
Kixunil commentedon Mar 7, 2025
I know effectively nothing about the compiler internals but couldn't it work by first pass also recording which generic functions instantiate constants (or have
constblocks) referencing generics and then only monomorphise those? If the assumption that there aren't many post-mono errors (presumably because most generic functions don't use such checks) is true then the slowdown from this should also be very small.RalfJung commentedon Jul 10, 2025
We are traversing a graph of generic function instances. Even if we know that we only care about 1% of the graph, that doesn't help much, since when we sit at some arbitrary node in the graph it may well be that we have to keep traversing until we eventually reach one of the "critical" nodes. I can't see a practical way to introduce backwards edges either... the entire graph is infinite, but only a finite part of it is reachable from the "monomorphization roots", and the question we are asking is whether any of those reachable nodes has a mono-time error.
The only practical option I see is to introduce a "slow check" mode that does the full monomorphization run, but then skips codegen. It'd take a bit to shake out all the cases where we actually error during codegen, but doing that should be feasible.