I used Zig for (most of) Advent Of Code last year, and while I did get up-to-speed on it faster than I did with Rust the previous year, I think that was just Second (low-level) Language syndrome. Having experienced it, I'm glad that I did (learning how cumbersome memory management is makes me glad that every other language I've used abstracts it away!), but if I had to pick a single low-level language to focus on learning, I'd still pick Rust.
As a systems programmer, Rust has won. It will take decades before there is substantial Rust replacing the absurd amounts of C that runs on any modern Unix system, but I do believe that our of all the replacements for C/C++, Rust has finally gained the traction most of them have lacked at the large companies that put resources behind these types of rewrites and exploratory projects.
I do not think Zig will see wide adoption, but obviously if you enjoy writing it and can make a popular project, more power to you.
I agree. It's not ideal but Rust is a genuine improvement across the board on C and C++. It has the inertia and will slowly infiltrate and replace those 2. It also has the rare capacity to add some new areas without detracting from the mainstay: It's actually good as an embedded language for the web and as a DSL. C/C++ definitely didn't have that.
Safe C++ could still be a genuine improvement on Rust - if only because the community would be larger by at least one order of magnitude compared to present-day Rust. Though you would also need a viable C++ epochs proposal to keep the complexity from becoming totally unmanageable.
I'm not convinced, actually. The problem is that every proposal for safe C++ that I've seen sacrifices compatibility with the broader C++ ecosystem. Yes, you could graft borrow checking onto a C++-like semantics, but what you would end up creating is an incompatible dialect of C++--essentially a new language. So the resulting ecosystem would actually be smaller than that of Rust.
I think this issue is a bit overstated. What people want is not to have interop with existing C++ code, but to just keep writing C++ in the same idiomatic style. And this just isn't feasible if you want to automatically ensure memory safety. It's especially problematic in larger codebases that can't be comprehensively surveyed, which is what people mostly want to use C++ for. So, something has to give.
Rust is a bit of a different story, because the clunkiness of Pin<> actually makes interop with C++ (and, to a lesser extent, C) surprisingly difficult in a way that might be amenable to improvement.
Many, after doing a review of Rust, say they don't like or will stop using it. It's very premature to declare it has "won", whatever that can be said to mean. Example, ThePrimeTime[1] (famous YouTube programmer) is another stating he does not like Rust anymore, and rather use some other language.
It appears part of the controversy surrounding Rust, is that many are of the opinion that it's not worth it because of the limited use case, poor readability, complexity, long compile times, etc... and that appears to be what certain advocates of Rust are not understanding or appreciating the difference in opinions. Rust is fine for them, specifically, but not for everyone.
A big company I worked at actually deprecated the last of its Rust code last year. Maintaining Rust was much more expensive than predicted, and hiring and/or mentoring Rust Engineers proved even more expensive.
A simpler performant language like Zig, or a boring language + a different architecture would have been the better choice.
"Simple language" is often used as shorthand for "a language in which it is simple to express ideas", rather than "a language with few features". A language can, in fact, have many features and still be simple(-to-write-in); or, like Go, it can have so few features that it is complex-to-write-in.
Rust has very real limitations and trade-offs. It compiles slow and the binaries are large. The compiler also makes performance sacrifices that makes it generally slower than C. I'm sure the language will continue to be successful, but it hasn't "won".
From my experience on C++ vs Rust on test algorithm. For a naive algorithm implementation rust is usually slightly faster than C++. But when you try to optimise stuff, it's the opposite. It's really hard to optimise Rust code, you need to put lots of unsafe and unsafe is not user-friendly. Rust also force you on some design that are not always good for performance.
The last I heard, Rust had issues with freeing memory when it wouldn't need to, particularly with short-lived processes (like terminal programs) where the the Rust program would be freeing everything while the C version would just exit out and let the operating system do cleanup.
Rust has ManuallyDrop, which is exactly the functionality you’re describing. It works just fine for those types of programs. The speed of the two is going to be largely dependent on the amount of effort that has gone into optimizing either one, not on some theoretical performance bound. They’re both basically the same there. There are tons of examples of this in the wild at this point.
I maintain a Rust project that is ~50,000 loc [1]. I've never felt that compiling is slow, in the contrary it's always a pleasure to see how fast the project compiles (at least in debug).
In release, build time is longer but in this case, it's in the CI/CD so it doesn't bother me. We try to be very conservative with adding dependencies so it may help compilation time. Also I'm coming from a Java/Kotlin world so a lot of things appear like fresh air in comparison...
Zig might not become very popular, but IMO, it will become more popular than Rust.
Zig is good at all the areas Rust is good at. Zig is also good at game development which Rust is not good at.
And Zig is better when integrating with C/C++ libraries.
You cannot use yourself as an argument about the productivity of Rust, you designed this language!
Joke aside, the velocity of the Bevy Engine as a whole is indeed a testament to Rust productivity.
Last year I had two groups of students who built a multiplayer FPS and a Tower defense respectively after just one and a half days of Rust class so the learning curve is clearly not as bad as people like to tell on HN.
While I wish this were true, I very much doubt it, at least not until Zig has proper interfaces and gives up its weird hangup on anonymous functions. It's also extremely easy to effectively lose access to Zig features without cluttering your code. For example, say you want to use libevent in Zig: your event callbacks must use C calling conventions, meaning you lose access to try and errdefer, which are two of the most defining features of Zig. And while you can remedy this by having the callback invoke a Zig function, doing that just doubles every interaction between your Zig code and libevent, which is already cluttered because of the lack of anonymous functions.
These things aren't as important as compile times, but they are annoyances that will drive a non-zero amount of people away.
It seems you are proving Zig will not become very popular, but not Zig will not become more popular than Rust.
I agree that Zig will not become very popular. It needs certain programming experiences to master it. But I'm quite sure it will become more popular than Rust.
(never used zig yet myself)
For UB detection I've read zig had prime support for sanitizers, so you could run your tests with ubsan and catch UBs at this point... Assuming there are enough tests.
As far as I'm concerned (doing half C / half rust) I'm still watching from the sidelines but I'll definitely give zig a try at some point.
This article was insightful, thank you!
> The message has some weird mentions in (alloc565), but the actual useful information is there: a pointer is dangling.
The allocation ID is actually very useful for debugging. You can actually use the flags `-Zmiri-track-alloc-id=alloc565 -Zmiri-track-alloc-accesses` to track the allocation, deallocation, and any reads/writes to/from this location.
It's a non issue to name vars in a descriptive way referring to the features initial_foo for example and then foo_feature_a. Or name them based on what they don't have and then name it foo. In the example he provided for Rust, vars in different scopes isn't really an example of shadowing imho and is a different concept with different utility and safety. Replacing the value of one variable constantly throughout the code could lead to unpredictable bugs.
> Replacing the value of one variable constantly throughout the code could lead to unpredictable bugs.
Having variables with scopes that last longer than they're actually used and with names that are overly long and verbose leads to unpredictable bugs, too, when people misuse the variables in the wrong context later.
When I have `initial_foo`, `foo_feature_a`, and `foo_feature_b`, I have to read the entire code carefully to be sure that I'm using the right `foo` variant in subsequent code. If I later need to drop Feature B, I have to modify subsequent usages to point back to `foo_feature_a`. Worse, if I need to add another step to the process—a Feature C—I have to find every subsequent use and replace it with a new `foo_feature_c`. And every time I'm modifying the code later, I have to constantly sanity check that I'm not letting autocomplete give me the wrong foo!
Shadowing allows me to correctly communicate that there is only one `foo` worth thinking about, it just evolves over time. It simulates mutability while retaining all the most important benefits of immutability, and in many cases that's exactly what you're actually modeling—one object that changes from line to line.
> When I have `initial_foo`, `foo_feature_a`, and `foo_feature_b`, I have to read the entire code carefully to be sure that I'm using the right `foo` variant in subsequent code. If I later need to drop Feature B, I have to modify subsequent usages to point back to `foo_feature_a`. Worse, if I need to add another step to the process—a Feature C—I have to find every subsequent use and replace it with a new `foo_feature_c`. And every time I'm modifying the code later, I have to constantly sanity check that I'm not letting autocomplete give me the wrong foo!
When you have only one `foo` that is mutated throughout the code you are forced to organize the processes in your code (validation, business logic) based on the current state of that variable. If your variables have values which are logically assigned you're not bound by the current state of that variable. I think this a big pro. The only downside most people disagreeing with me are mentioning is related to ergonomics of it being more convenient.
> When you have only one `foo` that is mutated throughout the code you are forced to organize the processes in your code (validation, business logic) based on the current state of that variable. If your variables have values which are logically assigned you're not bound by the current state of that variable.
If I'm understanding you right, this is just restating what I said as a positive thing. I stand by my assertion that it's not positive: you can always choose to leave previous states accessible by choosing different names. But if a language doesn't support shadowing then I don't have the capability to intentionally restrict myself from accessing those states. That means your language has less expressive power and fewer opportunities for me to build myself guardrails.
In some ways it's the opposite of unused variable warnings: if you disallow shadowing, the compiler is forcing you to leave variables accessible long after you need them. You're given no choice but to leave unused variables hanging around. With shadowing, I can choose the right path based on the situation.
> The only downside most people disagreeing with me are mentioning is related to ergonomics of it being more convenient.
As I said elsewhere, literally everything to do with programming languages is about ergonomics. Your arguments against shadowing boil down to ergonomics. You can't avoid having a debate by just saying "it's just ergonomics" when the debate that you started is which feature is more ergonomic!
If you allow shadowing, then you rule out the possibility of the value being used later. This prevents accidental use (later on, in a location you didn't intend to use it) and helps readability by reducing the number of variables you must keep track of at once.
If you ban shadowing, then you rule out the possibility of the same name referring to different things in the same scope. This prevents accidental use (of the wrong value, because you were confused about which one the name referred to) and helps readability by making it easier to immediately tell what names refer to.
And on the whole, I prefer shadowing. I’ve never had a bug in either direction, but keeping everything immutable without shadowing means you spend all your brain power Naming Things.
I mean that's a really fake problem. How many times per line of code do you actually need to name variables and how many of those times you're shadowing a previously defined var. I'm guessing a very small amount.
It's not just the naming things, it's also what you do after you've named them—if you can't shadow a name then you are stuck both coming up with new names and sifting through all the existing names in your autocomplete to try to remember which one is the real one at this point in the code. Get it wrong? There's a bug.
That's not a fake problem, it's a problem I've actually run into on a regular basis on languages that don't have shadowing.
It absolutely depends on the language and how heavily it encourages immutability. For example, Rust and Elixor allow shadowing.
An awkward middle ground for me is Kotlin. It allows shadowing, but warns, so it might as well not be allowed. So you end up using lots of scoping tricks to avoid either making everything mutable, or having dozens of nearly-identical variables.
In case of rust, it actually happens quite often. I find myself rarely needing to use mut, instead using functional approaches such as iterators and expressions. So a high percentage of the code is let statements
I think it's worth pointing out that the example in the article contains a bug caused by not having shadowing: "const foo3 = try foo.addFeatureB();" should not be using the original foo, but foo2.
I don't know zig at all, but why is the author trying to declare foo as const 3 times. Surely you would declare it as var with some default value that means uninitialized, then try and put values in it.
It's probably a Zig antipattern, but it's a very common Rust pattern. Shadowing in Rust allows immutability to be ergonomic, and lack of shadowing discourages immutability.
Zig isn't Rust, so it makes sense that patterns in Rust don't translate well, but also I totally get TFA's preference for Rust in this case.
Oh, I just read the rust doc and its says "once a value is bound to a name, you can’t change that value." but I've thought of immutability the other way around, once a name has a value, it can't be changed.
I thought the value of const was once you read const x = 1024, you can be sure that x is 1024 while its in scope, that subsequent code can make assumptions about the content of variable x. Or, when you see x in the code, you can jump directly to its definition and know what its value will be. Defined once and not changed.
Apparently I don't understand the value of const at all.
There is a distinction between the variable itself and its name. Const (and Rust's immutability-by-default) ensures that the variable does not change after assignment. This holds true even as references to it are passed to other functions or stored for later use. You "can't" accidentally pass a reference to that variable which will then be unexpectedly mutated a dozen calls deep into a library function you didn't write.
If you have shadowing, it simply means you can have a different variable with the same name later in the same (or child) scope, this usually must be explicit. The same name now refers to a different variable, but the original variable still exists and remains valid.
It's quite a useful pattern, particularly where the old value is no longer useful (for example transforming input), especially when using the old value might be valid code but would be a mistake.
but it's also just as wrong. And even if you get it right, when the code changes later, somebody may add const foo_feature_Z = try foo_feature_V.addFeatureX();. Shadowing prevents this.
Shadowing is a feature. It's very common that given value transforms its shape and previous versions become irrelevant. Keeping old versions under different names would be just confusing. With type system there is no room for accidental misuse. I write Rust professionally for > 2 years, and years before that I was using it my own projects. I don't think shadowing ever backfired on me, while being very ergonomic.
Depending on which language you are using shadowing could lead to either small issues or catastrophic ones (in the scope of the program). If you have Python and you start with a number but end up with a complex dict this is very different than having one value in Rust and a slightly different value which is enforced by the compiler.
Don't see how it could introduce bugs. The point of replacing a variable is precisely to make a value that is no longer needed inaccessible. If anything introducing new variables with new names has the potential to introduce subtle bugs since someone could mistakenly use one of the variables that is no longer valid or no longer needed.
Over the years, I’ve wasted 1-2 days of my life debugging bugs caused by unintentional variable shadowing in Go (yes, I’ve kept track). Often, the bug is caused by an accidental use of := instead of =.
I don’t understand why code that relies on shadowing isn’t harder to follow.
Wish I could disable it entirely.
> Often, the bug is caused by an accidental use of := instead of =.
This is a distinctly Go problem, not a problem with shadowing as a concept. In Rust you'd have to accidentally add a whole `let` keyword, which is a lot harder to do or to miss when you're scanning through a block.
There are lots of good explanations in this subthread for why shadowing as a concept is great. It sounds like Go's syntax choices make it bad there.
> There are lots of good explanations in this subthread for why shadowing as a concept is great
Not really. All of them boil down to ergonomics, when in reality it doesn't bring a lot of benefit other than people hating on more descriptive variable names (which is fair).
You're saying ergonomics aren't a good explanation? The entire point of a programming language over writing machine code boils down to ergonomics! You've got to do better than "it's just ergonomics" when your argument for a language to ban shadowing is also "just ergonomics".
The debate here is about which one truly has better ergonomics!
My position on shadowing is that it's a thing where different projects can have different opinions, and that's fine. There are good arguments for allowing shadowing, and there are good arguments for disallowing it.
This is another big difference between Rust and Zig. Rust lets you have it both ways with configuration. Zig places much more value on being able to read and understand any Zig code in the wild, based only on “it compiles”. Rust’s “it compiles” gives you lots of information about safety (modulo unsafe blocks), but very little about certain other things until you’ve examined the 4-5 places which might be tweaking configuration (#[attributes], various toml files, environment variables, command line flags).
You asked when it became a feature. I answered that.
But your antipathy towards the feature is misplaced. Several languages with the most rigorous foundations support shadowing: SML, Ocaml, Haskell, Scheme,
You're probably more familiar with languages that have unrestricted mutation, in which case something much worse than shadowing is allowed: changing the value of an existing variable.
This is absolutely not what the article is about. A good majority of it is spent on the myth that Zig is safer than Rust, which has nothing to do with wishing Zig was more like Rust.
Is there a myth that makes that claim? Virtually every take I've heard is that Zig is "safe enough" while giving developers more control over memory and actually, it's specifically better for cases where you must write unsafe code, as it's not possible to express all programs in safe Rust.
If you must write unsafe code, what's wrong with just dropping down to unsafe in Rust when you need to? You have all the power unsafe provides, and you have a smaller surface area to audit than if your entire codebase resides in one big unsafe block.
Zig has a C and C++ compiler built into it and works seamlessly with it. Several C/C++ projects use Zig as a build tool. Zig makes different trade-offs with C++ from a language design standpoint. C++ has a lot more footguns to create UB in the first place.
WTH they were talking about Rust and Zig, they did not even mention C++ and you come with "if your metrics is blah then C++ is superior, checkmate!", completely ignoring C++ is a monster of complexity while Zig is basically simple as C.
If you present an argument that you like X in a thing so you picked B, and A exist that's more X than B, it means your argument is partially (you like X and Z) or totally (you like Y) wrong.
In this context of, if you are using Zig for its safety via tooling, there is a much more mature candidate C++.
That's exactly what I was criticizing: you just make the unfounded assumption that "argument that you like X" is the only thing that matters, and the debate is not only about X in the context of languages A and B. Introducing C to the debate is stupid, as then the obvious answer is just "but C has Y which is horrile" and so on.
This is precisely the myth that the article talks about. Miri finds significantly more UB in unsafe Rust than Zig's checks do.
Even if it weren't, this exaggeration is a complete theater. You aren't supposed to use unsafe Rust unless you really have to. I have been using Rust since 2020 and I've used it once, for 3 lines of code. The entirety of all Zig codebases is unsafe. That's fine if you are fine with unsafe code, but this myth is dishonest, and I take great issue with using a language where the founder is the primary source of the dishonesty - because what else is being swept under the rug?
> Miri finds significantly more UB in unsafe Rust than Zig's checks do.
That's not a substantiated claim. Miri also runs very slowly.
> You aren't supposed to use unsafe Rust unless you really have to. I have been using Rust since 2020 and I've used it once, for 3 lines of code.
Cool, glad you haven't needed it. If you're ever writing interpreters or interfacing with external code, you'll need it.
> The entirety of all Zig codebases is unsafe
Zig is not 100% memory safe but it has compile-time safety features for vast majority of problems developers get themselves into with C/C++. Meanwhile, Rust's safety overhead has real trade-offs in terms of developer productivity, computational performance, compiler performance and binary size.
The article we are commenting on substantiates it with, several, actual examples.
> interpreters
In what world do interpreters require unsafe code? A naive interpreter that recursively descends an AST doesn't need it, and a bytecode interpreter doesn't need it either. You'll probably need it if you want to make a fast GC, but that does not mean your entire codebase has to be unsafe.
> interfacing with external code
This is one reason unsafe exists, yes. You are supposed to hide the unsafe parts behind a safe interface. For example, Rust unavoidably has to deal with external code to do I/O - yet, the exposed std::fs interface is safe. This is a well established doctrine in the Rust community, and at least one prominent project has received hot hell for ignoring it.
And, again, the portions of code that are unsafe in a Rust codebase - even when required - are supposed to be minimal, well contained, and well tested. Running a suite of tests to check a small amount of code under Miri is not prohibitive at all. If someone is going to insist on using unsafe across their codebase then, yes, they are far better served by using a language that is unsafe to begin with.
I have done embedded Rust, and even there I have largely avoided unsafe code (the 3 lines I was forced to write happened to be for embedded).
> Rust's safety overhead has real trade-offs [...]
I never claimed otherwise. Those trade-offs have a purpose: fewer degrees of freedom result in higher degrees of certainty. Even Rust has too many degrees of freedom[1], but we don't sweep that under the rug, deflect it, or outright lie about the situation.
The Rust zeitgeist largely agrees your opinion (or rather: Andrew's opinion) of unsafe Rust, in a very oblique way. It's shit, we don't like using it. It is certainly not an accurate summary of Rust as a whole.
Leveraging unsafe Rust against Rust as a whole is a dishonest line of thinking and I'm not going to engage with it further.
For one, it doesn't do all the "memory safety parts", according to the readme. I'm very skeptical that Zig can be made memory safe with a checker while still remaining compatible with existing code. Certainly neither C nor C++ can, and Zig isn't meaningfully different in expressivity (if anything, it's more expressive, which is the opposite of what you want).
Q: You didn't do X, so Zig will never be able to track X
A: Maybe. Only way to know for sure is to fork this (or, hopefully, a 'real' successor) and fail. However, consider that "trivially" it should be possible to externally annotate every zig file with lifetime/type annotations identical to that of Rust and run "exactly the same" analysis as Rust and get the same memory safety as Rust.
it appears the clr author anticipated you: you didnt fork it, try, and fail, so you have ceded the authority to credibly make your speculative complaint
> Zig isn't meaningfully different in expressivity
it is meaningfully different in expressivity at the AIR level. AIR looks nothing like c, c++, zig, or rust.
> it appears the clr author anticipated you: you didnt fork it, try, and fail, so you have ceded the authority to credibly make your speculative complaint
That's a caveat. Not an expectation.
Plus that's not how proof works. Neither Zig nor Zig+Clr have really proven they are safe, ergo they are unsafe or possibly safe (respectively).
The original argument is that clr proves Zig is as safe as Rust, per your wording " all the memory safety parts."
Does the incompletely POC do all the parts, or doesn't it? That is no criticism against the project itself, striving to improve memory safety in any language is an honorable goal. The cinch is that an incomplete POC doesn't prove things one way or another, the POC needs to be completed (or at least completed far enough to prove your point). It either matches or exceeds Rust memory safety, or it only vastly improves Zig memory safety. Both are great outcomes, for what it's worth.
It shouldn't have been shown as an example of how Zig is just as/more safe than Rust if it is not. Mispresenting the project tears it down, not questioning its use an an example. Just like misrepresenting Zig's safety (and Rust's unsafety) tears Zig down.
I guess the question is what level of satisfied are you.
Are you satisfied like "proof is left as an exercise for the reader" in a math textbook? Or is it like "I have proved this but it's too large to fit in the margins"?
It seems author couldn't be bothered to complete it since there's (minor) missing features in the zig compiler that need to be satisfied first to make it worthwhile?
> Are you satisfied like "proof is left as an exercise for the reader" in a math textbook? Or is it like "I have proved this but it's too large to fit in the margins"?
In the first example the proof would be available elsewhere. In the second example, no. My answer is "just the facts as they exist today."
A mathematical proof holds no inherent utility. The thing it proves is what is useful. What I find useful is not suffering memory safety woes. Can I run a tool, any tool, today that will give me the same assurances that the Rust compiler (and/or Miri) does? Conjecture about the future is pointless because someone could equally go and write some static analysis tool like clr for Rust to fix all the bad parts, or fix the compiler, or the parts of clr that aren't finished might be impossible (we don't know, nobody has tried), or the heat death of the universe could come early. All still irrelevant, because I'm a user and I care exclusively about what I can currently do.
Nothing, it's the principle of the thing. I.e. when you make a challenging to evaluate statement, the burden of proof is on the one making the claim.
If I say "Moon is made of millennia old cheese", the burden of proof isn't on you to go create a rocket, fly to the moon, sample it and come with conclusions, but on me, making a difficult to verify statement.
the author has put a lot of unpaid I'm guessing work into this repo -- seems reasonable for the author to demand a bit of work out of any asshole taking a potshot at it.
It’s harder to write correct unsafe Rust than correct Zig because (1) Rust uses references all over the place, but when writing unsafe code you must scrupulously avoid “producing” an invalid reference (even if you never deference it), and (2) there’s lots of syntax noise which obscures what the code is doing (though &raw is a step in the right direction).
For Rust references, rules[0] are not hard to follow:
The pointer must be properly aligned.
It must be non-null.
It must be “dereferenceable”: a pointer is dereferenceable if the memory range of the given size starting at the pointer is entirely contained within the bounds of that allocated object. Note that in Rust, every (stack-allocated) variable is considered a separate allocated object.
The pointer must point to a valid value of type T.
When creating a mutable reference, then while this reference exists, the memory it points to must not get accessed (read or written) through any other pointer or reference not derived from this reference.
When creating a shared reference, then while this reference exists, the memory it points to must not get mutated (except inside UnsafeCell).
One reason it’s harder to follow is you can’t have references to uninitialized memory. In Zig pointers to uninitialized memory are fine as long as you don’t dereference them. That’s also true of Rust’s raw pointers, but most Rust code uses references, so it can’t be reused in unsafe contexts.
As a concrete example, I previously used Vec in unsafe code that dealt with uninitialized memory. This should be fine because Vec is basically a raw pointer and two integers. But later they changed the docs to say that this was undefined behavior (essentially, Vec reserves the right to “produce” a reference whenever it wants, even if you avoid calling methods that would do that). So my code that previously followed the rules now appeared to violate them.
> you can’t have references to uninitialized memory.
The method is available[0] in nightly Rust
pub const unsafe fn as_uninit_ref<'a>(self) -> Option<&'a MaybeUninit<T>>
where
T: Sized,
{
// SAFETY: the caller must guarantee that `self` meets all the
// requirements for a reference.
if self.is_null() { None } else { Some(unsafe { &*(self as *const MaybeUninit<T>) }) }
}
This is mostly a concern with the Rust stdlib, though. And it's in principle fixable, by writing new varieties of those stdlib functions that take raw-pointer or &UnsafeCell<...> arguments, and delegating the "safe" varieties to those.
There's compiler-level traits like `Iterator` and `Future` which enforce references. If wanting to do intrusive pointers into them, one risks creating overlapping references: https://github.com/tokio-rs/tokio/issues/3399
References to UnsafeCell<> should still be safe, because the only thing you can do with an &UnsafeCell<T> is extract a possibly-mutating raw pointer to T. (UnsafeCell<> is also special in that, like Cell<>, you can mutate through it without incurring UB.)
Various modern alternative languages can claim to be "safe enough" or safer than C, along with C interop. Nim, Dlang... In fact, the V programming language (Vlang) can make the argument of being even safer, because it has more default safety features and an optional GC (that no libraries depend on) for greater memory safety. That and it being designed to be easier to use and learn, for general-purpose programming.
The article, in its review of the Zig language, goes after its marketing history and broken promises (calling Andrew's statements fallacious and worse) for attempting to portray itself as safer than unsafe Rust, the distortions around UB (undefined behavior), etc... As a consequence of the demonstrably valid test results and being tired of unreliability, the author stopped using Zig. It should also be mentioned, that v1 Zig is nowhere in sight, so likely many years more to wait.
The debate between static and dynamic typing continues unceasingly. Even when the runtime values are statically typed, it's merely reprised at the type level.
I honestly don't see how anyone who has used a language with both unions and interfaces could come up with anything else that makes dynamic types better.
Either way you need to fulfill the contract, but I'd much prefer to find out I failed to do that at compile time.
Don't confuse "presence of dynamic types" with "absence of static types."
Think about the web, which is full of dynamicism: install this polyfill if needed, call this function if it exists, all sorts of progressive enhancement. Dynamic types are what make those possible.
Sure, I'm primarily a C# programmer which does have a dynamic type object, and occasionally use VB which uses late binding and can use dynamic typing as well.
You want to know how often I find dynamic typing the correct tool for the job? It's literally never.
Dynamic typing does allow you to do things faster as long as you can keep the whole type system in your head, which is why JavaScript was designed the way it was. That doesn't mean it is necessary to do any of those things, or is even the best way to do it.
> The first one that comes to mind is its arbitrary-sized integers. That sounds weird at first, but yes, you can have the regular u8, u16, u32 etc., but also u3. At first it might sound like dark magic, but it makes sense with a good example that is actually a defect in Rust to me.
You don't need Rust to support that because it can be implemented externally. For example, crates like "bitbybit" and "arbitrary-int" provide that functionality, and more:
I'm normally not sympathetic to the "you don't need that" argument, but there is a much stronger argument for not having arbitrarily-sized integers in Rust: the fact that values of such types can't have an address. The reason why our types all have bit sizes measured in octets is that a byte is the minimum granularity for a pointer.
They could just be aligend and padded to the next power of 2, right? I think they work like that in Zig and only when they are put in a (bit)packed struct are they actually (bit)unaligned and unpadded.
A byte isn't the minimum granularity for a pointer. The minimum is based on whatever target you're compiling for. If it's a 32-bit target platform, then the minimum granularity is 4 bytes. Why should pointer size determine value size though? It's super fast to shift bits around, too, when needed.
> Because you should be able to take the address of any value
That's debatable, though. One could argue that languages should explicitly support "values that are never going to have their address taken, be passed by reference/pointer, etc." which would only become addressable, e.g. as part of a struct.
> Huh? How do you think `const char s = "Hello"; const char t = &s[1];` works?
I think you and the parent are using different definitions of granularity. The parent meant that sizeof(t) could be 32 or 64 bits. I think you just meant that the smallest thing the pointer references is the address of a single byte.
Rust already has fat pointers though. A reference to a smaller byte value could be a pointer plus a bit-mask.
> Huh? How do you think `const char s = "Hello"; const char t = &s[1];` works?
So this is just an example of a pointer offset by 1 slot. It's not conceptually all that different to take sub-values of a slot.
To work with values that consume fractions of a byte, such as a 3-bit integer, a pointer + a bit offset is used to grab that value (that complexity is abstracted by the compiler). My argument is the bit shift necessary to load the 3-bits you
need from the 8-bit slot is one of the cheapest operations a CPU can do. Meanwhile using 3-bits when all you need is 3-bits allows for things like arrays and packed structs to use much less memory than padding everything to 8 bits.
If your pointers have to have a bit offset, then now a pointer isn't just 1 hardware word, which would be odd to say the least for a systems language. (It would also be slow, because making a load anything other than a load is something you really don't want to do; you would lose the ability to fold into addressing modes on x86 in many cases for example.)
If some pointers have a bit offset but others don't, OK, I guess, but I'd argue that at that point it'd be a cleaner design to just have a "pointer plus bit offset" be a separate type from "regular pointer". And that would get back to the problem that you would have a type that you couldn't take a "regular" pointer to.
Just to close the loop here, it looks like that is exactly what zig does for pointers, such that there is a different type of pointer for each combination of bit offsets and byte alignments, and yes it is distinct from a "regular" pointer (of which there isn't one "regular" type either, but also many separate flavors, including C-compatible, single-item, and many-item): https://ziglang.org/documentation/master/#toc-packed-struct
> There’s a catch, though. Unlike Rust, ErrorType is global to your whole program, and is nominally typed.
What does "global to your whole program" mean? I'd expect types to be available to the whole compilation unit. I'm also weirded out by the fact that zig has a distinct error type. Why? Why not represent errors as normal records?
Zig automatically does what most languages call LTO, so "whole program" and "compilation unit" are effectively the same thing (these error indices don't propagate across, e.g., dynamically linked libraries). If you have a bunch of ZIg code calling other Zig code and using error types, they'll all resolve to the same global error type (and calling different code would likely result in a different global error type).
> distinct error type, why?
The langage is very against various kinds of hidden "magic." If you take for granted that (1) error paths should have language support for being easily written correctly, and (2) userspace shouldn't be able to do too many shenanigans with control flow, then a design that makes errors special is a reasonable result.
It also adds some homogeneity to the code you read. I don't have to go read how _your_ `Result` type works just to use it correctly in an async context.
The obvious downside is that your use case might not map well to the language's blessed error type. In that case, you just make a normal record type to carry the information you want.
On the other hand zig errors can't have any associated value (https://github.com/ziglang/zig/issues/2647). I often find this requires me to store those values in some other big sum type somewhere which leads to all the same problems/boilerplate that the special error type should have saved me from.
If I have multiple errors then that in-out parameter has to be a union(enum). And then I'm back to creating dozens of slightly different unions for functions which return slightly different sets of errors. Which is the same problem I have in rust. All of the nice inference that zig does doesn't apply to my in-out parameter either. And the compiler won't check that every path that returns error.Foo always initializes error_info.Foo.
> What does "global to your whole program" mean? I'd expect types to be available to the whole compilation unit.
I think they mean you only have one global/shared ErrorType . You can't write the type of function that may yeet one particular, specific type of error but not any other types of error.
> You can easily capture the error and conditionally handle it
Sure. But the compiler won't help you check that your function only throws the errors that you think it does, or that your try block is handling all the errors that can be thrown inside it.
> ...the compiler won't help you check that your function only throws the errors that you think it does, or that your try block is handling all the errors that can be thrown inside it.
It will do both of those:
const std = @import("std");
fn throws(i: usize) !void {
return switch (i) {
0 => error.zero,
1 => error.one,
else => error.many,
};
}
fn catches(i: usize) !void {
throws(i) catch |err| {
return switch (err) {
error.one => error.uno,
else => |other| other,
};
};
}
pub fn main() void {
catches(std.os.argv.len) catch |err| {
switch (err) {
// Type error if you comment out any of these:
// note: unhandled error value: 'error.zero'
error.zero => std.debug.print("0\n", .{}),
error.uno => std.debug.print("1\n", .{}),
error.many => std.debug.print("2\n", .{}),
// Type error if you uncomment this:
// 'error.one' not a member of destination error set
//error.one => std.debug.print("1\n", .{}),
}
};
}
It wouldn't hurt to just read the docs before making confident claims.
I said I wasn't speaking for Zig specifically, just on general principle that errors are not really values. Many languages reify errors as values to avoid having different semantics for errors, but errors probably should have their own semantics. Zig seems to take a middle ground here, where errors are a special type of value but that still sort of has its own semantics.
I was generally responding to the whole thread and pointing to how Zig sees errors. Enums are a type of value, yes, but they're typically dealt with differently than other data types.
lol, I knew exactly who wrote this once I saw the complaint about shadowing being forbidden. The author and I were just arguing about it the other day on irc. While the author considers it an annoying language bug because it requires creating additional variable names (given refactoring was an unpalatable option). I consider it a feature.
Said arguments have become a recurring and frustrating refrain; when rust imposes some limit or restriction on how code is written, it's a good thing. But if Zig does, it's a problem?
The remainder of the points are quite hollow, far be it from me to complain when someone starts with a conclusion and works their way backwards into an argument... but here I'd have hoped for more content. The duck typing argument is based on minimal, or missing documentation, or the doc generator losing parts of the docs. And "comptime is probably not as interesting as it looks" the fact he calls it probably uninteresting highlights the lack of critical examination put here. comptime is an amazing feature, and enables a lot of impressive idioms that I enjoy writing.
> I’m also fed up of the skill issue culture. If Zig requires programmers to be flawless, well, I’m probably not a good fit for the role.
But hey, my joke was featured as the closing thought! Zig doesn't require one to be flawless. But it' also doesn't try to limit you, or box you into a narrow set of allowed operations. There is the risk that you write code that will crash. But having seen more code with unwrap() or expect() than without, I don't think that's the bar. The difference being I personally enjoy writing Zig code because zig tries to help you write code instead of preventing you from writing code. With that does come the need to learn and understand how the code works. Everything is a learnable skill; and I disagree with the author it's too hard to learn. I don't even think it's too hard for him, he's just appears unwilling.... and well he already made up his mind about which language is his favorite.
The duck typing argument is absolutely not based on minimal or missing documentation. There wouldn't be countless issues about it in the Zig repository if it were that simple. See https://github.com/ziglang/zig/issues/17198
I'm simply going to quote one of the comments from the linked GitHub issue:
> generic code is hard. Hard to implement correctly, hard to test, hard to use, hard to reason about. But, for better or worse, Zig has generics. That is something that cannot be ignored. The presence of generic capabilities means that generic code will be written; most of the std relies on generic code.
Saying Zig has generics because it has comptime is like saying c has generics, because C has a pre-processor
It's a wild take that you have to willfully ignore the context and nuance (implemention, rules, and semantics) for it to be true. More true than misleading, at any rate.
> Zig does enhance on C, there is no doubt. I would rather write Zig than C. The design is better, more modern, and the language is safer. But why stop half way? Why fix some problems and ignore the most damaging ones?
I was disappointed when Rust went 1.0. It appeared to be on a good track to dethroning C++ in the domain I work in (video games)... but they locked it a while before figuring out the ergonomics to make it workable for larger teams.
Any language that imbues the entire set of special characters (!#*&<>[]{}(); ...etc) with mystical semantic context is, imo, more interested in making its arcane practitioners feel smart rather than getting good work done.
> I don’t think that simplicity is a good vector of reliable software.
No, but simplicity is often a property of readable, team-scalable, popular, and productive programming languages. C, Python, Go, JavaScript...
Solving for reliability is ultimately up to your top engineers. Rust certainly keeps the barbarians from making a mess in your ivory tower. Because you're paralyzing anyone less technical by choosing it.
> I think my adventure with Zig stops here.
This article is a great critique. I share some concerns about the BDFL's attitudes about input. I remain optimistic that Zig is a long way from 1.0 and am hoping that when Andrew accomplishes his shorter-term goals, maybe he'll have more brain space for addressing some feedback constructively.
> It appeared to be on a good track to dethroning C++ in the domain I work in (video games)... but they locked it a while before figuring out the ergonomics to make it workable for larger teams.
There are million-line Rust projects now. Rust is obviously workable for larger teams.
> Any language that imbues the entire set of special characters (!#*&<>[]{}(); ...etc) with mystical semantic context is, imo, more interested in making its arcane practitioners feel smart rather than getting good work done.
C uses every one of those symbols.
I think you're talking about @ and ~ boxes. As I recall, those were removed the same year the iPad and Instagram debuted.
> I think you're talking about @ and ~ boxes. As I recall, those were removed the same year the iPad and Instagram debuted.
Take criticism better.
A language choice on a project means the veterans are indefinitely charged with teaching it to newbies. For all Rust's perks, I judge that it would be a time suck for this reason.
Browsing some random rust game code:
[https://github.com/bevyengine/bevy/blob/8c7f1b34d3fa52c007b2...]
pub fn play<'p>(
&mut self,
player: &'p mut AnimationPlayer,
new_animation: AnimationNodeIndex,
transition_duration: Duration,
) -> &'p mut ActiveAnimation {
[https://github.com/bevyengine/bevy/blob/8c7f1b34d3fa52c007b2...]
#[derive(Debug, Clone, Resource)]
#[cfg_attr(feature = "bevy_reflect", derive(Reflect), reflect(Default, Resource))]
pub struct ButtonInput<T: Copy + Eq + Hash + Send + Sync + 'static> {
/// A collection of every button that is currently being pressed.
pressed: HashSet<T>,
...
I think this criticism is silly. Here's what your first example would look like in a language with keywords (where reasonable, perhaps like C#) instead:
But, this is still confusing! Let's remove even more symbols, and make the syntax more obvious by removing abbreviations:
PUBLIC FUNCTION Play
LIFETIMES
P
PARAMETERS
IN OUT Self
Player AS MUTABLE REFERENCE TO AnimationPlayer WITH LIFETIME P
NewAnimation AS AnimationNodeIndex
TransititionDuration AS Duration
RETURNS MUTABLE REFERENCE TO ActiveAnimation
BEGIN
...
END
IMO, using keywords instead of symbols for references, lifetimes, etc, would just make Rust overly verbose, and there's a reason BCPL used braces instead of BEGIN/END :^)
> Any language that imbues the entire set of special characters (!#*&<>[]{}(); ...etc) with mystical semantic context is, imo, more interested in making its arcane practitioners feel smart rather than getting good work done.
On that scale COBOL is a better programming language.
No idea how much the author is experienced at Zig, but my thoughts:
> No typeclasses / traits
This is purposeful. Zig is not trying to be some OOP/Haskell replacement. C doesn't have traits/typeclasses either. Zig prefers explicitness over implicit hacks, and typeclasses/traits are, internally, virtual classes with a vtable pointer. Zig just exposes this to you.
> No encapsulation
This appears to be more a documentation issue than anything else. Zig does have significant issues in that area, but this is to be expected in a language that hasn't even hit 1.0.
> No destructors
Uh... What? Zig does have destructors, in a way. It's called defer and errordefer. Again, it just makes you do it explicitly and doesn't hide it from you.
> No (unicode) strings
People seem to want features like this a lot -- some kind of string type. The problem is that there is no actual "string" type in a computer. It's just bytes. Furthermore, if you have a "Unicode string" type or just a "string" type, how do you define a character? Is it a single codepoint? Is it the number of codepoints that make up a character as per the Unicode standard (and if so, how would you even figure that out)? For example, take a multi-codepoint emoji. In pretty much every "Unicode string" library/language type I've seen, each individual codepoint is a "character". Which means that if you come across a multi-codepoint emoji, those "characters" will just be the individual codepoints that comprise the emoji, not the emoji as a whole. Zig avoids this problem by just... Not having a string type, because we don't live in the age of ASCII anymore, we live in a Unicode world. And Unicode is unsurprisingly extremely complicated. The author tries to argue that just iterating over byes leads to data corruption and such, but I would argue that having a Unicode string type, separate from all other types, designed to iterate over some nebulous "character" type, would just introduce all kinds of other problems that, I think, many would agree should NOT be the responsibility of the language. I've heard this criticism from many others who are new to zig, and although I understand the reasoning behind it, the reasoning behind just avoiding the problem entirely is also very sensible in my mind. Primarily because if Zig did have a full Unicode string and some "character" type, now it'd be on the standard library devs to not only define what a "character" is, and then we risk having something like the C++ Unicode situation where you have a char32_t type, but the standard library isn't equipped to handle that type, and then you run into "Oh this encoding is broken" and on and on and on it goes.
> typeclasses/traits are, internally, virtual classes with a vtable pointer
No, they're not. Rust "boxed traits" are, but those aren't what the author means.
> Primarily because if Zig did have a full Unicode string and some "character" type, now it'd be on the standard library devs to not only define what a "character" is, and then we risk having something like the C++ Unicode situation where you have a char32_t type, but the standard library isn't equipped to handle that type, and then you run into "Oh this encoding is broken" and on and on and on it goes.
The standard library not being equipped to handle Unicode is the entire problem. Not solving it doesn't avoid the issue: it just makes Unicode safety the programmer's responsibility, increasing the complexity of the problem domain for the programmer and leaving more room for error.
Not being able to easily write a program without Unicode being pulled in for Rust code was a reason I'd chosen C over Rust before. When targeting binary sizes measured in kilobytes, pulling in full unicode handling is not an option. Especially since programs that don't have direct human interaction rarely actually need unicode.
There is no nebulous 'character' type. There are bytes, codepoints and glyphs. All languages with Unicode support allow iterating over each for a given string.
> Zig does have destructors, in a way. It's called defer and errordefer.
defer ties some code to a static scope. Destructors are tied to object lifetime, which can be dynamic. For example, if you want to remove some elements from an ArrayList of, say, strings, the string's would need to be freed first. defer does not help you, but destructors would.
That's actually a great argument in favor of Zig over Rust. I assume Rust automatically writes code equivalent to this for you:
```
defer {
for (list.items) |str|
gpa.free(str);
list.deinit(gpa);
}
```
When it's spelled out like this, it becomes obvious to the reader that maybe this is the wrong allocation strategy. Maybe the whole thing should go in an Arena. Or, similarly, maybe there should be an ArrayList that holds all the character data that your string ArrayList indexes into with a u32 (or points to with pointers, if you want to update all the pointers on resize). Regardless, I'd be skeptical of code where each string has a separate lifetime even though all the lifetimes could be tied.
Rust makes classic (bad) allocation strategies automatic. Zig makes good allocation strategies more attractive than classic (bad) allocation strategies.
More succinctly: Rust makes bad code safe, Zig makes good code easy.
For me not having strings in Zig and being forced to use the fairly verbose '[]const u8' syntax every time I need a string was a little annoying at first, but it has had the effect of making me comfortable with the idea of buffers in a general sense, which is critical in systems programming. Most of the things that irked me about Zig when first learning it (I'm only a few weeks into it) have grown on me.
Having just gone down this road in C#, the way Unicode is now handled is via "runes".
Each rune may be comprised of various Unicode characters, which may themselves be 1-4 bytes (in the case of utf-8 encoding).
The one problem I have with this approach is that all of the categorization features operate a level below the runes, so you still have to break them up. The biggest drawback is that, at least in my (admittedly limited) research, there is no such thing as a "base" character in certain runes (such as family emojis- parents with kids). You can mostly dance around it with the vast majority of runes, because one character will clearly be the base character and one (or more) will clearly be overalys, but it's not universal.
Go does this too. I generally like the idea a lot, as long as it's consistent. The one thing I don't like is the inconsistency.
Not sure about C#, but in Go for example ranging strings ranges over runes, but indexing pulls a single byte. And len is the byte length rather than rune length.
So basically it's a byte array everywhere except ranging. I guess I would have preferred an explicit cast or conversion to do that instead of by default.
I don't necessarily disagree with not having a string type in a low level language, but you seem very fixated on needing a character type. Why not just have string be an opaque type, and have functions to iterate over code points, grapheme clusters, etc.?
I used Zig for (most of) Advent Of Code last year, and while I did get up-to-speed on it faster than I did with Rust the previous year, I think that was just Second (low-level) Language syndrome. Having experienced it, I'm glad that I did (learning how cumbersome memory management is makes me glad that every other language I've used abstracts it away!), but if I had to pick a single low-level language to focus on learning, I'd still pick Rust.