What Zig felt like, coming from Rust
besok.github.io203 points by ksec 17 hours ago
203 points by ksec 17 hours ago
For various purposes I work on a language comparison project that includes C and candidate successors such as Go, Zig. One question is the language to use for an archival port of a 1980's computer algebra system written in 32-bit K&R C. While Zig is a great debugging compiler, it's not yet stable enough to be the best target language for archival purposes.
https://github.com/Syzygies/Compare
So you're in a restaurant where you don't speak the language, you can't read the menu, but you see three price points for set meals featuring the house specialty. (Say, "Crossing the Bridge" noodles in Yunnan.) Which do you choose? My tour guide, the author Fuchsia Dunlop, later agreed with me this is obvious: The middle choice.
So you're choosing between Go and C23 as candidate successors to K&R C. They both have "royal blood". One got the name. Knowing nothing more, which do you choose?
The answer is equally obvious. The one that got the name also got the warts.
Despite my usual rants, naturally C23.
Minimal rewrite due to the breaking changes introduced in C23 versus K&R C, while the others are a complete rewrite.
Even if the syntax is a bit of a kludge there are now ways to indicate bounds on function arguments.
>https://github.com/Syzygies/Compare
You might want to take a look at Odin as well.
You mention there that you like Ruby and that you wanted Ruby with types (and that you looked at Crystal).
I've been messing around with a language that I summarize as:
ALOE = Scheme + Smalltalk + Types
https://github.com/dharmatech/2026-09-02-aloe-racket
One of the examples is a very basic computer algebra simplifier:
https://github.com/dharmatech/2026-09-02-aloe-racket/blob/ma...
That simplifier is based on a computer algebra library in Scheme:
Middle may be wrong (it is a marketing trick to add 3rd outragesly expensive option, to make 2nd option look reasonable). The correct answer is “it depends” (even how long you should spend on choosing may depend on context too).
For example, write in whatever language you know best, then translate to a more appropriate language using LLMs once the desired behavior can be checked automatically. It is a tactic that works in some cases.
It can be used as a marketing trick, possibly because of the logic in GP post. Marketing people use this to trick people, which is different from it being a marketing trick in and of itself.
If you’re C suite, ignorant and stupid and don’t know what you’re doing which choice minimises your chance of screwing up? The above is an analogy for that situation.
If you’re not stupid it fails because you can find out and then make an informed decision. An informed decision will dominate the suggested strategy in terms of chances of not screwing up.
This really should be obvious.
(In the analogy, your phone has an automatic translator and you use it).
Note that I have made no argument for one language over another.
One of the section headings says "Mutation vs. immutable monad is the core difference" but this is not true.
The code in that section is clear in its purpose and broad outline: "Give me a function and data; if the data is bare apply the function to it; if the data is a container apply the function to each item inside it."
You can absolutely do that with immutable data structures in Zig. You just have to pass an allocator to the function (i.e. instead of calling data.flat_map(f), you call data.flat_map(a, f) where a is your allocator).
That the Zig version of the code does mutation is a matter of programmer choice, not something imposed by the language.
(Also, what do monads have to do with it?)
Further up he discusses that using a functional paradigm is possible, but concludes that it doesn’t feel like a practical choice because of how Zig does memory management:
> But the language quickly forces you to diverge from the functional style, mostly because you’re now dealing with allocators directly, and a genuinely pure functional approach means constantly constructing new structures. That’s either expensive in memory or expensive in the manual bookkeeping needed to avoid it.
So I don’t think he’s trying to say that it’s literally impossible. It felt more like the result of a good faith attempt to understand how Zig itself actually wants to be used, and to compare that to how he’s used to using Rust.
I actually liked that he did it. So many other comparisons want to evaluate one language against the other language’s values. But I don’t want to know how well Zig can do Rust; I want to know how well Zig accomplishes its own goals, and what those goals are.
So, what you're not seeing is that even though the Rust doesn't announce mutation the implementation may be mutation anyway if that's probably faster/ cheaper.
unsafe impl<I, U, F> InPlaceIterable for FlatMap<I, U, F> where
I: InPlaceIterable,
U: BoundedSize + IntoIterator,
What you're seeing is the graceful goose on the water moving forward at pace. Beneath there is frantic action to make that happen. The Rust surface was more a maintainable immutable operations, but the implementation is a frantic whirling mutation like the Zig.In Zig you end up spending a lot of time writing How to do a thing, where in Rust you only wrote What the thing is and the machine did it. There are edge cases where Zig's explicitness wins for the best programmers, but there just aren't enough of those cases or those programmers for this to net out IMNSHO.
yes people don't understand that there is often a runtime performance cost to explictness as well
Or more of an opportunity cost. You are able to get to 100% performance with dedicated attention from skilled engineers. But otherwise normal gods just manage to get a bare 60% out, versus the 80% they would get in rust. (Numbers are fictional.)
Monads are where flat_map came from. A monadic programming style is where flat_map is used pervasively eschewing other APIs.
> Zig, by comparison, offered little beyond syntax highlighting and basic autocompletion.
Zig has a language server and "The majority of LSP features are supported" (https://github.com/zigtools/zls#features).
> Rust: Drop runs automatically at scope end, so this specific bug simply doesn’t exist.
That's why having no auto-destructors is a dead-end. This is the greatest mistake of such languages like Zig or Odin.
You can statically analyze for leaks.
But with static analysis it's still possible to miss some leaks or to have false-positives. That's why an integrated language mechanism preventing such leaks is much better.
Two additional points:
1. Tooling (as an extension to the mentioned IDE support point). Zig and Rust are both praised for their tooling and I think rightfully so. The C/C++ interop story and the cross-compiling story in Zig are great. From the standpoint of a working practitioner though I think Rust is way ahead. Not surprising given that Zig is much younger, but something to keep in mind.
2. Compile Time Stuff: Here Zig is praised and Rust not so much. I think this is undeserved. Rust has much higher aspirations for their compile time features, namely that outcome must be identical regardless when the code runs. This is a very useful property but makes the task much harder and fundamentally incomparable with Zig comptime.
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?
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.
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.
In C++ as well, constexpr math introduced in C++23 has similar concerns regarding floating point accuracy.
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.
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.
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.
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.
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.
> 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.
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.
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.
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.
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.
Just to note, Rust has had compile-time floats for a while now, but yes.
Yes, but no transcendentals on floats. You can add them and the like but you cannot calculate the sine.
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.
And they believe Zig comptime wouldn’t do that? For the same inputs? I’d love an example.
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.
I get it now. Important things but not things that I use often - hopefully addressed by 1.0.
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.
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.
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.