upvote
Are you saying that you think that Zig does not produce identical outcomes for comptime code regardless of when the code runs? What do you mean?
reply
Yes. For example in Rust it took a long time for floating point operations to be available at compile time and even now only a subset is. The reason is that a lot of energy and thought went into the issue of producing identical output (and what identical precisely means ) even when compilation is on a different processor than where the target runs.

As far as I know this is not a concern for Zig comptime.

reply
Zig Comptime uses softfloat so it has architecture independent determinism.

And the zig core team is absolutely concerned with bitwise determinism in the compiled artifacts, iirc this is why they rejected the sloppy bun PR to the compiler.

reply
Determinism and host/target agreement are two different properties.

The Zig core team is apparently not concerned enough about the second point to forbid transcendentals at comptime and this is something that'd be hard to take back, because I'd break existing code.

reply
Generally speaking, as a user of a language, stuff should just work while providing identical results to the target arch.

Which means that when cross compiling from x86 to ARM, if lets say, transcendentals provide different results then always the target archs implementations should be used, even if they have to be emulated.

If however, hypothetically, different x86 CPUs produce different results for transcendentals, and we can't control where the user will run our program, then imo the correct solution for the language is to provide a set of knobs for the user to communicate whether they favor accuracy or speed in this scenario. - but we can say there's no 'correct' decision in this case, only tradeoffs.

Forbidding transcendentals is not a correct decision as it adds a ton of compiler complexity (you have to track which functions use them transitively), and baffling UX - the user finds that comptime doesn't work because the function he uses might use transcendentals somewhere down the chain.

reply
there has been a ton of work on making comptime a pure and deterministic execution environment. I do not expect that work to stop. I don't know where you are getting the idea that it is not an important design consideration for the language.

to expand, if two different host platforms cross compiling to the same target platform have different results, I am almost certain that would be considered a compiler bug.

if you're pointing out that a runtime operation and a compile time operation might not agree, I'd be more interested in understanding when that would ever have any meaningful impact on anything. given the compilation is supposed to be deterministic, the difference can easily be addressed by comptime branching on target architecture in the rare case that it matters for your program.

reply
deleted
reply
Determinism and host/target agreement are two different properties. I meant the second one.

It's something Rust guarantees (without me having to take care of it e.g. by manually branching) and Zig does not.

reply
sure, it sounds like there is a difference here. I'm not aware of any stance by the Zig core team on the subject.

I am genuinely interested in a place that this matters for a program, or any practical consequence this has for an end user of the language.

no idea why your response was flagged originally.

reply
Floating point hardware on different CPU architectures don’t produce bitwise identical results, unfortunately. That’s the big issue.

As far as I’m aware, they do produce semantically identical results, but something like the specific bit pattern of a NaN value can theoretically vary, and people might do fun things like encoding extra information in those bits.

reply
About NaNs, you are right, because the standard does not mandate the use of a specific NaN for each kind of invalid operation.

Nonetheless, if you use a comparison function for which all NaNs are equivalent, different CPU architectures that are standard-compliant must produce bitwise identical results for the same sequence of operations.

Differences appear mainly when the compilers generate different operations or in a different order. Moreover, transcendental functions are computed using various approximations by the standard library, so if you use different libraries on different computers, you will get different results.

However, these are differences caused by software, not by hardware, and they happen even on the same computer when you use different versions of a compiler or of a standard library. Therefore such differences can be eliminated, if desired.

reply
rsqrt differs between intel and amd, out-of-bounds conversions differ between x64 and arm (saturating vs sentinel), denormal handling also varies iirc (not 100% sure about that), x64 has FTZ/DAZ separately, ARM has a combined FZ flag and this list is probably not exhaustive...
reply
The practical value is that moving a computation from runtime to compile time can then be purely an optimization, rather than potentially changing its semantics. Think generated lookup tables or numeric constants. Ideally f(x) means the same thing whether evaluated by the compiler or by the generated program.
reply
> Ideally f(x) means the same thing whether evaluated by the compiler or by the generated program.

Absolutely, that’s why I wanted to know what types of things aren’t covered yet, and that’s also why I don’t share your certainty that they won’t be resolved later.

