Specifying the semantics of when promotion to compile time could be fairly straightforward — a function call is guaranteed to be evaluated at compile time[1] if and only if either:
the call is in a guaranteed comptime context (eg a const item or block), or
the call is of an "eager" const function and every argument to the call is either:
an expression guaranteed to be evaluated at compile time, or
an expression that would be guaranteed to be evaluated at compile time except that it uses some number of local bindings which are all:
non-mut let bindings of Freeze types, and
assigned the value of an expression guaranteed to be evaluated at compile time.
This doesn't solve the deeper issues around post-mono errors and heuristic based promotion, though.
One way to meet the desire of the OP would be to allow a kind of const binding in function scope which is not a const item but instead behaves like a const block and is able to use generics as well as other const bindings within the scope. If such were to exist, then the OP ask would be to implicitly treat let bindings as const bindings when that's a legal change for the programmer to do (and it wouldn't be a "bad idea" for some unspecific heuristic of function better to evaluate at runtime if allowed.)
In the maximum, it's Zig's comptime — guaranteed eager partial evaluation in the function of any code only consuming constant and/or comptime inputs.
I'm ignoring the extra details around when we guarantee post-mono comptime errors vs allowing the unreachable code to be elided. ↩︎
I continue to be unsure why a guarantee here is important.
Sounds like it's asking more for just "well please const-prop harder around const fn things", if it's just that in release builds things aren't folding as much as people wish?
we'd error at compile-time if N is a compile-time constant 0. I doubt that's what you want.
I explicitly asked about code under conditionals above, for this exact reason.
I still don't understand the actual desire.
@nazar-pc I doubt you care about the way the MIR looks when it enters codegen -- that's an implementation detail, why would anyone outside the compiler team care about that? There's got to be some other thing you want to achieve and you think that having more constants computed early in MIR is somehow helpful. I think you are wrong about that, but it's hard to get to the bottom of this since we're still talking about the "how" before having even established the "why".
You mentioned Miri performance. If that's the motivation there's an easy first step to figure out if any of this could possibly help -- manually add the const {} blocks in a representative piece of code yourself and measure what that does to Miri performance. If that helps, especially if it helps significantly more than -Zmir-opt-level, that means that a better constant propagation pass, maybe a Miri-specific one, could be helpful. (I still wouldn't consider adding a new language feature just to make Miri faster.)
Maybe not relevant to this topic. When I saw that calling #[comptime] fn in a non-const context was an error, I was thinking, why can't the call to #[comptime] fn be automatically wrapped with const {} instead of reporting an error?
That was my first though as well, I was surprised it is not allowed to be called at runtime as well. But I guess there might be some non-trivial reasons for it.
The point of comptime is to allow things that we can't do at runtime. For example, it can allow looking at layout information and the "not at runtime" restriction is what makes it so we don't need to have huge space overhead from including that information for everything just in case.
You could also imagine comptime-only things that depend on stuff that only exists in the CTFE machinery, like "check if these two pointers are into the same allocation" that we can't do at runtime.
The const {} workaround is interesting — is that rust-gpu specific, or could you try something similar on the cpu side? The bottleneck seems to be when the compiler does the specialization.
Because it'd be very confusing. For instance, you could have
if precondition(value) {
let x = some_comptime_fn(value);
}
if we implicitly turn this call into a const block, then it will be evaluated even if the precondition does not hold!
The only code that we can implicitly wrap in const blocks is code that we know cannot fail to run. We actually do exactly that for code like &(1+2), which becomes basically const { &(1+2) } (and that is relevant as it makes the lifetime of that reference 'static). But for things involving arbitrary function calls, that's just a bad idea. For 1+2 without the surrounding reference we don't have to do that as that's just standard constant propagation which is better handled by the optimizer.
when i read the title i interpreted it as something else, functions that can only be called at compile time, with no runtime equivalent.
i'm not sure how useful that would be, but it could allow for things like concatenating &strs at compile time (as the compiler can just pull more static memory out of thin air)
What in my mind is, some_comptime_fn has been a comptime fn, so it can only be called at compile time. And then this would lead to two situations:
we don't wrap some_comptime_fn(value) in const block (current behavior), and will report an error tells comptime fn can only be called compile time.
we wrap it in const block automatically, and if value cannot be used at compile time, we will report an error like E0435. But if value can be used at compile time, I don't see there's any problem.
Yes, and the first one is better.
This is for the same reason that Rust does not automatically add ; when it realizes that you forgot to type one, to name just an obvious example of a general principle in how Rust was designed: code is meant to be read at least as much as it is written, and explicit is often better than implicit. Doing things implicitly is one of the major design flaws in JavaScript, and one reason why it is so hard to write reliable software in JavaScript. In Rust, we value reliability over minor conveniences such as implicit magic of this sort.
We can have an auto-applicable diagnostic telling you to wrap it in a const block, that's all fine. But we shouldn't do that transparently without the programmer even realizing what is happening.
(Also, the original issue here has nothing to do with const-first functions. So this is getting off-topic.)
That optional semicolon at the end is essentially automatic semicolon insertion. You could call it "optional semicolon" rather than "semicolon insertion", but it's the same thing: you could also say JavaScript has optional semicolons.
Are you seriously suggesting that optional semicolons at the end of a block are comparable with JavaScript's semicolon elision everywhere? If that's really what you mean then... I strongly disagree.
Anyway it was just an example, Rust does this everywhere. Other languages have the equivalent of implicit unwrap(), Rust makes it explicit. Other languages have implicit numeric conversions, Rust makes it explicit. And so on. Optimizing code for reading more than writing is a core value of Rust, and implicit const blocks go against that value.
There are plenty of situations where Rust is implicit rather than explicit, and they make it harder (at least for me) to read code rather than easier. More of them have been added since Rust 1.0. I would very much be in favour of not adding more.
The semicolon-at-the-end-of-a-block is a bit of an annoying inconsistency, but pretty minor. There are much larger implicitnesses that make a significant difference to program readability. Here are (IMO) the two largest:
let y = x.clone(); // could be Clone::clone(x), or Clone::clone(*x), etc.
match y {
None => panic!();
Some(z) => foo(z); // could be a borrow, or a move, or a copy
}
In both of these cases, it is possible for more than one option to be plausibly correct: in the first case, it's possible for both Clone::clone(x) and Clone::clone(*x) to typecheck (e.g. if x is an Rc); and in the second case, the behaviour is significantly different depending on whether or not y is a reference and it is usually not obvious from the surrounding code (to the extent that I, and I suspect most other Rust users, find it excessively difficult to work out whether a match expression is moving or borrowing without asking the compiler for help, and write such expressions primarily based on error messages rather than on reasoning about the code).
Interestingly, the first case is one where C is more explicit than Rust; C distinguishes between x.a which never dereferences x and x->a which always dereferences x. Although I'm not a fan of C's specific syntax for this, it does seem weird that the two are merged in Rust, disambiguated only by types, and occasionally requiring code to be rewritten in order to state which option you mean.