Low-Level Optimization with Zig
285 points
1 day ago
| 13 comments
| alloc.dev
| HN
WalterBright
5 hours ago
[-]
> The flexibility of Zig's comptime has resulted in some rather nice improvements in other programming languages.

Compile time function execution and functions with constant arguments were introduced in D in 2007, and resulted in many other languages adopting something similar.

https://dlang.org/spec/function.html#interpretation

reply
dustbunny
1 day ago
[-]
What interests me most by zig is the ease of the build system, cross compilation, and the goal of high iteration speed. I'm a gamedev, so I have performance requirements but I think most languages have sufficient performance for most of my requirements so it's not the #1 consideration for language choice for me.

I feel like I can write powerful code in any language, but the goal is to write code for a framework that is most future proof, so that you can maintain modular stuff for decades.

C/C++ has been the default answer for its omnipresent support. It feels like zig will be able to match that.

reply
haberman
1 day ago
[-]
> I feel like I can write powerful code in any language, but the goal is to write code for a framework that is most future proof, so that you can maintain modular stuff for decades.

I like Zig a lot, but long-term maintainability and modularity is one of its weakest points IMHO.

Zig is hostile to encapsulation. You cannot make struct members private: https://github.com/ziglang/zig/issues/9909#issuecomment-9426...

Key quote:

> The idea of private fields and getter/setter methods was popularized by Java, but it is an anti-pattern. Fields are there; they exist. They are the data that underpins any abstraction. My recommendation is to name fields carefully and leave them as part of the public API, carefully documenting what they do.

You cannot reasonably form API contracts (which are the foundation of software modularity) unless you can hide the internal representation. You need to be able to change the internal representation without breaking users.

Zig's position is that there should be no such thing as internal representation; you should publicly expose, document, and guarantee the behavior of your representation to all users.

I hope Zig reverses this decision someday and supports private fields.

reply
unclad5968
22 hours ago
[-]
I disagree with plenty of Andrew's takes as well but I'm with him on private fields. I've never once in 10 years had an issue with a public field that should have been private, however I have had to hack/reimplement entire data structures because some library author thought that no user should touch some private field.

> You cannot reasonably form API contracts (which are the foundation of software modularity) unless you can hide the internal representation. You need to be able to change the internal representation without breaking users.

You never need to hide internal representations to form an "API contract". That doesn't even make sense. If you need to be able to change the internal representation without breaking user code, you're looking for opaque pointers, which have been the solution to this problem since at least C89, I assume earlier.

If you change your data structures or the procedures that operate on them, you're almost certain to break someone's code somewhere, regardless of whether or not you hide the implementation.

reply
dgb23
22 hours ago
[-]
> I've never once in 10 years had an issue with a public field that should have been private, however I have had to hack/reimplement entire data structures because some library author thought that no user should touch some private field.

Very similar experience here. Also just recently I really _had_ to use and extend the "internal" part of a legacy library. So potentially days or more than a week of work turned into a couple of hours.

reply
haberman
16 hours ago
[-]
Most data structures have invariants that must hold for the data structure to behave correctly. If users can directly read and write members, there's no way for the public APIs to guarantee that they will uphold their documented API behaviors.

Take something as simple as a vector (eg. std::vector in C++). If a user directly sets the size or capacity, the calls to methods like push_back() will behave incorrectly, or may even crash.

Opaque pointers are one way of hiding representation, but they also eliminate the possibility of inlining, unless LTO is in use. If you have members that need to be accessible in inline functions, it's impossible to use opaque pointers.

