Just for completeness: Rust has functions which can be run at comptime as well. They are called const fn and Rust has them out of the box, no crate required. They are also true Rust and not macros with a separate syntax.
They are still not an adequate substitute for Zig's comptime feature. For one and in a sense they are much more limited than comptime functions in Zig but for another (and for better or worse) they also have much higher aspirations than Zig.
const fn must always be able to be run at compile time or run time and always produce bit-identical results. This is much harder than it looks at first glance because it must also uphold in a cross-compiling scenario where the compile time environment can be vastly different from the run time environment.
This requirement also forbids any kind of side effect, so Rust const fn are essentially pure functions and I've heard them called like that.
What Rust is missing is reflection and the ability to define types and functions via code. Zig's comptime is often used for this: to generate code (for example, a serialiser for a type) or to generate types (generics being a typical thing, but lots of other usages are viable).
You can define types and functions via code via macros. For example, [1] which creates a new sibling type and injects a ::builder method into your type.
And you can add reflection [2]. So if you can add what you need via crates, is the language actually missing it or is it just not as ergonomic / performant as it needs to be or is it an education problem?
Rust has compile time environment variables and const fns can parse them. It's one very nice and easy way to experiment with or configure rust code at compile time, and should be explored more.
The functions are pure but they take an input from a built-in (looks like a macro) that reads the environment variable at compile time.
Also, you only compile once, so how could you tell the difference? You could say - if it was using const fn that it's a "templated" function that depends on compile time settings.
There's two lookups that can occur, distinguishing them makes it clearer.
Looking up the value of an environment variable at runtime is not a const operation, and produces an error if you try to do it in a const fn.
Looking up the value of an environment variable during compile time _can_ be done in a const context, but it'll only happen once. The environment should be considered an input to a const fn, and that makes it "pure".
EDIT: These two operations can both be done in non-const functions too, they're different functions (well, one's a macro).
It's the other way around. Rust has always had contexts that are guaranteed to be compile-time (const and static items), and gradually added the ability to run some subset of the language at compile-time (const fn) specifically to accommodate const/static items (e.g. to replace the old lazy_static with the modern LazyLock), and naturally also allows these functions to run at runtime if you want (and in what would otherwise be a runtime context, you can enforce compile-time evaluation with a const block).
Huh? What is it you think Rust copied here? I agree that the choice in C++ is essentially worthless, so that in practice you can write functions which are definitely never executed at compile time and aren't constant in any sense, label them constexpr and that compiles anyway. It just becomes yet more noise C++ programmers learn to type by reflex to get the correct behaviour from their compiler, joining explicit.
But in Rust that's not what you're getting. Rust's const fn is none of the options C++ decided it needed, Rust says if the parameters are themselves constants then we promise we can evaluate this at compile time and if appropriate we will -- this means we can use Rust's const fn where we'd use C++ consteval, but the function can also be called at runtime with variable parameters - and we can use Rust's const where we'd use C++ constinit, calling these const fn with constant parameters.
Because Rust is more explicit about safety of course, we can often get away with claiming some value is "constant" in C++ despite actually figuring out what it is at runtime, and Rust isn't OK with that, for example in my code
The Rational type is a big rational, it owns heap allocations so we'll just make one once, at runtime, and then re-use it whenever we need this particular fraction (it's for calculating natural logarithms of arbitrary computable real numbers).
If we want a constant context, we can say so. Because Rust is expression oriented we can write for example a loop (though if you're unfamiliar with Rust it may not be clear why a for loop can't work yet, other loops are fine) and wrap the whole expression in a const block and that'll be evaluated at compile time. For example:
let a = const {
let mut x: u64 = 0;
let mut k = 5;
loop {
if k == 0 {
break x;
}
k -= 1;
x += 2;
x *= x;
}
};
Yeah, it's nice that const blocks are stable now. Before them, you had to use hacks like defining a trait impl with an associated const, which was verbose and messy.
(If I recall correctly, one of the big questions was "Will const blocks unreachable at runtime still be evaluated at compile time?" It looks like the answer was to leave it unspecified.)
> Zig's `comptime` must be evaluated at compile time.
Yes, and the equivalent in Rust is any constant context, such as a const item, or a const block inside of a non-const function. Anything in a constant context is guaranteed to run at compile-time.
It's not quite as bad as it sounds, because the only difference is that the representation of NaN (the sign and the payload bits) isn't guaranteed to be stable. If you're not relying on any specific representation of NaN, then floating-point math in const fn is identical, and observed differences would be considered a soundness bug in the Rust compiler.
I had to look this up because it is a while that I tried to use floating point math in a const fn and it seems that the differences you described have been decided to be acceptable.
Oh, nice. Sometime I find it really hard to track the status of new Rust features. For example the tracking issue I linked is still open with "Stabilize" missing.
what happens if you compile on a system that has a different precision than the system you run on? like suppose you compile on a 64 bit system targetting 32 bit embedded with an fp accelerator or a 16 bit system with softfloat?
I'm not personally familiar with the implementation, but Rust's const fn is evaluated using an interpreter called MIRI with its own softfloat implementation, and therefore isn't limited by the precision of the host platform. The act of cross-compilation shouldn't pose a problem, and would be a soundness issue in the compiler if it did.
As long as the target is compliant with IEEE 754, which is what Rust expects, it shouldn't be an issue. The only platform that I know of that causes problems is extremely old pre-SSE 32-bit x86, where floats sometimes have 80-bit precision and which can't be worked around because of LLVM limitations which nobody's going to fix because the target is so obscure. Rust will probably just end up deprecating that target and replacing it with a softfloat equivalent.
> so your claim is that rust compiler knows in advance which will be used by the target and adjusts its softfloat accordingly?
Rust performs FP operations using the precision of the underlying type. For compile time evaluation this is enforced by Miri, and for runtime evaluation this is enforced by carefully emitting the appropriate LLVM IR.
> IIRC there are cases for SIMD where there is only a 2 ULP guarantee and some tryhard silicon gives you 1 ULP for the same opcode.
Rust only permits operations in constant contexts when it's confident that it can make useful guarantees about their behavior. In particular, FP ops in const contexts are currently limited as follows:
"This RFC specifies the behavior of +, - (unary and binary), *, /, %, abs, copysign, mul_add, sqrt, as-casts that involve floating-point types, and all comparison operations on floating-point types."
Last time I checked the float functions that have no bit-identical results (mostly transcendental functions) were missing from Rust's const fn for exactly that reason.
> They are still not an adequate substitute for Zig's comptime feature. For one and in a sense they are much more limited than comptime functions in Zig
The syntactic restrictions don't really matter; it's still Turing-complete. The key difference is that types are values in Zig but not in Rust, which is a core design feature of the language and can't be changed easily.
Rust doesn't have Zig's comptime feature. Rust's const fn's are normal functions that are capable of running at compile time. It's an optional optimisation; it doesn't exist any additional semantic capabilities because they also need to be able to run at runtime.
Zig's comptime functions only run at compile time, so they can do extra things - in particular manipulating types - that you can't do if your function needs to run at runtime. (Don't mention dependent types.)
Note that dependently-typed code also effectively "runs at compile-time", it's inherent to that programming model. You can "extract" an ordinary program from dependently-typed code which you can then compile to a binary and run as usual, but then that program will not feature dependent types in their full generality.
> Zig's comptime functions only run at compile time, so they can do extra things - in particular manipulating types - that you can't do if your function needs to run at runtime.
Careful, I'm not sure this is true. I haven't found a Zig comptime function that doesn't also work just as well at runtime function.
This is, in fact, the primary characteristic that makes Zig comptime easier to reason about than any "macro" system. If something is wrong in my comptime function, I can normally make a small adjustment to force it to be a runtime function that I can step through and probe and debug.
It's sort of a unification of compile time and run time semantics and it is long overdue. The late John Shutt's Scheme-alike Kernel (https://web.cs.wpi.edu/~jshutt/kernel.html) sort of approached this as did old-school Tcl.
They are still not an adequate substitute for Zig's comptime feature. For one and in a sense they are much more limited than comptime functions in Zig but for another (and for better or worse) they also have much higher aspirations than Zig.
const fn must always be able to be run at compile time or run time and always produce bit-identical results. This is much harder than it looks at first glance because it must also uphold in a cross-compiling scenario where the compile time environment can be vastly different from the run time environment.
This requirement also forbids any kind of side effect, so Rust const fn are essentially pure functions and I've heard them called like that.