rust-lang / rust

Empowering everyone to build reliable and efficient software.
https://www.rust-lang.org
Other
97.96k stars 12.68k forks source link

'cargo check' passes but 'cargo build' fails when there are errors during monomorphization #99682

Open RalfJung opened 2 years ago

RalfJung commented 2 years ago

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 https://github.com/rust-lang/miri/issues/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.

This is related to https://github.com/rust-lang/rust/issues/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.

RalfJung commented 2 years ago

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 commented 2 years ago

Tangential question: is there an official guarantee as to what cargo check is supposed to catch?

AngelicosPhosphoros commented 2 years ago

Tangential question: is there an official guarantee as to what cargo check is supposed to catch?

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.

RalfJung commented 1 year ago

https://github.com/rust-lang/rust/issues/49292 is a related but different issue -- this one here is about errors due to const-eval running only during monomorphization (which check doesn't run); https://github.com/rust-lang/rust/issues/49292 is about errors that show up pre-monomorphization but that are emitted by lint passes which are not run during check.

RalfJung commented 7 months ago

To fix this, we have to at least do something like a "mentioned item" monomorphizing traversal of the collector (also see https://github.com/rust-lang/rust/pull/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.

RalfJung commented 7 months ago

I did an implementation experiment in https://github.com/rust-lang/rust/pull/122744 (to be able to measure the regression). Some work needs to happen before we can even do the experiment:

So the information to do this is simply not present because we don't store the MIR of things in rmeta files, or something like that? To to make this approach work I think we'd have to (a) make "mentioned items" traversal work without full MIR (based on Oli's proposal of a separate query that just returns the mentioned items), and then (b) do a mentioned items traversal of everything (rather than the usual used items traversal) in a check build.