Like a modern C with lessons learned. Instead of macros it uses Zig itself to execute code at runtime (comptime). Custom allocators are the norm. No hidden control flow, everything is very explicit and easy to follow.
But it’s not only the language itself, it is also the tooling around it. Single unit of compilation has some nice properties, allowing to support colorless async. Fast compile times. Being able to use existing C code easily and having optimization across language boundaries. Cross compilation out of the box. Generally caring for performance in all aspects.
So for me it is a better C, low-level but still approachable and not having so much cruft.
> Instead of macros it uses Zig itself to execute code at runtime (comptime).
Nice. FWIW, I have a vague PL design in my head that does this despite being a much higher-level language. (For that matter, I think of my idea much as "like a modern Python with lessons learned".) Point being I definitely think this is a good idea.
To my understanding, the things actually called "macros" in Lisp also do this.
> Custom allocators are the norm.
On the other hand, this doesn't sound to me like an upside. Of course it's fine and well if it's easy to do this. But hopefully you'd prefer not to have to... ?
> No hidden control flow, everything is very explicit and easy to follow.
What sort of hidden control flow do you see in C? (Are modern code bases using setjmp/longjmp in new code?) I would have thought that C++ is where that really started, via exceptions. But I also don't think most programmers see that as problematic for understanding the code.
> Single unit of compilation has some nice properties, allowing to support colorless async.
Would appreciate some explanation of the theory here. Though it does occur to me that the languages I can easily think of with "coloured" async also don't exactly statically link everything all the time.
Also, how does all of this compare to Rust, in your view?
Comptime is very nice but certainly more limited then Lisp. You can't generate arbitrary code with it. But good enough to implement something like JSON serialization or Struct-of-Arrays in normal code that is readable.
Custom allocators are very nice. We are very much in manual memory management + optimization territory here. Having things like arena allocators makes a lot of difference in specific use-cases when you want/need every bit of performance. Also nice being able to switch the allocator for tests that is able to report leaks for example.
Yes, hidden control flow I mean something like exceptions, RAII or Rust's Dispose. So more a comparison to other languages than C.
The explanation I would refer to the talks "Don't forget to flush" or "Zig Roadmap 2026" from Andrew Kelley. Also the blog post "Zig's New Async I/O". I think it has something to do with being able to infer the required size of the stack, but already forgot the details.
https://kristoff.it/blog/zig-new-async-io/ https://youtu.be/f30PceqQWko?si=g2nLTE4ubWD14Zvn https://youtu.be/x3hOiOcbgeA?si=SUntYOYNOaxCRagc&t=3653
As to compared to Rust. The fast compile times are nice. Having a small language that you actually can understand helps to be productive. Not being restricted by the borrow checker makes it easier to implement some low-level things. Just being able to import most C code without wrapper makes the smaller ecosystem a much smaller problem. Rust is nice and certainly a good pick for many cases, but personally I often feel overwhelmed by the complexity and tons of tons of types for everything.
> Yes, hidden control flow I mean something like exceptions, RAII or Rust's Dispose. So more a comparison to other languages than C.
C has macros, which is the ultimate form of hidden control flow, where a symbol can expand to any arbitrary code... also hidden allocations and functions that can error, which you could argue isn't traditionally understood as hidden control flow, but it's still nice to know when stuff is allocated and/or can create an error
Rust dispose? I think you mean drop. But I don't see how that is hidden control flow. It's very clear when it drop is called.
It's clear once you know that an object implements the Drop trait, but you can't see that at the use site, ergo it's hidden (same goes for C++ destructors). Zig wants every call to be visible.
The tradeoff is between making sure you don't forget to write the cleanup call (Rust, C++) and making sure you don't forget to read the cleanup call (Zig). For low-level code I personally prefer Zig's tradeoff; others prefer the C++/Rust tradeoff.
A funny thing I’ve noticed is that some Rust programmers will explicitly call `std::mem::drop(…)` in tricky situations where it really matters, like releasing a mutex with complex interactions - even at the end of scope. I kind of like it whenever a lock is held for more than a few lines.
I think it’s a good compromise, because the consequences of forgetting it are way harsher. Memory leaks, deadlocks…
> I think it’s a good compromise, because the consequences of forgetting it are way harsher.
And easier to detect. Knowing that no operation is carried out unless you can see it is important to many who do low-level programming.
But there is no one right answer. Differences between programming languages, including those between Zig and Rust, are mostly about people's personal preferences because language designers rarely make choices that are universally inferior than others. When they differ, it's because both sides are reasonable and have their proponents.
What do you mean if an object implements a drop? Whether an object implements a drop has no bearing on when it is called. I mean a developer can manually call it. But it is always clear when it is called.
The point is the code is on another type. Any variable could by of a type that implements some Drop logic. It is mostly called implicitly where it is used, wether you as a programmer are aware of it or not. You would need to check.
In Zig you need to call everything explicitly, meaning in the function you need to call what you want to be executed, no other code will run. The decision if you want some cleanup logic is made at the point of usage, not by the type itself.
That is the point of it, you look at a function and directly see what happens right there, not in other files/packages.
People seem to underestimate this. One of the first reasons I noticed about c++ was trying to figure out what functions were being called in an overly complex inheritance hierarchy. The next was from hidden behavior from seemingly benign looking sequence of statements. Both of these are a barrier of entry for bringing in new coders to a complex code base.
Drop yes. Thanks for the correction.
It is clear when it is called, but you have to check in code you are not currently seeing as any type could implement it. May seem like a minor thing, but is not explicit at the point of usage. In Zig only code you call explicitly runs, meaning if there is no defer nothing happens at the end of the scope.
> On the other hand, this doesn't sound to me like an upside. Of course it's fine and well if it's easy to do this. But hopefully you'd prefer not to have to... ?
Why wouldn't you? You can often make your code simpler and more performant by using certain allocation strategy rather than relying on global allocator. Malloc/Free style strategy is also very error prone with complex hierarchical data structures where for example arena allocation can tie the whole lifetime to a single deallocation.
> Would appreciate some explanation of the theory here. Though it does occur to me that the languages I can easily think of with "coloured" async also don't exactly statically link everything all the ti
Async is moot point, it does not exist in zig right now, it used to but it was removed. There are plans to reintroduce it back, but using async as any sort of benefit for zig is not being honest.
Regarding async I kind of agree with you right now, but the new design is there and currently getting implemented. If you don’t believe it will work out or need to use async right now sure, use something else. It is not a stable language yet and very much WIP. I don’t think it’s dishonest to write about something that has a design and is worked on right now with a realistic chance of working out.
Sure and I'm excited for it (especially stackless), but I'd be wary of marketing something that doesn't actually exist yet.
> Like a modern C with lessons learned.
Yet no string types. So lessons not so well learned. Zig does remove many warts in C.
It has proper arrays and slices, even being able to define a sentinel value in the type system, so you know when it is a C string or a slice with a specific size with arbitrary values. Strings can become a problem if you need another encoding than the chosen one. Then you start to use byte slices anyway. You need to allocate a new one if you want to change just a part of it. Safer yes, but can produce unwanted overhead and you have to duplicate many APIs for strings and byte arrays as it is the case in Go.
It feels like C but raised with Java as its strict stepfather.
Many of those warts were already not present in other systems languages predating C, and others of similar age.
To be clear, the problem with C isn't macros, but text based macros. Syntax based macros are really good (as shown by Lisp and friends)
AST macros are very powerful. As to them being "good", that's debatable. They have advantages but also disadvantages. For one, they operate on syntax, so they're a separate metalanguage and not so pleasant to debug. For another, they are referentially opaque (i.e. when passed different expressions with the same meaning, they can produce different results), which makes them powerful but also mysterious.
What's interesting about Zig's comptime is that while it's strictly less powerful than AST macros, it can do a lot of what macros can do while not being a metalanguage - just Zig.
To be clear, we all know GP is talking about C macros.
Not only being text based but also having another separate language is problematic. Rust also has powerful declarative macros but it is it’s own language. Procedural macros and crabtime are imho a bit better for more complex cases as you write Rust.
I’m not sold that they’ve actually solved the coloring problem vs given it a different syntax.
> with lessons learned
A very small subset of possible lessons that could have been learned...
I've seen the custom allocators mentioned many times as central to the value proposition. Do the allocators shipped with the standard library compose the system allocator, or are they completely distinct from the *alloc family shipped as part of libc?
You can ofc use the *alloc from libc if you want. Otherwise there are different ones in the standard library, ArenaAllocator wraps another allocator, FixedBufferAllocator uses a given byte slice. I recommend to take a look at https://ziglang.org/documentation/master/std/#std.heap to get an overview.