reply
You'd have to read the discussions the Rust community has about this to get a picture. It's complicated.

From what I understand, there is hope for sqrt but there is no consensus on transcendentals.

reply
I think there's some misconceptions floating around here regarding both Rust and Zig's const-evaluation philosophies.

Rust is concerned about memory-safety, yes, but the only strict requirement for memory-safety when it comes to const-evaluation is as follows: "The only guarantee the type system needs is that evaluating `some_crate::SOME_CONST` will produce consistent results if evaluation is repeated in different compilation units" ( https://rust-lang.github.io/rfcs/3514-float-semantics.html ).

Beyond that, from a philosophical standpoint, Rust takes great pains to ensure that const functions produce identical results regardless of whether or not those functions are called at compile-time or at runtime. Rust has adopted this stance because it wants to reserve the right to opportunistically evaluate const-capable functions at compile time, as a performance optimization, even if the user has not explicitly asked for it (for that matter, Rust also does its best to const-evaluate non-const functions when it can). Because of this, Rust's assumption is that users would be annoyed if their program's visible behavior depends on whether or not the optimizer has exercised its discretion to evaluate a specific function at compile-time.

However, this is only a guideline, not a strict guarantee. There is one exception to the above rule: "when a floating-point operation produces a NaN result, the resulting NaN bit pattern is some deterministic function of the operation’s inputs that satisfies the constraints placed on run-time floating point semantics. However, the exact function is not specified, and it is allowed to change across targets and Rust versions, and even with compiler flags. In particular, there is no guarantee that the choice made in const evaluation is consistent with the choice made at runtime."

In other words, calling the `.to_bits()` function on a floating-point value that happens to be NaN is allowed to produce a different result at runtime than it does at compile-time (note that all compile-time evaluations are guaranteed to always produce the same result for a given toolchain version for a given target, as required above).

This exception is made because otherwise otherwise it would be basically impossible to support floating-point math at all, thanks to the way various platforms have implemented their floating-point functions in practice.

In contrast, Zig doesn't have such a philosophical compunction against a function's result being determined by whether or not it's being evaluated at compile-time, as shown by the existence of the `@inComptime` builtin. But Zig does still broadly attempt to make comptime deterministic, including going so far as to forbid I/O, though I don't see where any specific guarantees are documented in the Zig reference.

reply
In C++ as well, constexpr math introduced in C++23 has similar concerns regarding floating point accuracy.
reply
What's your source for this? Comptime Zig code can do pointer casts and whatnot, all emulated as if run on the target bitness/endianness/etc. And any operations that are undefined on the target platform result in a compile error. I've never had an issue cross-compiling.
reply
Endianness and pointer casts are the mechanical part and I would expect Zig to emulate them correctly.

Other parts, like floats are harder. This is where a difference shows. Rust is like: "Sorry, since we cannot uphold our guarantees, no transcendentals for you at comptime ", whereas Zig is chill about that and let you have your transcendentals even if results may differ between comptime and runtime. Different mindsets.

reply
The guarantees could be provided, if there would be a way to ensure that the compiler uses the same standard math library that is used by the executable program that is created.

This could be done, for instance, if the standard math library would be dynamically linked into the compiler, so the same library would be available for the compiled program.

reply
If you disregard performance, yes. Unfortunately forcing softfloat everywhere would result in abysmal performance, so this is not an option.
reply
Just to note, Rust has had compile-time floats for a while now, but yes.
reply
Yes, but no transcendentals on floats. You can add them and the like but you cannot calculate the sine.
reply
Now I see this response, I responded to the other comment.
reply
I assumed that it meant that if you ran code at compile time or at runtime, the results should be exactly the same given the same inputs.
reply
And they believe Zig comptime wouldn’t do that? For the same inputs? I’d love an example.
reply
Traditional stuff in this space are:

* floating point differences between the build machine and the target. By far the most common

* endiannes - code assumes little median runs on big endian

There’s other more subtle issues that can crop up but those are the big two.

Not saying I agree though - those can happen anyway when you run on two different machines anyway.

reply
Zig Comptime is softfloat
reply
I get it now. Important things but not things that I use often - hopefully addressed by 1.0.
reply
I'm not sure there is something to be addressed. Zig has just a different approach and Zig people different expectations.

In Rust we can expand what is possible at compile time without breaking existing code because we took a very careful approach only stabilizing what we are sure about. Some things will probably never be possible at compile time in Rust.

Zig is much more powerful but that also means they cannot take stuff away without breaking existing code and making comptime more restricted. So it is unlikely Zig will ever become like Rust in that regard, but that is ok - just different approaches.

reply
I hear you but I’m not sure it’s as dramatic as that, it’s also been quite predictable in my use over the last few years since I haven’t happened to use floating point or different endianness.
reply
I think the Rust approach is incorrect - if the runtime behavior can change based on the machine that the code gets run on, why is it so important the build time behavior has such a restrictive definition? Especially considering build.rs can be used to bypass that definition anyway. It feels like a weird cut to make that I can’t figure out the understanding for. It feels like it inherited the const philosophy of c++11 without reexamining if it’s actually a good idea.
reply
reproducible builds
reply
build.rs checkmate
reply
not mandatory
reply
Neither is using const
reply
correct, but it is desirable
reply
Doesn't comptime run under the target's float and endianness semantics? I need to check, but I believe they emulate the target when evaluating.
reply
Endianness yes, but float no. They just force software ieee754. It gives you exact bit accuracy on all build machines but it results in math that would differ between build time and run time depending if the function is evaluated as const or not
reply
sin(x) can produce different results at comptime and runtime. In Rust there is no sin(x) at comptime for precisely this reason.
reply
Interesting, I hadn’t run across that but that’s probably just my problem domain. I did find this bug that looks like it was resolved a year ago https://github.com/ziglang/zig/issues/24184 and I have also read they are doing some other work on floating point that will hopefully address other cross-platform inconsistencies.
reply
deleted
reply
Zig comptime feels easier and more effective in practice. I've had some fun const evaluating some stuff in Rust, but I needed to use a bunch of annoying imperative hacks because so much of the functional stuff wasn't supported in const context back then. It's probably a bit better these days.

For one of my crates I needed to have a build script make a bunch of lookup tables as separate files for me to `include_bytes!` because at the time I couldn't generate a bunch of floating point conversions in const.

reply
Certainly every new Rust release tends to have either new things which were stabilized as const on day one, or things which already existed but now have stable const.

The biggest constraint today on Rust's constant evaluation compared to where you'd expect is that trait implementations can't ever be constant, this obviously means you can't call SomeTrait::function in your constant, even if you can see the implementation of SomeTrait::function and if it were not a trait it'd obviously be constant -- but it also means sugar like Rust's for loop, which de-sugars into trait invocations, can never be constant today.

I think we can expect that to get fixed in the relatively near future, but I'd have said that last year too so what do I know.

If you have C++ experience you'd probably want a lot more. C++ is allowed to allocate inside constant evaluation, and I believe in C++ 26 it's now even allowed to persist the allocation to runtime rather than being required to always clean up during compilation, so that's a much bigger set of crazy things you can do at compile time.

reply
> it also means sugar like Rust's for loop, which de-sugars into trait invocations, can never be constant today.

Yep, that was my annoyance.

Const allocation is possible in Rust as an unstable feature. Not sure if you can persist it to runtime, though you can persist a reference which will become a static reference. I think it being unstable is why I needed `include_bytes!`.

reply
FWIW const traits are progressing quite nicely. It will also allow for Default to be used in const context, which is quite handy (and will integrate nicely with default field values, which only allows consts today in nightly).
reply
There’s a few comptime crates out there. Crabtime is iirc the most mature and popular
reply
I don't know if I would ever depend on something like that for a library crate. There's enough syn+proc_macro2 pollution in the ecosystem already.
reply
It does seem like maybe a crate with convenience macros so you can get a `&'static [Foo; N/size(Foo)]` rather than `&'static [u8; N]` and maybe even a delicious compile time "Hey jerk, that's not a valid Foo, you screwed up" if appropriate from your pre-baked data files, would be nice regardless of having more constant eval.
reply
deleted
reply