Forcing overflow checks in const evaluation

Hi. This is my first time using this forum. If this is not appropriate place to ask this kind of questions, then I am sorry (please tell me where I should ask in such case).

This is kind of followup on a topic I started on Rust Users Forum - How to enable overflow checks in const evaluation. I have noticed, that there is inconsistency between Rust enabling overflow checks during const evaluation. For example following code:

pub const fn add(a: u8, b: u8) -> u8 {
    a + b
}

pub const FOO: u8 = const { 200 + 200 };
pub const BAR: u8 = const { add(200, 200) };

fails to compile with 2 errors (stating that overflow happened during compilation) in debug mode. But in release mode only calculating FOO panics. BAR is successfully computed and overflow is happily accepted. As far as I understand there is currently no way to enable all overflow checks in const evaluation, without also enabling them for runtime.

I feel that this is wrong and I would want to at least have an option (maybe even always forcing it) to enable those checks during const eval independently of adding overflow checks during runtime. Arguments other that "it feels good" would be:

  • it reduces inconsistency
  • I believe there is already a consensus in Rust community that overflow <=> a bug. This would allow Rust to catch more of those bugs at compile time (which is consistent with general Rust's approach of pushing invariant checking from runtime to comptime).
  • There is already a precedence of const evaluation having more rigorous requirements than "normal" code. For example it has stricter safety rules.

So I would like to ask how could such change to the language be proposed? Was it already discussed and dismissed? If so, could you please point me to such discussions? Do you think it would benefit the language? Is it even possible to implement (maybe this would be a breaking change)?

4 Likes

As an aside, this depends on your perspective. One of the intentions (though this may be relaxed now) is that const functions behave the same at runtime versus compile-time. Changing that would make it inconsistent.

(Not a refutation of your idea, which I think has merit. Just a clarification of what the intent was.)

1 Like

OTOH, with the overflow check they would still behave the same at runtime as they did at compile time as long as the code compiles. If it doesn't compile there is no runtime, so no inconsistency.

7 Likes

The main problem we have on the impl side is that we generate MIR once for a function, borrowck it, and then split it into const eval mir (unoptimized) and codegen mir (optimized). I do not see a good way to improve this situation.

While we could just always generate the overflow checks and have a pass that removes them from codegen mir, that's likely a prohibitive compilation time regression.

Ideally I would like all of debug-assertions enabled for const, including my own custom assertions. I suppose the pain of this would be in still only codegenning once, but it seems like that could be fine:

  • generate mir without "knowing" the value of debug-assertions
  • const eval mir → debug-assertions = true → run assertions
  • optimize mir → debug-assertions = false → dead code eliminate the assertions

And yes that should probably generalize to any cfg, or people (I'm people!) will start using debug-assertions to help detect const eval :upside_down_face:

2 Likes

It certainly can't for any cfg, because inside a disabled CFG doesn't even have to compile.

But really, lang has a wishlist item for fewer cfgs anyway -- if we could move more things to trait bounds, that'd be nice. (Imagine where Platform: Windows instead of cfg(windows), say.)

1 Like

If it's compiled with --const-cfg "debug-assertions" then the cfg'd code does have to compile (or fail the compilation). Sorry, I'm not suggesting that const eval should enable every cfg willy nilly. I don't know if miri has any existing notion of reified cfgs though, or if they are evaluated at a much earlier stage.

Note that debug_assert! uses if cfg!() and not #[cfg], so the code in that case does have to check both ways independent of the config.

"Compiling both" isn't really an option for #[cfg] because entire items can be present or not, or you can have fun things like

fn f() -> T {
    #[cfg(x)] { /* code */ }
    #[cfg(not(x))] { /* code */ }
}

which compiles fine and whichever block is included is the tail block the function returns the value of.