Why can't the compiler detect that functions are constant without annotating them with const?

5.3k views Asked by At

In Rust, const functions are quite limited in what code can be put inside them, e.g. for loops aren't allowed, nor are any non-const function calls. I understand that there are issues with heap allocations in const functions, but why is it that for example the below code isn't valid:

fn add(a: u8, b: u8) -> u8 {
    a + b
}
const A: u8 = add(1, 2);

This would be valid if add was declared as a const function.

  • Why can't the Rust compiler detect that this is valid?
  • Why are const functions necessary at all?
  • Why aren't even for loops allowed inside them (even though while loops are)?
3

There are 3 answers

0
Ibraheem Ahmed On BEST ANSWER

Why are const functions necessary at all?

Constant functions are declared const to tell the compiler that this function is allowed to be called from a const context. Const-eval is done in a tool called miri. Miri is not capable of evaluating all expressions, and so const functions are pretty limited. There is a tracking issue of extensions that have been accepted and there is active work going on to make them const-compatible.

Why can't the Rust compiler detect that this is valid?

While technically possible, the Rust team decided against it. The primary reason being semantic versioning issues. Changing the body of a function, without changing the signature, could make that function no longer const-compatible and break uses in a const-context. const fn is a contract, and breaking that contract is an explicit breaking change.

Why aren't even for loops allowed inside them (even though while loops are)?

As to why for loops aren't allowed, they technically are. The only problem is that the Iterator::next method is not const, meaning that not some iterators may perform operations that are not const-evaluatable. Until a language construct that allows trait methods to be marked as const, or the ability to put const bounds, you will have to stick to regular loops.

const fn looop(n: usize) {
    let mut i = 0;
    loop {
        if i == n {
            return;
        }
        // ...
        i += 1;
    }
}
1
Silvio Mayolo On

We annotate const functions for the same reason we annotate pub functions: to make our intentions clear to the user. If you're writing a const function, you probably know it and are doing so intentionally (since, as you've stated, const functions are very limited in what they can do). By annotating it with a keyword, you

  1. Communicate very clearly to all callers that this is intended as a const function,
  2. Allow the compiler to verify that rather than silently letting it pass if you accidentally do something non-const in it, and
  3. Guarantee that future changes to this function's implementation won't cause problems downstream if someone wrongly assumed a function was or wasn't const.

As for the for loops, they're just not the intention. If you want to do compile-time code generation, we have a whole subsystem for that: it's called macros and it's amazing. const fn is for short functions that can be used in runtime context but that allow the added optimization of running at compile-time. As such, something like a loop (which may or may not terminate) adds a lot of complexity to that, and it can cause the compiler to fail to terminate. There are lots of infinite iterators, and determining whether the iteratee is infinite or not is equivalent to the halting problem. So it's easier to keep it simple and disallow this.

2
Masklinn On

Why can't the Rust compiler detect that this is valid?

It can. The reason it won't is that removing const is a backwards-incompatible change, and so must be opt-in: as the developer, by publishing a const function you're making a very specific promise to user. Having the const-ness of a function automatically change without warnings based on its implementation details would be terrible.

It's very similar to the reason why Copy isn't automatically implemented as soon as it could be: removing Copy breaks code, so Copy needs to be a guarantee which is opted into, while it's technically trivial change, it's absolutely not an innocuous change.

Why are const functions necessary at all?

Because the alternative (something like Zig's comptime or C++'s templates) doesn't really jive with Rust's safety philosophy, as well as its Elm-inspired desire to provide somewhat useful compiler errors.

Why aren't even for loops allowed inside them (even though while loops are)?

Have you considered looking? There's literally an RFC which tells you that: https://rust-lang.github.io/rfcs/2344-const-looping.html

for loops are technically allowed, too, but can't be used in practice because each iteration calls iterator.next(), which is not a const fn and thus can't be called within constants. Future RFCs (like https://github.com/rust-lang/rfcs/pull/2237) might lift that restriction.