There is certainly a risk of "implicit interfaces" (Hyrum's Law), where users break even when you're changing the internals, but we can lessen the risk by encapsulating data structures as much as possible. There are other strategies for lessening this risk, like randomizing unspecified behaviors, so that people cannot take dependencies on behaviors that are not guaranteed.

reply
xboxnolifes
14 hours ago
[-]
> Most data structures have invariants that must hold for the data structure to behave correctly. If users can directly read and write members, there's no way for the public APIs to guarantee that they will uphold their documented API behaviors.

You can, just not in the "strictly technical" sense. You add a "warranty void if these fields are touched" documentation string.

reply
pjmlp
1 hour ago
[-]
I prefer liability when devs misuse software with consequences for society infrastructure.
reply
Ygg2
4 hours ago
[-]
That's honestly horrible. It's like finding your job is guaranteed by a pinkie promise, or the equivalent.
reply
the__alchemist
7 hours ago
[-]
Like unclad, I disagree that not having private fields is a problem. I think this comes down to programming style. For an OOP style (Just one example), I can see how that would be irritating. Here's my anecdote:

I write a lot of rust. By default, fields are private. It's rare to see a field in my code that omits the `pub` prefix. I sometimes start with private because I forget `pub`, but inevitably I need to make it public!

I like in principle they're there, but in practice, `pub` feels like syntactic clutter, because it's on all my fields! I think this is because I use structs as abstract bags of data, vice patterns with getters/setters.

When using libraries that rely on private fields, I sometimes have to fork them so I can get at the data. If they do provide a way, it makes the (auto-generated) docs less usable than if the fields were public.

I suspect this might come down to the perspective of application/firmware development vice lib development. The few times I do use private fields have been in libs. E.g. if you have matrix you generate from pub fields and similar.

reply
pjmlp
1 hour ago
[-]
One the key principles for modular software is encapsulation, it predates OOP by decades, and at least even C got that correct.
reply
dgb23
22 hours ago
[-]
Some years ago I started to just not care about setting things to "private" (in any language). And I care _a lot_ about long term maintainability and breakage. I haven't regretted it since.

> You cannot reasonably form API contracts (...) unless you can hide the internal representation.

Yes you can, by communicating the intended use can be made with comments/docstrings, examples etc.

One thing I learned from the Clojure world, is to have a separate namespace/package or just section of code, that represents an API that is well documented, nice to use and more importantly stable. That's really all that is needed.

(Also, there are cases where you actually need to use a thing in a way that was not intended. That obviously comes with risk, but when you need it, you're _extremely_ glad that you can.)

reply
haberman
17 hours ago
[-]
I have the opposite experience. Several years ago I didn't worry too much about people using private variables.

Then I noticed people were using them, preventing me from making important changes. So I created a pseudo-"private" facility using macros, where people had to write FOOLIB_PRIVATE(var) to get at the internal var.

Then I noticed (I kid you not) people started writing FOOLIB_PRIVATE(var) in their own code. Completely circumventing my attempt to hide these internal members. And I can't entirely blame them, they were trying to get something done, and they felt it was the fastest way to do it.

After this experience, I consider it an absolute requirement to have a real "private" struct member facility in a language.

I respect Andrew and I think he's done a hell of a job with Zig. I also understand the concern with the Java precedent and lots of wordy getters/setters around trivial variables. But I feel like Rust (and even C++) is a great counterexample that private struct variables can be done in a reasonable way. Most of the time there's no need to have getters/setters for every individual struct member.

reply
josephg
28 minutes ago
[-]
> Then I noticed (I kid you not) people started writing FOOLIB_PRIVATE(var) in their own code.

If it’s in an internal monorepo, this should be super easy to fix using grep.

Honestly it sounds like a great opportunity to improve your API. If people are going out of their way to access something that you consider private, it’s probably because your public APIs aren’t covering some use case that people care about. That or you need better documentation. Sometimes even a comment helps:

    int _foo; // private. See getFoo() to read.
I get that it’s annoying, but finding and fixing internal code like this should be a 15 minute job.
reply
geysersam
14 hours ago
[-]
It's about the contract with the users. I don't think you should worry about breaking someone using the private fields of your classes. Making a field private, for example by prefixing an underscore in Python, tells the users "for future maintainability of the software I allow myself the right to change this field without warning, use at your own peril".

If you hesitate changing it because you worry about users using it anyway you are hurting the fraction of your users who are not using it.

reply
haberman
7 hours ago
[-]
This is company code in a monorepo. If a change breaks users, it will simply be rolled back.

Everyone is brainstorming ways to work around Zig's lack of "private". But nobody has a good answer for why Zig can't just add "private" to the language. If we agree that users shouldn't touch the private variables, why not just have the language enforce it?

reply
SpaghettiCthulu
4 hours ago
[-]
Because sometimes the user really wants to access those fields, and if the language enforces them being private, the user will either copy-paste your code into their project, or fork your project and make the fields public there. And now they have a lot of extra work to stay up-to-date when compared to just making the necessary changes if those fields ever change had they been public.
reply
haberman
3 hours ago
[-]
I would be satisfied if the language supported this use case by offering a “void my warranty” annotation that let a given source file access the privates of a given import.

Companies with monorepos could easily just ban the annotation. OSS projects could easily close any user complaint if the repro requires the annotation.

This seems like a great compromise to me. It would let you unambiguously mark which parts of the api are private, in a machine checkable way, which is undoubtedly better than putting it into comments. But it would offer an escape hatch for people who don’t mind voiding their warranty.

reply
pjmlp
1 hour ago
[-]
That is the beauty of binary libraries, they enforce encapsulation.
reply
pjmlp
1 hour ago
[-]
C++ precedent though, getters and setters were widely adopted in C++ frameworks before Java was even an idea.
reply
raincole
14 hours ago
[-]
> And I can't entirely blame them

You can't blame them, but they can't blame you if you break their code.

reply
magicalhippo
11 hours ago
[-]
I started using Boost's approach, that is keep those things public but in their own clearly-named internal namespace (be it an actual namespace or otherwise).

This way users can get to them if they really need to, say for a crucial bug fix, but they're also clearly an implementation detail so you're free to change it without users getting surprised when things break etc.

reply
tayo42
14 hours ago
[-]
That's pretty much why I never bother with the underscore prefix convention when using python. If someone wants to use it they'll do it anyway.
reply
pdpi
11 hours ago
[-]
> The idea of private fields and getter/setter methods was popularized by Java, but it is an anti-pattern.

I agree with this part with no reservations. The idea that getters/setters provide any sort of abstraction or encapsulation at all is sheer nonsense, and is at the root of many of the absurdities you see in Java.

The issue, of course, is that Zig throws out the baby with the bath water. If I want, say, my linked list to have an O(1) length operation, i need to maintain a length field, but the invariant that list.length actually lines up with the length of the list is something that all of the other operations need to maintain. Having that field be writable from the outside is just begging for mistakes. All it takes is list.length = 0 instead of list.length == 0 to screw things up badly.

reply
ArtixFox
9 hours ago
[-]
You can have a debug time check.
reply
Galanwe
4 hours ago
[-]
> You cannot reasonably form API contracts (which are the foundation of software modularity) unless you can hide the internal representation

Python is a good counter example IMHO, the simple convention of having private fields prefixed with _/__ is enough of a deterrent, you don't need language support.

reply
eddd-ddde
1 day ago
[-]
Just prefix internal fields with underscore and be a big boy and don't access them from the outside.

If you really need to you can always use opaque pointers for the REALLY critical public APIs.

reply
haberman
23 hours ago
[-]
I am not the only user of my API, and I cannot control what users do.

My experience is that users who are trying to get work done will bypass every speed bump you put in the way and just access your internals directly.

If you "just" rely on them not to do that, then your internals will effectively be frozen forever.

reply
lll-o-lll
22 hours ago
[-]
Or you change it and respond with “You were warned”.

I seriously do not get this take. People use reflection and all kinds of hacks to get at internals, this should not stop you from changing said internals.

There will always be users who do the wrong thing.

reply
jjmarr
20 hours ago
[-]
Let's say I'm in a large company. Someone on some other team decided to rely on my implementation internals for a key revenue driver, and snuck it through code review.

I can't break their app without them complaining to my boss's boss's boss who will take their side because their app creates money for the company.

Having actual private fields doesn't 100% prevent this scenario, but it makes it less likely to sneak through code review before it becomes business-critical.

reply
lll-o-lll
18 hours ago
[-]
You can still create modules in zig, just use the standard handle pattern as you might in c/c++. I think that many of us have worked in “large company”, and the issue you describe is not resolved with the “private” keyword. You need to make your “component/module” with a well defined boundary (normally dll/library), a “public interface” and the internals not visible as symbols.

That doesn’t save you in languages that support reflection, but it will with zig. Inside a module, all private does is declare intent.

In languages with code inheritance, I think inheritance across module boundaries is now widely viewed as the anti-pattern that it is.

reply
nicoburns
23 hours ago
[-]
> If you "just" rely on them not to do that, then your internals will effectively be frozen forever.

Or they will be broken when you change them and they upgrade. The JavaScript ecosystem uses this convention and generally if a field is prefixed by an underscore and/or documented as being non-public then you can expect to break in future versions (and this happens frequently in practice).

Not necessarily saying that's better, but it is another choice that's available.

reply
9d
23 hours ago
[-]
[flagged]
reply
lll-o-lll
22 hours ago
[-]
Not everyone has to follow the MS approach of not breaking clients that rely on “undocumented” behavior. Document what will not be broken in future, change the rest and ignore the wailing.

It’s antithetical to what Zig is all about to hide the implementation. The whole idea is you can read the entire program without having to jump through abstractions 10 layers deep.

reply
flohofwoe
12 hours ago
[-]
> Zig is hostile to encapsulation. You cannot make struct members private

In Zig (and plenty of other non-OOP languages) modules are the mechanism for encapsulation, not structs. E.g. don't make the public/private boundary inside a struct, that's a silly thing anyway if you think about it - why would one ever hand out data to a module user which is not public - just to tunnel it back into that same module later?

Instead keep your private data and code inside a module by not declaring it public, or alternatively: don't try to carry over bad ideas from C++/Java, sometimes it's better to unlearn things ;)

reply
cobbal
4 hours ago
[-]
Why would you hand out data that gets tunneled back in?

There are lots of use cases for this exact pattern. An acceleration structure to speed up searching complex geometry. The internal state of a streaming parser. A lazy cache of an expensive property that has a convenient accessor. An unsafe pointer that the struct provides consistent, threadsafe access patterns for. I've used this pattern for all these things, and there are many more uses for encapsulation. It's not just an OO concern.

reply
jandrewrogers
6 hours ago
[-]
I think the bigger issue with "public" and "private" is that is insufficiently granular, being essentially all or nothing. The use of those APIs in various parts of the code base is not self-documenting. Hyrum's Law is undefeated.

C++ has the PassKey idiom that allows you to whitelist what objects are allowed to access each part of the public API at compile-time. This is a significant improvement but a pain to manage for complex whitelists because the language wasn't designed with this in mind. C++26 has added language features specifically to make this idiom scale more naturally.

I'd love to see more explicit ACLs on APIs as a general programming language feature.

reply
flohofwoe
2 hours ago
[-]
> I'd love to see more explicit ACLs on APIs as a general programming language feature.

In that I agree, but per-member public/private/protected is a dead end.

I'd like a high level language which explores organizing all application data in a single, globally accessible nested struct and filesystem-like access rights into 'paths' of this global struct (read-only, read-write or opaque) for specific parts of the code.

Probably a bit too radical to ever become mainstream (because there's still this "global state == bad" meme - it doesn't have to be evil with proper access control - and it would radically simplify a lot of programs because you don't need to control access by passing 'secret pointers' around).

reply
the__alchemist
7 hours ago
[-]
Concur. Or, the in-between: Set the structs to be private if you need. I make heavy use of private structs and modules, but rarely private fields.
reply
sramsay64
10 hours ago
[-]
I think I mostly agree, but I do have one war story of using a C++ library (Apache Avro) that parsed data and exposed a "get next std::string" method. When parsing a file, all the data was set to the last string in the file. I could see each string being returned correctly in a debugger, but once the next call to that method was made, all previous local variables were now set to the new string. Never looked too far into it but it seemed pretty clear that there was a bug in that library that was messing with the internals of std::string, (which if I understand is just a pointer to data). It was likely re-using the same data buffer to store the data for different std::string objects which shouldn't be possible (under the std::string "API contract"). It was a pain to debug because of how "private" std::string's internals are.

In other words, we can at best form API contracts in C++ that work 99% of the time.

reply
jandrewrogers
6 hours ago
[-]
FWIW, the std::string buffer is directly accessible for (re-)writing via the public API. You don't need to use any private access to do this.
reply
mwkaufma
22 hours ago
[-]
> You need to be able to change the internal representation without breaking users.

Unless the user only links an opaque pointer, then just changing the sizeof() is breaking, even if the fields in question are hidden. A simple doc comment indicating that "fields starting with _ are not guaranteed to be minor-version-stable" or somesuch is a perfectly "reasonable" API.

reply
nevi-me
15 hours ago
[-]
I'd imagine semantic versioning to be more subjective with a language that relies on a social contract, because if a user chooses to use those private fields, a minor update or patch could break their code.

It does feel regressive to me. I've seen people easily reach for underscored fields in Python. We can discourage them if the code is reviewed, but then again there's also people who write everything without underscores.

reply
Dylan16807
18 hours ago
[-]
The chance of someone relying on the size at an API level is extremely small. That's far less risky than exposing every field.
reply
voidfunc
6 hours ago
[-]
How is this any different than Python or Ruby? You can access internals easily and people don't have a problem writing maintainable modular software in those languages.

Not to mention just about every language offers runtime reflection that let's you do bad stuff.

IMO, the Python adage of "We are all consenting adults here" applies.

reply
jenadine
14 hours ago
[-]
From my understanding, making stable API is impossible in Zig anyway, since Zig itself is still making breaking changes at the language level
reply
ants_everywhere
19 hours ago
[-]
You're getting a lot of responses with very strong opinions from people who talk as if they've never had to care about customers relying on their APIs.
reply
josephg
34 minutes ago
[-]
It’s a trust thing.

If you can trust that downstream users of your api won’t misuse private-by-convention fields (or won’t punish you for doing so), it’s not a problem. That works a lot of the time: You can trust yourself. You can usually your team. In the opensource world, you can just break compatibility with no repercussions.

But yes, sometimes that trust isn’t there. Sometimes you have customers who will misuse your code and blame you for it. But that isn’t the case for all code. Or even most code.

reply
9d
23 hours ago
[-]
Andrew has so many wrong takes. Unused variables is another.

Such a smart guy though, so I'm hesitant to say he's wrong. And maybe in the embedded space he's not, and if that's all Zig is for then fine. But internal code is a necessity of abstraction. I'm not saying it has to be C++ levels of abstraction. But there is a line between interface and implementation that ought to be kept. C headers are nearly perfect for this, letting you hide and rename and recast stuff differently than your .c file has, allowing you to change how stuff works internally.

Imagine if the Lua team wasn't free to make it significantly faster in recent 5.4 releases because they were tied to every internal field. We all benefited from their freedom to change how stuff works inside. Sorry Andrew but you're wrong here. Or at least you were 4 years ago. Hopefully you've changed your mind since.

reply
girvo
10 hours ago
[-]
> But internal code is a necessity of abstraction

I just fundamentally disagree with this. Not having "proper" private methods/members has not once become a problem for me, but overuse of them absolutely has.

reply
haberman
23 hours ago
[-]
I agree with almost all of this, including the point about c header files, except that code has to be in headers to be inlined (unless you use LTO), which in practice forces code into headers even if you’d prefer to keep it private.
reply
keldaris
21 hours ago
[-]
There's nothing wrong with using LTO, but I prefer simply compiling everything as a single translation unit ("unity builds"), which gets you all of the LTO benefits for free (in the sense that you still get fast compile times too).
reply
philwelch
22 hours ago
[-]
> I'm not saying it has to be C++ levels of abstraction. But there is a line between interface and implementation that ought to be kept. C headers are nearly perfect for this, letting you hide and rename and recast stuff differently than your .c file has, allowing you to change how stuff works internally.

Can’t you do this in Zig with modules? I thought that’s what the ‘pub’ keyword was for.

You can’t have private fields in a struct that’s publicly available but the same is sort of true in C too. OO style encapsulation isn’t the only way to skin a cat, or to send the cat a message to skin itself as the case may be.

reply
9d
22 hours ago
[-]
I don't know Zig so I dunno maybe
reply
LAC-Tech
18 hours ago
[-]
The solution to this is to simply put an underscore before the variables you don't think others should rely on, then move on with your life.
reply
dustbunny
23 hours ago
[-]
I don't care about public/private.
reply
pif
10 hours ago
[-]
You are right. Don't listen to the idiots!
reply
FlyingSnake
1 day ago
[-]
I recently, for fun, tried running zig on an ancient kindle device running stripped down Linux 4.1.15.

It was an interesting experience and I was pleasantly surprised by the maturity of Zig. Many things worked out of the box and I could even debug a strange bug using ancient GDB. Like you, I’m sold on Zig too.

I wrote about it here: https://news.ycombinator.com/item?id=44211041

reply
osigurdson
1 day ago
[-]
I've dabbled in Rust, liked it, heard it was bad so kind of paused. Now trying it again and still like it. I don't really get why people hate it so much. Ugly generics - same thing in C# and Typescript. Borrow checker - makes sense if you have done low level stuff before.
reply
int_19h
23 hours ago
[-]
If you don't happen to come across some task that implies a data model that Rust is actively hostile towards (e.g. trees with backlinks, or more generally any kind of graph with cycles in it), borrow checker is not much of a hassle. But the moment you hit something like that, it becomes a massive pain, and requires either "unsafe" (which is strictly more dangerous than even C, never mind Zig) or patterns like using indices instead of pointers which are counter to high performance and effectively only serve to work around the borrow checker to shut it up.
reply
creata
10 hours ago
[-]
> patterns like using indices instead of pointers which are counter to high performance

Using indices isn't bad for performance. At the very least, it can massively cut down on memory usage (which is in turn good for performance) if you can use 16-bit or 32-bit indices instead of full 64-bit pointers.

> "unsafe" (which is strictly more dangerous than even C, never mind Zig)

Unsafe Rust is much safer than C.

The only way I can imagine unsafe Rust being more dangerous than C is that you need to keep exception safety in mind in Rust, but not in C.

reply
whytevuhuni
5 hours ago
[-]
Not quite, you also need to keep pointer non-nullness, alignment and aliasing safety in Rust, which is very pervasive in Rust (all shared/mutable references) but very rare in C (the 'restricted' keyword).

In Rust, it's not just using an invalid reference that causes UB, but their very creation, even if temporary. For example, since references have to always be aligned, the compiler can assume the pointer they were created from was also aligned, and so suddenly some ending bits from the pointer are ignored (since they must've been zero).

And usually the point of unsafe is to make safe wrappers, so unafe Rust makes or interacts with safe shared/mutable references pretty often.

reply
creata
5 hours ago
[-]
It's just hard for me to imagine someone accidentally messing up nonnullness or aliasing, because it's really in-your-face that you need to be careful when constructing a reference unsafely. There are even idiomatic methods like ptr::as_ref to avoid accidentally creating null references.
reply
jplusequalt
7 hours ago
[-]
>which is strictly more dangerous than even C, never mind Zig

No it's not? The Rust burrow checker, the backbone of Rust's memory safety model, doesn't stop working when you drop into an unsafe block. From the Rust Book:

>To switch to unsafe Rust, use the unsafe keyword and then start a new block that holds the unsafe code. You can take five actions in unsafe Rust that you can’t in safe Rust, which we call unsafe superpowers. Those superpowers include the ability to:

    Dereference a raw pointer
    Call an unsafe function or method
    Access or modify a mutable static variable
    Implement an unsafe trait
    Access fields of a union
It’s important to understand that unsafe doesn’t turn off the borrow checker or disable any of Rust’s other safety checks: if you use a reference in unsafe code, it will still be checked. The unsafe keyword only gives you access to these five features that are then not checked by the compiler for memory safety. You’ll still get some degree of safety inside of an unsafe block.
reply
dwattttt
23 hours ago
[-]
> the moment you hit something like that, it becomes a massive pain, and requires either "unsafe" (which is strictly more dangerous than even C, never mind Zig) or patterns like using indices instead of pointers

If you need to make everything in-house this is the experience. For the majority though, the moment you require those things you reach for a crate that solves those problems.

reply
cornstalks
7 hours ago
[-]
(This is a reply to multiple sibling comments, not the parent)

For those saying unsafe Rust is strictly safer than C, you're overlooking Rust's extremely strict invariants that users must uphold. These are much stricter than C, and they're extremely easy to accidentally break in unsafe Rust. Breaking them in unsafe Rust is instant UB, even before leaving the unsafe context.

This article has a decent summary in this particular section: https://zackoverflow.dev/writing/unsafe-rust-vs-zig/#unsafe-...

reply
creata
7 hours ago
[-]
The author seems to mostly be talking about the aliasing rules, but if you don't want to deal with those, can't you use UnsafeCell?

Imo, the more annoying part is dealing with exception safety. You need to ensure that your data structures are all in a valid state if any of your code (especially code in an unsafe block) panics, and it's easy to forget to ensure that.

reply
jplusequalt
7 hours ago
[-]
>using indices instead of pointers which are counter to high performance

Laughs in graphics programmer. You end up using indices to track data in buffers all the time when working with graphics APIs.

reply
carlmr
23 hours ago
[-]
>requires either "unsafe" (which is strictly more dangerous than even C, never mind Zig)

Um, what? Unsafe Rust code still has a lot more safety checks applied than C.

>It’s important to understand that unsafe doesn’t turn off the borrow checker or disable any of Rust’s other safety checks: if you use a reference in unsafe code, it will still be checked. The unsafe keyword only gives you access to these five features that are then not checked by the compiler for memory safety. You’ll still get some degree of safety inside of an unsafe block.

https://doc.rust-lang.org/book/ch20-01-unsafe-rust.html

reply
Ar-Curunir
13 hours ago
[-]
I’m sorry, but your comment is a whole lot of horseshit.

Unsafe rust still has a bunch of checks C doesn’t have, and using indices into vectors is common code in high performance code (including Zig!)

reply
sapiogram
23 hours ago
[-]
Haters gonna hate. If you're working on a project that needs performance and correctness, nothing can get the job done like Rust.
reply
LAC-Tech
18 hours ago
[-]
unless you have to do anything that relies on a C API (such as provided by an OS) with no concept of ownership, then it's a massive task to get that working well with idiomatic rust. You need big glue layers to really make it work.

Rust is a general purpose language that can do systems programming. Zig is a systems programming language.

(Safety Coomers please don't downvote)

reply
saghm
17 hours ago
[-]
What does it even mean to be able to "do systems programming" but not actually be a "systems programming language"? I would directly disagree with you, but what you're arguing is so vague that I don't even know what you're trying to claim. The only way I can make sense of this is if you literally define a "systems programming language" as C and only other things that are tightly tied to it, which I guess is fine if you like tautologies but kind of makes even having a concept of " systems programming language" pretty useless.
reply
bbkane
17 hours ago
[-]
And yet Rust in the one in the Linux and Windows kernels, so people must think it's worth the effort. https://threadreaderapp.com/thread/1577667445719912450.html is certainly a glowing recommendation
reply
LAC-Tech
17 hours ago
[-]
Kernels are big pieces of software. Rust is used for device drivers mainly, right? So in that case you write an idiomatic rust lib and wrap it in a C interface and load it in.

Actually interfacing with idiomatic C APIs provided by an OS is something else entirely. You can see this is when you compare the Rust ecosystem to Zig; ie Zig has a fantastic io-uring library in the std lib, where as rust has a few scattered crates none of which come close the Zig's ease of use and integration.

One thing I'd like to see is an OS built with rust that could provide its own rusty interface to kernel stuff.

reply
ArtixFox
9 hours ago
[-]
Hello can you point me to more information about zig's and rust's io-uring implementations
reply
LAC-Tech
23 minutes ago
[-]
Hey Artix!

Zig's is in the standard library. From the commits it was started by Joran from Tigerbeetle, and now maintained by mlugg who is a very talented zig programmer.

https://ziglang.org/documentation/master/std/#std.os.linux.I...

The popular Rust one is tokio's io-uring crate which 1) relies on libc; the zig one just uses their own stdlib which wraps syscalls 2) Requires all sorts of glue between safe and unsafe rust.

github.com/tokio-rs/io-uring

reply
WD-42
6 hours ago
[-]
The OS is called Redox.
reply
LAC-Tech
23 minutes ago
[-]
It actually provides rust APIs to dev systems software against that run on it?

I know it's written in rust, but I am talking more specifically than that.

reply
dgb23
21 hours ago
[-]
Both are great languages. To me there's a philosophical difference, which can impact one to prefer one over the other:

Rust makes doing the wrong thing hard, Zig makes doing the right thing easy.

reply
wg0
1 day ago
[-]
Zig seems to be simpler Rust and better Go.

Off topic - One tool built on top of Zig that I really really admire is bun.

I cannot tell how much simpler my life is after using bun.

Similar things can be said for uv which is built in Rust.

reply
FlyingSnake
1 day ago
[-]
Zig is nothing like Go. Go uses GC and a runtime while Zig has none. While Zig’s functions aren’t coloured, it lacked the CSP style primitives like goroutines and channels.
reply
9d
21 hours ago
[-]
Zig is like a highly opinionated modern C

Rust is like a highly opinionated modern C++

Go is like a highly opinionated pre-modern C with GC

reply
cgh
19 hours ago
[-]
In a previous comment, you remarked you don’t even know Zig.
reply
LexiMax
17 hours ago
[-]
I do. I find his osmosis-based summation accurate.
reply
9d
8 hours ago
[-]
Yay!
reply
9d
19 hours ago
[-]
I don't.
reply
raincole
1 day ago
[-]
I wonder how zig works on consoles. Usually consoles hate anything that's not C/C++. But since zig can be transpiled to C, perhaps it's not completely ruled out?
reply
jeroenhd
1 day ago
[-]
Consoles will run anything you compile for them. There are stable compilers for most languages for just about any console I know of, because modern consoles are pretty much either amd64 or aarch64 like phones and computers are.

Language limitations are more on the SDK side of things. SDKs are available under NDAs and even publicly available APIs are often proprietary. "Real" test hardware (as in developer kits) is expensive and subject to NDAs too.

If you don't pick the language the native SDK comes with (which is often C(++)), you'll have to write the language wrappers yourself, because practically no free, open, upstream project can maintain those bindings for you. Alternatively, you can pay a company that specializes in the process, like the developers behind Godot will tell you to do: https://docs.godotengine.org/en/stable/tutorials/platform/co...

I think Zig's easy C interop will make integration for Zig into gamedev quite attractive, but as the compiler still has bugs and the language itself is ever changing, I don't think any big companies will start developing games in Zig until the language stabilizes. Maybe some indie devs will use it, but it's still a risk to take.

reply
9d
21 hours ago
[-]
> C/C++ has been the default

You're not really going to make something better than C. If you try, it will most likely become C++ anyway. But do try anyway. Rust and Zig are evidence that we still dream that we can do better than C and C++.

Anyway I'm gonna go learn C++.

reply
flohofwoe
1 hour ago
[-]
C++ has been piling more new problems on top of C than it inherited from C in the first place (and C++ is now caught in a cycle of trying to fix problems it introduced a couple of versions ago).

Creating a better C successor than C++ is really not a high bar.

reply
el_pollo_diablo
1 day ago
[-]
> In fact, even state-of-art compilers will break language specifications (Clang assumes that all loops without side effects will terminate).

I don't doubt that compilers occasionally break language specs, but in that case Clang is correct, at least for C11 and later. From C11:

> An iteration statement whose controlling expression is not a constant expression, that performs no input/output operations, does not access volatile objects, and performs no synchronization or atomic operations in its body, controlling expression, or (in the case of a for statement) its expression-3, may be assumed by the implementation to terminate.

reply
tialaramex
1 day ago
[-]
C++ says (until the future C++ 26 is published) all loops, but as you noted C itself does not do this, only those "whose controlling expression is not a constant expression".

Thus in C the trivial infinite loop for (;;); is supposed to actually compile to an infinite loop, as it should with Rust's less opaque loop {} -- however LLVM is built by people who don't always remember they're not writing a C++ compiler, so Rust ran into places where they're like "infinite loop please" and LLVM says "Aha, C++ says those never happen, optimising accordingly" but er... that's the wrong language.

reply
kibwen
1 day ago
[-]
> Rust ran into places where they're like "infinite loop please" and LLVM says "Aha, C++ says those never happen, optimising accordingly" but er... that's the wrong language

Worth mentioning that LLVM 12 added first-class support for infinite loops without guaranteed forward progress, allowing this to be fixed: https://github.com/rust-lang/rust/issues/28728

reply
loeg
1 day ago
[-]
For some context, 12 was released in April 2021. LLVM is now on 20 -- the versions have really accelerated in recent years.
reply
username223
20 hours ago
[-]
At least it's not just clownish version acceleration. They decided they wanted versions to increase faster somewhere around 2017-2018 (4.xx), and the version increase is more or less linear before and after that time, just at different slopes.
reply
3836293648
18 hours ago
[-]
And it's now a yearly major release, is it not?

Same as with GCC

reply
loeg
16 hours ago
[-]
Looks like two major versions per year for LLVM.
reply
username223
18 hours ago
[-]
I can't say I'm happy with "yearly broken backward compatibility," but at least it's predictable.
reply
el_pollo_diablo
1 day ago
[-]
Sure, that sort of language-specific idiosyncrasy must be dealt with in the compiler's front-end. In TFA's C example, consider that their loop

  while (i <= x) {
      // ...
  }
just needs a slight transformation to

  while (1) {
      if (i > x)
          break;
      // ...
  }
and C11's special permission does not apply any more since the controlling expression has become constant.

Analyzes and optimizations in compiler backends often normalize those two loops to a common representation (e.g. control-flow graph) at some point, so whatever treatment that sees them differently must happen early on.

reply
pjmlp
1 day ago
[-]
In theory, in practice it depends on the compiler.

It is no accident that there is ongoing discussion that clang should get its own IR, just like it happens with the other frontends, instead of spewing LLVM IR directly into the next phase.

reply
WalterBright
5 hours ago
[-]
> Rust's memory model allows the compiler to always assume that function arguments never alias. You must manually specify this in Zig.

I've avoided such manual specification of aliasing because:

1. few people understand it

2. using it erroneously can result in baffling bugs in your code

reply
uecker
1 day ago
[-]
You don't really need comptime to be able to inline and unroll a string comparison. This also works in C: https://godbolt.org/z/6edWbqnfT (edit: fixed typo)
reply
Retro_Dev
1 day ago
[-]
Yep, you are correct! The first example was a bit too simplistic. A better one would be https://github.com/RetroDev256/comptime_suffix_automaton

Do note that your linked godbolt code actually demonstrates one of the two sub-par examples though.

reply
uecker
1 day ago
[-]
I haven't looked at the more complex example, but the second issue is not too difficult to fix: https://godbolt.org/z/48T44PvzK

For complicated things, I haven't really understood the advantage compared to simply running a program at build time.

reply
Cloudef
1 day ago
[-]
To be honest your snippet isn't really C anymore by using a compiler builtin. I'm also annoyed by things like `foo(int N, const char x[N])` which compilation vary wildly between compilers (most ignore them, gcc will actually try to check if the invariants if they are compile time known)

> I haven't really understood the advantage compared to simply running a program at build time.

Since both comptime and runtime code can be mixed, this gives you a lot of safety and control. The comptime in zig emulates the target architecture, this makes things like cross-compilation simply work. For program that generates code, you have to run that generator on the system that's compiling and the generator program itself has to be aware the target it's generating code for.

reply
uecker
1 day ago
[-]
It also works with memcpy from the library: https://godbolt.org/z/Mc6M9dK4M I just didn't feel like burdening godbolt with an inlclude.

I do not understand your criticism of [N]. This gives compiler more information and catches errors. This is a good thing! Who could be annoyed by this: https://godbolt.org/z/EeadKhrE8 (of course, nowadays you could also define a descent span type in C)

The cross-compilation argument has some merit, but not enough to warrant the additional complexity IMHO. Compile-time computation will also have annoying limitations and makes programs more difficult to understand. I feel sorry for everybody who needs to maintain complex compile time code generation. Zig certainly does it better than C++ but still..

reply
Cloudef
1 day ago
[-]
> I do not understand your criticism of [N]. This gives compiler more information and catches errors. This is a good thing!

It only does sane thing in GCC, in other compilers it does nothing and since it's very underspec'd it's rarely used in any C projects. It's shame Dennis's fat pointers / slices proposal was not accepted.

> warrant the additional complexity IMHO

In zig case the comptime reduces complexity, because it is simply zig. It's used to implement generics, you can call zig code compile time, create and return types.

This old talk from andrew really hammers in how zig is evolution of C: https://www.youtube.com/watch?v=Gv2I7qTux7g

reply
uecker
1 day ago
[-]
Then the right thing would be to complain about those other compilers. I agree that Dennis' fat pointer proposal was good.

Also in Zig it does not reduce complexity but adds to it by creating an distinction between compile time and run-time. It is only lower complexity by comparing to other implementations of generic which are even worse.

reply
pron
1 day ago
[-]
C also creates a distinction between compile-time and run-time, which is more arcane and complicated than that of Zig's, and your code uses it, too: macros (and other pre-processor programming). And there are other distinctions that are more subtle, such as whether the source of a target function is available to the caller's compilation unit or not, static or not etc..

C only seems cleaner and simpler if you already know it well.

reply
uecker
1 day ago
[-]
My point is not about whether compile-time programming is simpler in C or in Zig, but that is in most cases the wrong solution. My example is also not about compile time programming (and does not use macro: https://godbolt.org/z/Mc6M9dK4M), but about letting the optimizer do its job. The end result is then leaner than attempting to write a complicated compile time solution - I would argue.
reply
pyrolistical
1 day ago
[-]
Right tool for the job. There was no comptime problem shown in the blog.

But if there were zig would prob be simpler since it uses one language that seamlessly weaves comptime and runtime together

reply
uecker
1 day ago
[-]
I don't know, to me it seems the blog tries to make the case that comptime is useful for low-level optimization: "Is this not amazing? We just used comptime to make a function which compares a string against "Hello!\n", and the assembly will run much faster than the naive comparison function. It's unfortunately still not perfect." But it turns out that a C compiler will give you the "perfect" code directly while the comptime Zig version is fairly complicated. You can argue that this was just a bad example and that there are other examples where comptime makes more sense. The thing is, about two decades ago I was similarly excited about expression-template libraries for very similar reasons. So I can fully understand how the idea of "seamlessly weaves comptime and runtime together" can appear cool. I just realized at some point that it isn't actually all that useful.
reply
pron
23 hours ago
[-]
> But it turns out that a C compiler will give you the "perfect" code directly while the comptime Zig version is fairly complicated.

In this case both would (or could) give the "perfect" code without any explicit comptime programming.

> I just realized at some point that it isn't actually all that useful.

Except, again, C code often uses macros, which is a more cumbersome mechanism than comptime (and possibly less powerful; see, e.g. how Zig implements printf).

I agree that comptime isn't necessarily very useful for micro optimisation, but that's not what it's for. Being able to shift computations in time is usedful for more "algorithmic" macro optimisations, e.g. parsing things at compile time or generating de/serialization code.

reply
uecker
23 hours ago
[-]
Of course, a compiler could possibly also optimize the Zig code perfectly. The point is that the blogger did not understand it and instead created an overly complex solution which is not actually needed. Most C code I write or review does not use a lot of macros, and where they are used it seems perfectly fine to me.
reply
pjmlp
1 day ago
[-]
Not only it was a good proposal, since 1990 that WG14 has not done anything else into that sense, and doesn't look like it ever will.
reply
uecker
1 day ago
[-]
Let's see. We have a relatively concrete plan to add dependent structure types to C2Y: struct foo { size_t n; char (buf)[.n]; };

Once we have this, the wide pointer could just be introduced as syntactic sugar for this. char (buf)[:] = ..

Personally, I would want the dependent structure type first as it is more powerful and low-level with no need to decide on a new ABI.

reply
int_19h
23 hours ago
[-]
This feels like such a massive overkill complexity-wise for something so basic.
reply
uecker
23 hours ago
[-]
Why do you think so? The wide pointers are syntactic sugar on top of it, so from an implementation point of view not really simpler.
reply
pjmlp
17 hours ago
[-]
Thanks, interesting to see how it will turn out.
reply
Cloudef
1 day ago
[-]
Sure there's tradeoffs for everything, but if I had to choose between macros, templates, or zig's comptime, I'd take the comptime any time.
reply
uecker
1 day ago
[-]
To each their own, I guess. I still find C to be so much cleaner than all the languages that attempt to replace it, I can not possibly see any of them as a future language for me. And it turns out that it is possible to fix issues in C if one is patient enough. Nowadays I would write this with a span type: https://godbolt.org/z/nvqf6eoK7 which is safe and gives good code.

update: clang is even a bit nicer https://godbolt.org/z/b99s1rMzh although both compile it to a constant if the other argument is known at compile time. In light of this, the Zig solution does not impress me much: https://godbolt.org/z/1dacacfzc

reply
quibono
1 day ago
[-]
Possibly a stupid question... what's a descent span type?
reply
uecker
1 day ago
[-]
Something like this: https://godbolt.org/z/er9n6ToGP It encapsulates a pointer to an array and a length. It is not perfect because of some language limitation (which I hope we can remove), but also not to bad. One limitation is that you need to pass it a typedef name instead of any type, i.e. you may need a typedef first. But this is not terrible.
reply
quibono
1 day ago
[-]
Thanks, this is great! I've been having a look at your noplate repo, I really like what you're doing there (though I need a minute trying to figure out the more arcane macros!)
reply
uecker
1 day ago
[-]
In this case, the generic span type is just #define span(T) struct CONCAT(span_, T) { ssize_t N; T* data; } And the array to span macro would just create such an object form an array by storing the length of the array and the address of the first element. #define array2span(T, x) ({ auto __y = &(x); (span(T)){ array_lengthof(__y), &(__y)[0] }; })
reply
csjh
1 day ago
[-]
> High level languages lack something that low level languages have in great adundance - intent.

Is this line really true? I feel like expressing intent isn't really a factor in the high level / low level spectrum. If anything, more ways of expressing intent in more detail should contribute towards them being higher level.

reply
wk_end
1 day ago
[-]
I agree with you and would go further: the fundamental difference between high-level and low-level languages is that in high-level languages you express intent whereas in low-level languages you are stuck resorting to expressing underlying mechanisms.
reply
jeroenhd
1 day ago
[-]
I think this isn't referring to intent as in "calculate the tax rate for this purchase" but rather "shift this byte three positions to the left". Less about what you're trying to accomplish, and more about what you're trying to make the machine do.

Something like purchase.calculate_tax().await.map_err(|e| TaxCalculationError { source: e })?; is full of intent, but you have no idea what kind of machine code you're going to end up with.

reply
csjh
22 hours ago
[-]
Maybe, but from the author's description, it seems like the interpretation of intent that they want is to generally give the most information possible to the compiler, so it can do its thing. I don't see why the right high level language couldn't give the compiler plenty of leeway to optimize.
reply
raincole
21 hours ago
[-]
In other words, high-level languages express high-level intents, while low-level languages express low-level intents.

In yet other words, tautology.

reply
saagarjha
1 day ago
[-]
> As an example, consider the following JavaScript code…The generated bytecode for this JavaScript (under V8) is pretty bloated.

I don't think this is a good comparison. You're telling the compiler for Zig and Rust to pick something very modern to target, while I don't think V8 does the same. Optimizing JITs do actually know how to vectorize if the circumstances permit it.

Also, fwiw, most modern languages will do the same optimization you do with strings. Here's C++ for example: https://godbolt.org/z/TM5qdbTqh

reply
vanderZwan
1 day ago
[-]
In general it's a bit of an apples to fruit salad comparison, albeit one that is appropriate to highlight the different use-cases of JS and Zig. The Zig example uses an array with a known type of fixed size, the JS code is "generic" at run time (x and y can be any object). Which, fair enough, is something you'd have to pay the cost for in JS. Ironically though in this particular example one actually would be able to do much better when it comes to communicating type information to the JIT: ensure that you always call this function with Float64Arrays of equal size, and the JIT will know this and produce a faster loop (not vectorized, but still a lot better).

Now, one rarely uses typed arrays in practice because they're pretty heavy to initialize so only worth it if one allocates a large typed array one once and reuses them a lot aster that, so again, fair enough! One other detail does annoy me a little bit: the article says the example JS code is pretty bloated, but I bet that a big part of that is that the JS JIT can't guarantee that 65536 equals the length of the two arrays so will likely insert a guard. But nobody would write a for loop that way anyway, they'd write it as i < x.length, for which the JIT does optimize at least one array check away. I admit that this is nitpicking though.

reply
Retro_Dev
1 day ago
[-]
You can change the `target` in those two linked godbolt examples for Rust and Zig to an older CPU. I'm sorry I didn't think about the limitations of the JS target for that example. As for your link, It's a good example of what clang can do for C++ - although I think that the generated assembly may be sub-par, even if you factor in zig compiling for a specific CPU here. I would be very interested to see a C++ port of https://github.com/RetroDev256/comptime_suffix_automaton though. It is a use of comptime that can't be cleanly guessed by a C++ compiler.
reply
saagarjha
1 day ago
[-]
I just skimmed your code but I think C++ can probably constexpr its way through. I understand that's a little unfair though because C++ is one of the only other languages with a serious focus on compile-time evaluation.
reply
kamma4434
1 hour ago
[-]
I know nothing of Zig, but I worked long enough in lisp to know that the best macros are the ones you don’t write. They are wonderful but they have just as many drawbacks, and don’t compose nicely.
reply
KingOfCoders
1 day ago
[-]
I do love the allocator model of Zig, I would wish I could use something like an request allocator in Go instead of GC.
reply
usrnm
1 day ago
[-]
Custom allocators and arenas are possible in go and even do exist, but they ara just very unergonomic and hard to use properly. The language itself lacks any way to express and enforce ownership rules, you just end up writing C with a slightly different syntax and hoping for the best. Even C++ is much safer than go without GC
reply
KingOfCoders
1 day ago
[-]
They are not integrated in all libraries, so for me they don't exist.
reply
flohofwoe
1 day ago
[-]
> I love Zig for it's verbosity.

I love Zig too, but this just sounds wrong :)

For instance, C is clearly too sloppy in many corners, but Zig might (currently) swing the pendulum a bit too far into the opposite direction and require too much 'annotation noise', especially when it comes to explicit integer casting in math expressions (I wrote about that a bit here: https://floooh.github.io/2024/08/24/zig-and-emulators.html).

When it comes to performance: IME when Zig code is faster than similar C code then it is usually because of Zig's more aggressive LLVM optimization settings (e.g. Zig compiles with -march=native and does whole-program-optimization by default, since all Zig code in a project is compiled as a single compilation unit). Pretty much all 'tricks' like using unreachable as optimization hints are also possible in C, although sometimes only via non-standard language extensions.

C compilers (especially Clang) are also very aggressive about constant folding, and can reduce large swaths of constant-foldable code even with deep callstacks, so that in the end there often isn't much of a difference to Zig's comptime when it comes to codegen (the good thing about comptime is of course that it will not silently fall back to runtime code - and non-comptime code is still of course subject to the same constant-folding optimizations as in C - e.g. if a "pure" non-comptime function is called with constant args, the compiler will still replace the function call with its result).

TL;DR: if your C code runs slower than your Zig code, check your C compiler settings. After all, the optimization heavylifting all happens down in LLVM :)

reply
messe
1 day ago
[-]
With regard to the casting example, you could always wrap the cast in a function:

    fn signExtendCast(comptime T: type, x: anytype) T {
        const ST = std.meta.Int(.signed, @bitSizeOf(T));
        const SX = std.meta.Int(.signed, @bitSizeOf(@TypeOf(x)));
        return @bitCast(@as(ST, @as(SX, @bitCast(x))));
    }

    export fn addi8(addr: u16, offset: u8) u16 {
        return addr +% signExtendCast(u16, offset);
    }
This compiles to the same assembly, is reusable, and makes the intent clear.
reply
flohofwoe
1 day ago
[-]
Yes, that's a good solution for this 'extreme' example. But in other cases I think the compiler should make better use of the available information to reduce 'redundant casting' when narrowing (like the fact that the result of `a & 15` is guaranteed to fit into an u4 etc...). But I know that the Zig team is aware of those issues, so I'm hopeful that this stuff will improve :)
reply
hansvm
1 day ago
[-]
This is something I used to agree with, but implicit narrowing is dangerous, enough so that I'd rather be more explicit most of the time nowadays.

The core problem is that you're changing the semantics of that integer as you change types, and if that happens automatically then the compiler can't protect you from typos, vibe-coded defects, or any of the other ways kids are generating almost-correct code nowadays. You can mitigate that with other coding patterns (like requiring type parameters in any potentially unsafe arithmetic helper functions and banning builtins which aren't wrapped that way), but under the swiss cheese model of error handling it still massively increases your risky surface area.

The issue is more obvious on the input side of that expression and with a different mask. E.g.:

  const a: u64 = 42314;
  const even_mask: u4 = 0b0101;
  a & even_mask;
Should `a` be lowered to a u4 for the computation, or `even_mask` promoted, or however we handle the internals have the result lowered sometimes to a u4? Arguably not. The mask is designed to extract even bit indices, but we're definitely going to only extract the low bits. The only safe instance of implicit conversion in this pattern is when you intend to only extract the low bits for some purpose.

What if `even_mask` is instead a comptime_int? You still have the same issue. That was a poor use of comptime ints since now that implicit conversion will always happen, and you lost your compiler errors when you misuse that constant.

Back to your proposal of something that should always be safe: implicitly lowering `a & 15` to a u4. The danger is in using it outside its intended context, and given that we're working with primitive integers you'll likely have a lot of functions floating around capable of handling the result incorrectly, so you really want to at least use the _right_ integer type to have a little type safety for the problem.

For a concrete example, code like that (able to be implicitly lowered because of information obvious to the compiler) is often used in fixed-point libraries. The fixed-point library though does those sorts of operations with the express purpose of having zeroed bits in a wide type to be able to execute operations without loss of precision (the choice of what to do for the final coalescing of those operations when precision is lost being a meaningful design choice, but it's irrelevant right this second). If you're about to do any nontrivial arithmetic on the result of that masking, you don't want to accidentally put it in a helper function with a u4 argument, but with implicit lowering that's something that has no guardrails. It requires the programmer to make zero mistakes.

That example might seem a little contrived, and this isn't something you'll run into every day, but every nontrivial project I've worked on has had _something_ like that, where implicit narrowing is extremely dangerous and also extremely easy to accidentally do.

What about the verbosity? IMO the point of verbosity is to draw your attention to code that you should be paying attention to. If you're in a module where implicit casting would be totally fine, then make a local helper function with a short name to do the thing you want. Having an unsafe thing be noisy by default feels about right though.

reply
throwawaymaths
1 day ago
[-]
you could give the wrapper function a funny name like @"sign-cast" to force the eye to be drawn to it.
reply
johnisgood
1 day ago
[-]
Yeah but what is up with all that "." and "@"? Yes, I know what they are used for, but it is noise for me (i.e. "annotation noise"). This is why I do not use Zig. Zig is more like a lighter C++, not a C replacement, IMO.

I agree with everything flohofwoe said, especially this: "C is clearly too sloppy in many corners, but Zig might (currently) swing the pendulum a bit too far into the opposite direction and require too much 'annotation noise', especially when it comes to explicit integer casting in math expressions ".

Seems like I will keep using Odin and give C3 a try (still have yet to!).

Edit: I quite dislike that the downvote is used for "I disagree, I love Zig". sighs. Look at any Zig projects, it is full of annotation noise. I would not want to work with a language like that. You might, that is cool. Good for you.

reply
pjmlp
1 day ago
[-]
Despite all bashes that I do at C, I would be happy if during the last 40 years we had gotten at least fat pointers, official string and array vocabulary types (instead of everyone getting their own like SDS and glib), namespaces instead of mylib_something, proper enums (like enum class in C++, enums in C# and so forth), fixing the pointer decay from array to &array[0], less UB.

While Zig fixes some of these issues, the amount of @ feels like being back in Objective-C land and yeah too many uses of dot and starts.

Then again, I am one of those that actually enjoys using C++, despite all its warts and the ways of WG21 nowadays.

I also dislike the approach with source code only libraries and how importing them feels like being back in JavaScript CommonJS land.

Odin and C3 look interesting, the issue is always what is going to be the killer project, that makes reaching for those alternatives unavoidable.

I might not be a language XYZ cheerleeder, but occasionally do have to just get my hands dirty and do the needfull for an happy customer, regardlees of my point of view on XYZ.

reply
elcritch
15 hours ago
[-]
You might try out Nim. It has a low annotation noise level. The python like syntax feels odd for a systems language at first but since it’s statically typed it works well. The simpler syntax seems to work very well with LLMs too.

Basically it’s a better C/C++ for me and sits between C and C++ in complexity.

I tried Zig for a while years ago but found the casts and other annotations frustrating. And at the time the language was pretty unstable in how it applied those rules. Plus I’ve never found a use for custom allocators, even on embedded.

reply
codethief
1 day ago
[-]
> Yeah but what is up with all that "." and "@"

"." = the "namespace" (in this case an enum) is implied, i.e. the compiler can derive it from the function signature / type.

"@" = a language built-in.

reply
johnisgood
1 day ago
[-]
I know what these are, but they are noise to me.
reply
Simran-B
1 day ago
[-]
It's not annotation noise however, it's syntax noise.
reply
johnisgood
1 day ago
[-]
Thanks for the correction. Is it really not "annotation"? What makes the difference?
reply
Simran-B
23 hours ago
[-]
You're not providing extra information to the compiler, clarifying the intent, but merely follow the requirements of the language when writing . to infer the type or @ to use a built-in function.
reply
johnisgood
13 hours ago
[-]
Thank you. My previous comment got down-voted despite it being a legitimate question, weird times.
reply
kprotty
1 day ago
[-]
C++'s `::` vs Zig's `.`

C++'s `__builtin_` (or arguably `_`/`__`) vs Zig's `@`

reply
johnisgood
1 day ago
[-]
I hate C++, too.
reply
pyrolistical
1 day ago
[-]
It is waaaaaaay less noisy than c++

C syntax may look simpler but reading zig is more comfy bc there is less to think about than c due to explicit allocator.

There is no hidden magic with zig. Only ugly parts. With c/c++ you can hide so much complexity in a dangerous way

reply
johnisgood
1 day ago
[-]
FWIW: I hate C++, too.
reply
throwawaymaths
1 day ago
[-]
the line noise is really ~only there for dangerous stuff, where slowing down a little bit (both reading and writing) is probably a good idea.

as for the dots, if you use zig quite a bit you'll see that dot usage is incredibly consistent, and not having the dots will feel wrong, not just in an "I'm used to it sense/stockholm syndrome" but you will feel for example that C is wrong for not having them.

for example, the use of dot to signify "anonymous" for a struct literal. why doesn't C have this? the compiler must make a "contentious" choice if something is a block or a literal. by contentious i mean the compiler knows what its doing but a quick edit might easily make you do something unexpected

reply
titzer
1 day ago
[-]
Zig has some interesting ideas, and I thought the article was going to be more on the low-level optimizations, but it turned out to be "comptime and whole program compilation are great". And I agree. Virgil has had the full language available at compile time, plus whole program compilation since 2006. But Virgil doesn't target LLVM, so speed comparisons end up being a comparison between two compiler backends.

Virgil leans heavily into the reachability and specialization optimizations that are made possible by the compilation model. For example it will aggressively devirtualize method calls, remove unreachable fields/objects, constant-promote through fields and heap objects, and completely monomorphize polymorphic code.

reply
skywal_l
1 day ago
[-]
Maybe with the new x86 backend we might see some performance differences between C and Zig that could definitely be attributed solely to the Zig project.
reply
saagarjha
1 day ago
[-]
I would be (pleasantly) surprised if Zig could beat LLVM's codegen.
reply
Zambyte
1 day ago
[-]
So would the Zig team. AFAIK, they don't plan to (and have said this in interviews). The plan is for super fast compilation and incremental compilation. I think the homegrown backend is mainly for debug builds.
reply
Cloudef
13 hours ago
[-]
The backends do already have some simple optimizations. Of course focus is debug builds and speed, but long term goal is for them to be competitive as well.
reply
Zambyte
1 day ago
[-]
Regarding the explicit integer casting, it seems like there is some cleanup that will be coming soon: https://ziggit.dev/t/short-math-notation-casting-clarity-of-...
reply
int_19h
23 hours ago
[-]
I rather suspect that the pendulum will swing rather strongly towards more verbose and explicit languages in general in the upcoming years solely because it makes things easier for AI.

(Note that this is orthogonal to whether and to what extent use of AI for coding is a good idea. Even if you believe that it's not, the fact is that many devs believe otherwise, and so languages will strive to accommodate them.)

reply
Retro_Dev
1 day ago
[-]
Ahh perhaps I need to clarify:

I don't love the noise of Zig, but I love the ability to clearly express my intent and the detail of my code in Zig. As for arithmetic, I agree that it is a bit too verbose at the moment. Hopefully some variant of https://github.com/ziglang/zig/issues/3806 will fix this.

I fully agree with your TL;DR there, but would emphasize that gaining the same optimizations is easier in Zig due to how builtins and unreachable are built into the language, rather than needing gcc and llvm intrinsics like __builtin_unreachable() - https://gcc.gnu.org/onlinedocs/gcc-4.5.0/gcc/Other-Builtins....

It's my dream that LLVM will improve to the point that we don't need further annotation to enable positive optimization transformations. At that point though, is there really a purpose to using a low level language?

reply
matu3ba
1 day ago
[-]
> LLVM will improve to the point that we don't need further annotation to enable positive optimization transformations

That is quite a long way to go, since the following formal specs/models are missing to make LLVM + user config possible:

- hardware semantics, specifically around timing behavior and (if used) weak memory

- memory synchronization semantics for weak memory systems with ideas from “Relaxed Memory Concurrency Re-executed” and suggested model looking promising

- SIMD with specifically floating point NaN propagation

- pointer semantics, specifically in object code (initialization), se- and deserialization, construction, optimizations on pointers with arithmetic, tagging

- constant time code semantics, for example how to ensure data stays in L1, L2 cache and operations have constant time

- ABI semantics, since specifications are not formal

LLVM is also still struggling with full restrict support due to architecture decisions and C++ (now worked on since more than 5 years).

> At that point though, is there really a purpose to using a low level language?

Languages simplify/encode formal semantics of the (software) system (and system interaction), so the question is if the standalone language with tooling is better than state of art and for what use cases. On the tooling part with incremental compilation I definitely would say yes, because it provides a lot of vertical integration to simplify development.

The other long-term/research question is if and what code synthesis and formal method interaction for verification, debugging etc would look like for (what class of) hardware+software systems in the future.

reply
eptcyka
1 day ago
[-]
For constant time code, it doesn’t matter too much if data spills out of a cache, constant time issues arise from compilers introducing early exits which leaves crypto open to timing attacks.
reply
matu3ba
1 day ago
[-]
Thanks for the info. Do you have a good overview on what other hardware properties or issues are relevant?
reply
flohofwoe
1 day ago
[-]
Yeah indeed. Having access to all those 'low-level tweaks' without having to deal with non-standard language extensions which are different in each C compiler (if supported at all) is definitely a good reason to use Zig.

One thing I was wondering, since most of Zig's builtins seem to map directly to LLVM features, if and how this will affect the future 'LLVM divorce'.

reply
Retro_Dev
1 day ago
[-]
Good question! The TL;DR as I understand it is that it won't matter too much. For example, the self-hosted x86_64 backend (which is coincidentally becoming default for debugging on linux right now - https://github.com/ziglang/zig/pull/24072) has full support for most (all?) builtins. I don't think that we need to worry about that.

It's an interesting question about how Zig will handle additional builtins and data representations. The current way I understand it is that there's an additional opt-in translation layer that converts unsupported/complicated IR to IR which the backend can handle. This is referred to as the compiler's "Legalize" stage. It should help to reduce this issue, and perhaps even make backends like https://github.com/xoreaxeaxeax/movfuscator possible :)

reply
knighthack
1 day ago
[-]
I'm not sure why allowances are made for Zig's verbosity, but not Go's.

What's good for the goose should be good for the gander.

reply
Zambyte
1 day ago
[-]
FWIW Zig has error handling that is nearly semantically identical to Go (errors as return values, the big semantic difference being tagged unions instead of multiple return values for errors), but wraps the `if err != nil { return err}` pattern in a single `try` keyword. That's the verbosity that I see people usually complaining about in Go, and Zig addresses it.
reply
kbolino
1 day ago
[-]
The way Zig addresses it also discards all of the runtime variability too. In Go, an error can say something like

    unmarshaling struct type Foo: in field Bar int: failed to parse value "abc" as integer
Whereas in Zig, an error can only say something that's known at compile time, like IntParse, and you will have to use another mechanism (e.g. logging) to actually trace the error.
reply
metaltyphoon
19 hours ago
[-]
Yep. Errors carry no context whatsoever and you have no idea where they came from.
reply
Zambyte
18 hours ago
[-]
reply
ummonk
1 day ago
[-]
Zig's verbosity goes hand in hand with a strong type system and a closeness to the hardware. You don't get any such benefits from Go's verbosity.
reply
nurbl
1 day ago
[-]
I think a better word may be "explicitness". Zig is sometimes verbose because you have to spell things out. Can't say much about Go, but it seems it has more going on under the hood.
reply
9d
23 hours ago
[-]
> People will still mistakenly say "C is faster than Python", when the language isn't what they are benchmarking.

Yeah but some language features are disproportionately more difficult to optimize. It can be done, but with the right language, the right concept is expressed very quickly and elegantly, both by the programmer and the compiler.

reply
timewizard
1 day ago
[-]
That for loop syntax is horrendous.

So I have two lists, side by side, and the position of items in one list matches positions of items in the other? That just makes my eyes hurt.

I think modern languages took a wrong turn by adding all this "magic" in the parser and all these little sigils dotted all around the code. This is not something I would want to look at for hours at a time.

reply
int_19h
23 hours ago
[-]
Such arrays are an extremely common pattern in low-level code regardless of language, and so is iterating them in parallel, so it's natural for Zig to provide a convenient syntax to do exactly that in a way that makes it clear what's going on (which IMO it does very well). Why does it make your eyes hurt?
reply
timewizard
21 hours ago
[-]
It looks to me like:

   for (one, two, three) |uno, dos, tres| { ... }
My eyes have to bounce back and forth between the two lists. When the identifiers are longer than this example it increases eye strain. Maybe it's better when you wrote it and understand it, but trying to grok someone else's code, it feels like an obstacle to me.
reply
justmarc
1 day ago
[-]
Optimization matters, in a huge way. Its effects are compounded by time.
reply
sgt
1 day ago
[-]
Only if the software ends up being used.
reply