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.
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.
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).
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.)
Neither Rust macros nor const fn are 100% what Zig comptime is but they have other properties that Zig comptime lacks. Apples and oranges.
So what are the major differences between the two anyways? Just to be sure.
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.
C++ ended up having to add consteval and constinit which really are compile-time.
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
const OVERSIZE: u32 = SIG_BITS.next_power_of_two() << 1;
We can just calculate what power of two is bigger than SIG_BITS and shift it left at compile time. But... pub(super) static SHORT_80: LazyLock<Rational> = LazyLock::new(|| Rational::fraction(1, 80).unwrap());
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).Well exactly. "if appropriate". So like C++'s `constexpr`, Rust doesn't make any guarantees about compile-time evaluation.
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.
let a = const {
let mut x: u64 = 0;
let mut k = 5;
loop {
if k == 0 {
break x;
}
k -= 1;
x += 2;
x *= x;
}
};(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.)
Looks like floating point math in const fn is coming. Here is the respective tracking issue: https://github.com/rust-lang/rust/issues/128288
so your claim is that rust compiler knows in advance which will be used by the target and adjusts its softfloat accordingly?
I'm not convinced. 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 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."
https://github.com/rust-lang/rfcs/blob/master/text/3514-floa...
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?
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.
You raised my hopes and dashed them quite expertly, sir. Bravo!