“ The language is different from Rust (who could’ve thought that, yeah), but it left a genuinely good impression. It’s straightforward, modern, and blazingly fast. I believe it has real potential to become the true successor to C. On the other hand, it’s still young, and it shows: the shape of the language itself feels unfinished in places, and I suspect it’ll pick up more of the cooler quality-of-life features and syntax sugar as it matures.
As for me, I’d like to keep contributing to the ecosystem, and I will, whenever I come across a project worth building.”
Idk I don’t write either well enough to have a hand in this but losing out all of this for more
Imperative stuff seems like a step back.
“ No functional paradigm
Rust is technically an imperative language, but it draws heavily on functional concepts: zero-cost iterators, lazy evaluation, ADTs, pattern matching, monadic types, traits, closures, and so on. Having also spent time with Haskell and Erlang, I’ve become fairly inclined toward the functional style, and it shows in this library. It leans heavily on FP idioms:
Monadic error control via combinators like Queryable and related types
Monadic-style data types like Data<T> with map, flat_map, reduce, and friends
Pure, immutable transformations
Combinators over iterators instead of loops
Closures for local abstraction
Declarative macros as a small embedded DSL
Sum types and product types”
Modern and blazing fast, what we lost leaving behind languages like Modula-2 and Object Pascal, having newer generations to think C and C++ were the only compiled languages alternatives to scripting languages.
I miss the years of writing CLI tools, web servers and clients, in Ada (and of real-time complex distributed system...). Felt so simple and right and fast and robust. The code is still readable today and maintaining it is a zero effort today. Clean Java without the enterprise BS was a close second in ease of programming - boilerplate be damned.
I'm glad NVIDIA found a way to make GPUs programmable and got us out of the shaders tarpit, but did it have to be C++...
You repeating this weird strawman take a million times doesn't make it true. Why don't you finally just put out some genuinely interesting projects demonstrating how everybody was doing it wrong, so people can make up their own mind and finally be convinced. There must be some true magic in those languages and platforms you mention, that should offset the pain of writing in upper case and with super long KEYWORDs everywhere, and to offset the cost of switching to a culture that has way less mindshare and way less of a software ecosystem around it.
FWIW I've actually worked for 6 months on a large old Delphi project. It was some performance work that, as almost always, mainly required getting the language crap out of the way. In the end I got the job done (100x-1000x speedup) but I wouldn't want to switch back to this ecosystem: Licensing costs, weird language warts there too. A slow moving ecosystem. Ultimately, I just need something that does what I tell it to do, reliably and fast, and that doesn't get in the way.
I’m more than a bit out of my depth discussing the topic, but I’m not sure than imperative-dominant languages will ever really go away or that functional-dominant languages will ever become as popular as C and C++. Ugly as they may be, imperative languages seem to be grokked by humans more readily and are more often than not “good enough” for the most part so it’s difficult to see them losing substantial momentum.
Until there is a machine that is natively functional, there is always going to an incentive to go lower level for more performance.
Even hardware (GPUs) that functional language could trivially exploit, it’s still higher performance to write low level code and manages all the memory imperatively
I think you’re right about people, but by volume, 99% of all future code will be written by machines. So there’s ample opportunity for other languages to flourish; if humans can’t get past imperative programming, machines can.
In my mind this hinges on whether LLMs become capable of actual comprehension of the code they’re writing. If not, for serious projects (especially those which are mission-critical), it still makes sense to optimize for human understanding when selecting languages so the code can reasonably be verified.
I don’t see the point of letting an LLM generate an article when the topic is your personal, subjective experience which only you, a human, would be able to express.
> The first thing that caught me off guard — and honestly, who would’ve expected this to be the memorable part — was IDE support, or the near-total lack of it.
I think when we're looking back on the 2020s we'll be struck by the Allocator obsession
All of the Handmade "C successor" languages seem to have this obsession, including not only Zig but Odin, C3 and Jai.
For some toy problems you can do clever allocator tricks and get a huge perf win. For example Jai and Odin both seem to really want you to write code which can throw away a "per-frame" arena periodically so they're not paying to track allocations in the arena because they're all thrown away at the same time.
But a lot of real world software just isn't that simple. This doesn't make such features worthless, it just means they're one of a thousand tools the experienced developer could want in their toolkit, not really deserving headline status.
AAA games use allocators extensively, and they are more complex pieces of software with higher performance requirements than nearly anything else out there. So I'm not sure what toy problems you're talking about.
Allocators have been in wide use long before the 2020s but I agree there does seem to be a resurgent interest lately. Although I would argue it's part of a more broad trend of focusing data driven design. Which makes sense because accessing main memory is one of the slowest things your program can do.
They do, but not necessarily together with generic, standard containers.
When you find yourself wanting a nonstandard allocator, you usually want it because you want it to have some interesting property. It's not necessarily trivial to fit that into the interface of something like `std::vector`, or `std::unordered_map`, etc.
Here's my take as a game developer: 99% of use cases for custom allocators are scratch allocators for doing stuff within a frame. 95% of those are much easier to serve by just amortizing allocations by storing things in an `std::vector` (or equivalent) that gets cleared every frame. You can use linear storage to back many interesting data structures, including queues, ring buffers, priority queues, binary heaps, etc., and that's more than enough for a large number of systems in a game.
The overwhelming majority of the time, more complex data structures (like hash maps etc.) have a longer lifetime than the current frame, because the whole point of using them in the first place is to amortize lookup time across frames.
actually large programs that are persistent is where you really care. kernels and databases often use explicit allocators because there is so much policy wrapped up in allocation.
this goes back decades, its not just a feature of the 2020s. as a systems programmer I always want this, and the idea that allocator state should be completely hidden and implicit is shortsighted.
but yes, it is a bit onerous to pass around allocator(s). the real complaint that I have is that if 'malloc' is global and a compiler primitive, then we can do things like coalesce allocations and have compiler managed lifetimes when appropriate.
explicit allocators are an important lever, its not clear to me that we could never find a way to make them possible without the minor downsides.
yes i've noticed this as well. they all stem from a heterodoxical corner of the programming community who are obsessed about performance. this isn't a bad thing, considering how slow modern software is! but this group believes performance is a memory issue, which is true for some software but not all. i think these languages can be really useful for realtime applications like gaming where allocation has a real cost, but the so-called "pointer jungles" are probably not the reason why microsoft teams takes a trillion cpu instructions to boot.
I'd word it slightly differently, as something like the Allocator Effect System obsession. It's not so much that allocator tricks don't have a role in modern software--all of the large applications I've worked on rely on things like arena allocation at least some of the time--but rather that things like making container types generic over allocators seems to be more trouble than it's worth. I've never seen anyone use anything other than the default allocator for STL types, for example.
> But a lot of real world software just isn't that simple.
Zig doesn't force or even tends to prefer one way or another. If I want unassuming heap allocations that can be reclaimed in any order, there's an allocator for that. If I want an arena to discard at the end of something like a request or a video frame, there's an allocator for that. If I want to use a fixed backing buffer for the allocations, there's an allocator for that.
The point is that the standard library doesn't assume one or the other, which seems good if the problem is that "real world software just isn't that simple" in the more general sense that there's no one-size-fits-all allocation strategy.
But for how many software domains are allocators more specialized than a single built-in general purpose one so important it should be the focus of your language?
How many seems like an irrelevant question to me. If there are such software domains at all, Zig may be a compelling alternative to languages that don't have a standard library that lets you choose an allocation strategy freely.
I also disagree that this is "the focus" of Zig. Technically speaking it seems like a rather minor design choice for the standard library.
Allocation happens. You can leave it to the compiler, the runtime, or let the code control it.
The automatic solutions are usually pretty good and usually the right place to start. But if performance is a priority, you want options.
BTW, “per-frame arena” is part of a general pattern of a repeated interval of work doing significant allocation. This is really common in software of all kinds… servers that process requests (like web servers and database servers) and typical command line tools.
Way back in the aughties I saw an interesting analysis (on a now defunct blog) indicating that under typical usage C# implementations tend to outperform C++ implementations in long-running business applications. The supposed reason was that C#’s compacting GC keeps the cost of new allocations fairly constant. By contrast, in C++ under typical use every new allocation requires probing for a sufficiently large block of free memory in an increasingly fragmented heap.
I haven’t tried to replicate this for myself. And, even assuming for the sake of argument that it was definitely true back then, a lot can happen in 20 years. But still, it does speak to wanting options when performance really is critical.
> For example Jai and Odin both seem to really want you to write code which can throw away a "per-frame" arena periodically
You can generalize this far beyond per-frame semantics. Think per-http-request, per-pubsub-message.
Per each, you can create a new virtual.Arena, set it as your context.temp_allocator and use it for the entirety of the request or message. Afterwards, throw it away.
The nice thing about video game frames is that they're monotonous. We render exactly one at a time for ever. So all the memory we don't reserve for long-lived data forms a single Arena for our per-frame temporary storage and we can put all kinds of stuff in there "for free" so long as we don't fill the Arena in any one frame we're all good.
Our HTTP server may have dozens, or hundreds, or thousands of simultaneous requests. But we've decided to give each a specific arena with a size chosen in advance so that we can use arenas. This means that the request to check a CSS file hasn't changed (it has not) and the request to modify the holiday of an employee whose manager is changing mid-way through their annual leave are both given, say, a 10MB Arena. No problem for that CSS check, some text parsing, one OS call, an ALU operation, done. But alas the SQL Transaction for that holiday change used up so much memory now the text parsing code "temporary" allocation in code to queue email for both managers fails and it unwinds the whole HTTP request. 500 error, rewrite your Odin program.
Arenas make lots of sense for video games, but their applicability for these other applications is much more dubious.
> Arenas make lots of sense for video games, but their applicability for these other applications is much more dubious.
It's a fairly common pattern in non-video contexts to preallocate arenas of different sizes for different workload pools (e.g. different routes for a server).
It's also fairly common to allocate per-work-item (e.g. request/session/batch job) arenas for "general-purpose" scratch allocations that are small (your header parsing, auth context retrieval, etc.) and then default to a global manual/refcounting allocator for one-off large data actions like your holiday change. Since most business applications are returning summary/small aggregates over the large data action (in this example, something like "holiday updated successfully", not the entire history of the PTO table or the database connection's internal state), the copy cost of moving data between the global dynamic allocator and the arena for result transmission tends to be small.
That's not a terrible approach in some situations. If the large majority of code only needs the scratch arena and/or some per-handle allocators stored on e.g. the database connection pool, it can work well. But if, over time, the amount of bookkeeping required to maintain data tagging/movement between arenas/allocators becomes severe, it's worth stopping, stepping back, and considering that you've kind of walked backwards into inventing a shitty generational GC.
Some of this surely stems from Rust not originally supporting allocators at the standard API level until later. You can manually create your own, and create/modify your own containers, but until GlobalAlloc came around it sometimes involved doing things like passing through Arc<Allocator> through to things if you wanted multiple things to use a single one.
Sometimes using a global one isn't the best thing (it's often a good idea to specialise base on allocation size, reuse and lifetimes), but I've used them quite a bit in C++ over the past 16 years doing HPC for graphics, rendering and simulation, so calling them only useful for "toy problems" likely shows you just haven't found a need for them in what you've been doing.
I’m not so sure the usefulness of custom allocators really relates to the complexity of the problem. Take the scratch arena example: ifaik it’s just free performance (& simplicity) whenever you need dynamic allocations with lifetimes that begin and end on a critical path. Presumably this is why Jai/Odin focus on this situation so much since it applies to per-frame stuff in videogames, but generally I’d expect this to come up more often in nontrivial problems.
> For example Jai and Odin both seem to really want you to write code which can throw away a "per-frame" arena periodically so they're not paying to track allocations in the arena because they're all thrown away at the same time
Given that both of these are intended for game development, wouldn't that make the most sense?
The C3 temp allocator leans into this though. It’s not a frame allocator but acts like a stack allocator. It solves the ”oh, how do you make sure the data you got back is properly freed”. In Zig you need to create and free arenas by hand, which is not far from freeing the returned allocations by hand.
Off topic, but I remember fondly the pre-LLM days when I used to love reading about programming languages. I never got a chance to professionally work with Rust, but made some cool hobby projects with it. Would have eventually tried out zig too.
Now it all feels so pointless though. Like memorizing rules to do mental arithmetic. Sure, there is still use for language expertise, but not enough to get excited over new concepts and ideas.
I used Rust extensively pre-LLM, and I'm much happier to serialize my thoughts to Rust than any other language.
I prefer to prototype in Golang since it compiles fast and makes for quick iteration, but at the end I ask the LLM to port the Golang to Rust.
Nice Rust enums for APIs are the chef's kiss.
I absolutely will not write anything in Python or scripting languages anymore. They're too brittle and don't have great devex or deployment stories. Especially when you can just as easily build in a typesafe language with good error handling that compiles down to a single static binary.
I was thinking about that too recently. We have a new service that we’re trying to publish an SDK for. I don’t like the SDK that was created and kept nitpicking about how verbose certain things are and how “unergonomic” it feels (long tedious type names, annoying redundant constructs, etc) But then I was wondering if for a brand new service/SDK if anyone cares anymore and how much fuss i should be making about that.
This is a bit nuanced, because if there’s redundant constructs then that does impact maintainability and efficiency with both runtime and LLMs working with the code.
I’d push this more towards personal preference of how code is expressed matters much less now than how maintainable it is.
There is the aspect of long type names, they often don’t have much impact when tokenized. The character count of words is nearly negligible - they often become one or two tokens anyways. But, the choice of words may have a greater impact on how the word choice weights an LLMs contextual processing of that word (a human may be able to ignore an inaccuracy in naming a bit more flexibly than some LLMs).
For years I used Rust as my hobby-programming language and loved it greatly. I never managed to land a job working with it full-time, because either the work was too niche or it didn't pay enough, or I was simply too comfortable where I was to change. And now that I finally have enough discretion over my technology choices to run a "proper" project using whatever tools and languages I want, it is not me but the AI that writes all of the code.
There's a part of me that feels a quite sad about all this. It's almost as if there actually all along existed a real final deadline on finding that "dream job". And I missed it. And while I expect this one miss to be just a small piece in the grand picture of things that we're going to lose or have already lost to the zeitgeist of agentic SWE, it feels big to me. It was my professional dream, while I still had professional dreams.
I seemed to have been through this pattern in my interests multiple times and just wait in the between times for something of interest. For hobby/interests I started with programming languages and then moved into understanding hardware and then mobile devices. I wrote HP 41 s/w, HP 48 s/w, the first public dethreader for HP 48 ROMs, Newton s/w, Windows CE s/w, jailbreak s/w and on device iPhone 1.0 s/w (pre App Store), but have been waiting for the next thing since iPhone 5 or so. In languages I went through a lot as well and now am waiting on C# to be improved.
I relate to this comment so much. Like you said, just the first in many things we will lose. Gratitude I can still support my family and build cool stuff at all now.
> Sure, there is still use for language expertise, but not enough to get excited over new concepts and ideas.
Programming and learning new things can still be fun in the era of agentic coding.
With LLMs, I get to quicky ask: what would this look like? Why do it that way? If you suspect that the LLM isn't doing it the right way, you can still investigate that yourself.
e.g. the other day, https://rhombus-lang.org/ was mentioned on HN. With LLMs, the cost for trying this out is practically much lower.
Yep, learning new things is amazing now. Just today I went through some really crappy slides, just dropped them to gemini and asked for elaboration. It saved me hours of figuring the shit out the old fashioned way.
> Sure, there is still use for language expertise, but not enough to get excited over new concepts and ideas.
In 5-10 years, the people who have paid attention to these will be needed to bail us out of the mess that the rest of the slop-addled monke brains have created.
It isn't like LLMs are actually good at programming, so there's no reason to give up on your interest in it. The hype around LLMs is not sustainable, the quality simply is not there.
Step zero of using arena allocation is to build realistic benchmarks so you can measure if it's worth the trouble in the first place. Standard allocators are incredibly good these days, and even plugging in mimalloc or jemalloc will be much less work, and much less error prone.
i think the reason is more that arena's let you manage lifetimes in groups instead of pointer chasing. if you have to manually manage memory, it's eaiser to manage a small number of arena objects instead of a large number of individual objects.
that being said, it's even easier to not manage any lifetimes at all :)
although i suppose some will say that you still manage lifetimes in rust, you just have full support from the compiler to make sure you do it right. that seems better to me than relying on simplification to ensure you don't make mistakes.
I don't know about more error prone. Benchmarks completely aside, freeing a batch of stuff you've allocated in a single place makes it easier to manage memory. I think it should be preferred wherever it's an option for that reason most of all. The "killer app" is something like an arena allocator that lives for the duration of an HTTP request.
I respectfully disagree. The technique has its place, and I use it once in a while, but whether it makes a positive difference for performance is highly sensitive to a number of factors.
For example, bump style allocators allocate very quickly, but at the cost of higher memory usage and therefore sometimes worse cache locality.
You disagree with a point I never made. My point is that they enable easier memory management, regardless of whether they're more performant. I really don't understand how that point didn't get across. You must simply not have read what you responded to.
> I think it should be preferred wherever it's an option for that reason most of all.
And that’s far too broad in my opinion.
Sure, if you’re writing in a language that makes memory management difficult, like C or Zig, that might be worth it, but arena allocators apply to many other languages too.
> I respectfully disagree. The technique has its place, and I use it once in a while, but whether it makes a positive difference for performance is highly sensitive to a number of factors.
My argument deliberately and explicitly ignores the difference in performance characteristics. Your concerns about performance are irrelevant to my argument.
Now, I could respond to your new, entirely different argument, but I don't see the point of that if you ignore my response to the arguments you've already made.
Similarly, it's far too broad to say that it's "much less error prone" to use a more general allocation strategy. Clearly we both make unspoken assumptions, but in the context of this discussion on Zig, where you initially responded to an example written in Zig, I think you actually understood what I meant.
I meant to suggest using arena allocation for everything and not even using a malloc style allocator. Just getting memory via memmap at program start and then using arenas for everything after that.
It makes it much easier to avoid lifetime mistakes in my experience.
Using arena allocation also makes me think more about how much memory I am using and how much memory I should be using etc.
It is hard to benchmark it against just using a global allocator because it is a structural change to the whole codebase.
If this works for the programs you write, that’s great. It does preclude you from using many great data structures with potentially better performance - especially hash maps. Rehashing is pretty detrimental to most arena allocators you can think of.
1. Tooling (as an extension to the mentioned IDE support point). Zig and Rust are both praised for their tooling and I think rightfully so. The C/C++ interop story and the cross-compiling story in Zig are great. From the standpoint of a working practitioner though I think Rust is way ahead. Not surprising given that Zig is much younger, but something to keep in mind.
2. Compile Time Stuff: Here Zig is praised and Rust not so much. I think this is undeserved. Rust has much higher aspirations for their compile time features, namely that outcome must be identical regardless when the code runs. This is a very useful property but makes the task much harder and fundamentally incomparable with Zig comptime.
There will never be a true successor to C since C is "high level assembly." The error checking and bounds checking that are missing aren't an oversight but by design! Assembly doesn't have them so neither does C! There's no reason to whine about it.
The real problem is that people are using C for the wrong reasons. C is for the development of operating systems and low-level code, not applications.
Zig is more modern but anything that does even one iota more of hand-holding or has anything that looks like a guardrail fails the test.
If you want to write applications use Pascal, Java, C# or Rust.
It's useful, but systems programming languages shouldn't be used to create applications in the first place.
We're trying to solve a problem that shouldn't be solved. We're continuing and even confirming the usage of systems programming languages for application development.
I don’t think this delineation is that clear, unless by “application” you mean the app tier of a 3 tier app.
Postgres and Nginx make sense in system programming languages; they’re extremely performance sensitive and that granular level of control offers them features. Interpreters are sort of the same, they interact with the OS a ton, it makes sense to work in the same language as the OS.
I do generally agree for the app tier of a web app. I wouldn’t build a CMS in Rust, but I also wouldn’t build a reverse proxy in Python.
Writing an operating system kernel in C is valid usage since there's a great deal of thought going into it and it almost never changes.
EVERYTHING ELSE is invalid usage no matter how performance critical people claim their application is. These are just excuses for people to use a grossly unsafe language to get that last 2% of performance whilst costing the world trillions in lost productivity and security breaches.
New development in C++ would be a dire mistake, but you can't expect everyone to switchover on a moment's notice.
C and C++ are a liability for many reasons since memory corruption is notoriously hard to detect, localize and fix. And multi-threading is even harder, almost a dark science to get right.
The big one for me is that Rust packages tend to pull in dozens or hundreds of transitive dependencies - C/C++ code tends to be monolithic, so it has fewer maintainers who could become compromised.
i dunno, people are using rust effectively to build applications too. and apple has started using swift in the kernal.
i do agree that applications are probably best built in fast garbage collected language. but it also seems like go, java, C#, etc have other downsides that push people towards things like zig or rust even for apps. it also depends on what you mean by "apps". is a server an app? what about an actual native cross-platform desktop application? does go, c#, or java have a good paradigm for that? what if you want to use an oss language not tied to a big tech company? you start running out of suitable languages pretty dang fast.
C wasn't meant to fully replace low-level languages like Assembly. Context switching still isn't possible in C (nor in Zig).
And most C library calls are actually Unix system calls. Malloc() is an operating system call and not a library call. Yes, they made it a library call later on to facilitate usage in other operating systems but it was originally an integral part of the Unix OS.
For various purposes I work on a language comparison project that includes C and candidate successors such as Go, Zig. One question is the language to use for an archival port of a 1980's computer algebra system written in 32-bit K&R C. While Zig is a great debugging compiler, it's not yet stable enough to be the best target language for archival purposes.
So you're in a restaurant where you don't speak the language, you can't read the menu, but you see three price points for set meals featuring the house specialty. (Say, "Crossing the Bridge" noodles in Yunnan.) Which do you choose? My tour guide, the author Fuchsia Dunlop, later agreed with me this is obvious: The middle choice.
So you're choosing between Go and C23 as candidate successors to K&R C. They both have "royal blood". One got the name. Knowing nothing more, which do you choose?
The answer is equally obvious. The one that got the name also got the warts.
Middle may be wrong (it is a marketing trick to add 3rd outragesly expensive option, to make 2nd option look reasonable). The correct answer is “it depends” (even how long you should spend on choosing may depend on context too).
For example, write in whatever language you know best, then translate to a more appropriate language using LLMs once the desired behavior can be checked automatically. It is a tactic that works in some cases.
It can be used as a marketing trick, possibly because of the logic in GP post. Marketing people use this to trick people, which is different from it being a marketing trick in and of itself.
If you’re C suite, ignorant and stupid and don’t know what you’re doing which choice minimises your chance of screwing up? The above is an analogy for that situation.
If you’re not stupid it fails because you can find out and then make an informed decision. An informed decision will dominate the suggested strategy in terms of chances of not screwing up.
This really should be obvious.
(In the analogy, your phone has an automatic translator and you use it).
Note that I have made no argument for one language over another.
The point for those who clearly missed it is an objection to “if you know nothing more”
No.
Find out more.
You could still be wrong but it’s less likely than if you apply hokey heuristics on wilful ignorance hoping it somehow works out sight unseen to knowable problems that can be understood and analysed.
One of the section headings says "Mutation vs. immutable monad is the core difference" but this is not true.
The code in that section is clear in its purpose and broad outline: "Give me a function and data; if the data is bare apply the function to it; if the data is a container apply the function to each item inside it."
You can absolutely do that with immutable data structures in Zig. You just have to pass an allocator to the function (i.e. instead of calling data.flat_map(f), you call data.flat_map(a, f) where a is your allocator).
That the Zig version of the code does mutation is a matter of programmer choice, not something imposed by the language.
Further up he discusses that using a functional paradigm is possible, but concludes that it doesn’t feel like a practical choice because of how Zig does memory management:
> But the language quickly forces you to diverge from the functional style, mostly because you’re now dealing with allocators directly, and a genuinely pure functional approach means constantly constructing new structures. That’s either expensive in memory or expensive in the manual bookkeeping needed to avoid it.
So I don’t think he’s trying to say that it’s literally impossible. It felt more like the result of a good faith attempt to understand how Zig itself actually wants to be used, and to compare that to how he’s used to using Rust.
I actually liked that he did it. So many other comparisons want to evaluate one language against the other language’s values. But I don’t want to know how well Zig can do Rust; I want to know how well Zig accomplishes its own goals, and what those goals are.
So, what you're not seeing is that even though the Rust doesn't announce mutation the implementation may be mutation anyway if that's probably faster/ cheaper.
unsafe impl<I, U, F> InPlaceIterable for FlatMap<I, U, F> where
I: InPlaceIterable,
U: BoundedSize + IntoIterator,
What you're seeing is the graceful goose on the water moving forward at pace. Beneath there is frantic action to make that happen. The Rust surface was more a maintainable immutable operations, but the implementation is a frantic whirling mutation like the Zig.
In Zig you end up spending a lot of time writing How to do a thing, where in Rust you only wrote What the thing is and the machine did it. There are edge cases where Zig's explicitness wins for the best programmers, but there just aren't enough of those cases or those programmers for this to net out IMNSHO.
Maybe I'm not that deep into programming, but I don't understand the hype about Zig?
I programmed in rust a bit and can't say I'm an expert, but in my view rust mostly-solved the memory management problem at compile time and without a GC, and it works very well. The biggest con and cost I've always seen repeated so far is that "it's slow to compile", and I get that, if you're past 250 crates the final --release link tends to become noticeable, but there were improvements to incremental compilation.
On the other hand - looking at the syntax from this post - Zig feels a blend of javascript, python and golang syntax that still requires memory management. So a nicer-written C that inherits all the issues from C? From the post: no functional programming, data mutation, memory leak, double-free, memory corruption.
Personally I'd rather trade a couple minutes of final link every time when this is the other option.
yeah to me zig exists as a counter-reaction to rust. which means avoiding both the good and bad things rust does. and rust does a lot of things right, so...
Rust is for devs who think "if only c++ had a few more features, it would be perfect". Zig is for devs who think "if only C had fewer features, it would be perfect"
Give it a chance! Rust hasn't even had its bar mitzvah yet and C++ is already buying a Porsche during its midlife crisis.
Yes, on paper zig has more features than C, but what zig has is explicitness. C has a big murky space of implicitness. For example, there might be 5 different zig features which can be used at various times you might use a void* in C, buy the conceptual space of void* fully contains(and then some) the spaces of those zig features.
Basically you pick the type safety that Modula-2 or Mesa already offered in the late 1970's, repackage it with comptime and more C like syntax, and have a whole legion of new devs jumping into it.
Note that AT&T, where UNIX and C were born, the language they were researching as C replacement was Cyclone, not something that is not much different.
Not everyone reads the same posts, and post 2000 younger generations need to be educated in what came first, what is actually new, and what is language adoption marketing.
Because it is a rip off, pretending nothing else has done it before, in fact it could even be called a PL/I ripoff for that matter, which predated C for about a decade, had already had all the bounds checking, proper strings and arrays, was actually used to deliver several IBM OSes, and is used to this day on z/OS. Certainly much larger production deployments than Zig.
Yes I will, as long as I am part of this realm, can be in HN, or whatever is the social media of the day, I started with BBS, USENET, Slashdot and plenty others.
Zig is aiming to be lower level than Rust. As the project homepage prominently advertises, it has no hidden memory allocation or control flow. It also gives more control over how memory is allocated, which is potentially useful in applications with particularly tight performance requirements.
Kelley first created Zig when hr was working on a digital audio workstation and found most existing languages to be awkward for working with particularly hard real time requirements, but still wanted something more modern than C. Im speculating here, but I believe its advantage over Rust for that specific application is that you have tighter control over exactly when memory is allocated and deallocated, and how data is laid out in it. Rust wants to tie allocation lifetimes to scope in a very fine grained way that I would guess is beneficial the vast majority of the time, but does still make it harder to reason about when you’re about to stall out the CPU while the allocator does its thing.
Zig and Rust are equivalently "low level". Zig isn't any closer to the hardware than Rust is.
> but I believe its advantage over Rust for that specific application is that you have tighter control over exactly when memory is allocated and deallocated, and how data is laid out in it.
Rust gives you all this, too.
Zig's primary (possibly only) advantage over Rust is that it has much faster compilation times.
Comptime is a big one too. You can achieve similar things in Rust with generics and macros, but comptime makes certain things easier (and other things harder).
Rust could not work in a world where compilation was a single threaded affair. I think it exists now instead of in the 90’s and 00’s in good part because of this.
I don’t think it’s an accident that it has succeeded as multicore took over. We are now well past a point that a task that can be split with 70% efficiency into multiple parallel tasks is 5-10x faster than the optimal sequential solution. Expensive multicore machines existed when Rust was a baby but it didn’t really catch on until 4 core was common in consumer hardware. And now I have an ancient laptop with 16 cores.
But Rust is also good for producing correct code to run on those systems. So it benefits twice.
Zig is exciting to people that still actually like C. Is that a rationale choice? Not very often. Is it a wrong choice? Also again not very often. At least, not for anything in scope of a solo dev.
The industry where it seems strongest positioned is embedded. Will it actually break into that domain? No idea.
As a C veteran, Zig is interesting to me because of the compiler that still happily eats C. It has potential as a (cross!) development tool that can do a bit more than C tools alone. Which also describes Rust to some extent, but I think Rust would be more a full replacement. Which in turn is exciting because there hasn't been anything before that might replace C in my (embedded or -ish) environment.
I haven't actually tried to find out if that Zig promise is real, though. Just some trivial little things. No day job excuse and no personal projects where it would fit.
As a long-time low-level programmer, and as someone working on a popular mainstream language, I find Zig fascinating, and I also think it addresses a long-standing problem in low-level programming. I'll get to the problem later, but the fascinating part is its use of partial evaluation (comptime) as a single coherent mechanism that replaces a myriad of other partial-evaluation mechanisms (macros, templates/generics, constexprs). That one mechanism is the core of the language, like macros are in lisps, and that design - whether you like it or not - is revolutionary. It's never been done before (other languages have partial evaluation mechanisms that are almost as general, but they're offered in addition to, not as a replacement of, other features).
> rust mostly-solved the memory management problem at compile time and without a GC
"Mostly" does a lot of work here because 1., if you look at the implementation of very efficient, possibly specialised data structures - the very thing you reach for a low-level language for - they typically require unsafe, and 2., it still suffers from the problem C++ has had for decades, which is that over time, as program changes and evolves over years, things tend to drift toward the more general mechanisms that rely on malloc/free on an individual objects, and the program gets slower and slower (huge runtimes like TCMalloc help, but not enough, because they can't move pointers). To truly compete with the performance of moving GCs, which allocate through bumping a pointer and deallocate objects in bulk, low-level languages need to rely on arenas (which work similarly to moving GCs), and Zig is the first language that makes arenas almost user-friendly and hopefully sufficiently composable to withstand program evolution.
> the very thing you reach for a low-level language for - they typically require unsafe
There's a formal proof asserting that if you keep up the safety invariants within an unsafe region then that will not infect other code, even in the presence of arbitrary other correctly-written unsafe blocks.
This means you can build abstractions on top of these low-level primitives to keep it contained, so consumer code never has to even think about or know there's unsafe blocks in it. The type system lets you build very powerful abstractions so these go a long way.
There's a lot of woo-woo scare quoting around how much you actually have to use unsafe code in Rust. It's fairly uncommon to actually have to reach for them in practice. Most of my usage ends up being things like converting a &[u8] to a &str when I know it's already valid UTF-8 so I want to skip the linear-time validity check. Very rarely do I have to build data structures with complicated pointer juggling, because there's often a library that already does what I need!
> which is that over time, as program changes and evolves over years, things tend to drift toward the more general mechanisms that rely on malloc/free on an individual objects, and the program gets slower and slower
What are you talking about? I've never encountered this and I've been using Rust for 10 years.
That's always been true in all the safe languages with unsafe escape hatches, except here these "primitives" are the main reason to reach for a low-level language in the first place - because they presumably require the control that low-level languages offer. Combining them in the same language might appeal to some and not to others who think that the high-level, safe parts are unnecessarily complicated because it needs to integrate with the low-level parts, and the low-level parts are unnecessarily complicated because they need to integrate with the safe parts. Anyway, some like this and some don't, but my point is that it's not "mostly solved".
> What are you talking about? I've never encountered this and I've been using Rust for 10 years.
Okay, but I've been doing low-level programming for 25 years, and have encountered this over and over in large programs (over 500KLOC) as they evolve.
That's just not an accurate description of how you write Rust in practice. There's no separate "high level" and "low level" parts/forms of the language any more than the software development process already is all about building abstractions. You should be doing this in Zig, too.
It's just that sometimes some of the abstractions you need to build go outside what the ownership and borrowing system can model. And when you don't need to do that (which is 99% of the time) you also get all the benefits of the ownership/borrow system for free.
They're are not separate forms but they are separate modes, and it is precisely because the language tries to fit both these modes into the same language that both suffer. I fully understand the goal of trying to unify these modes into the same language (C++ does the same thing), but there have always been very experienced people who like this approach and people who dislike it, hence it's not "solved". Something is solved when there's a broad consensus it's solved, and there isn't one here.
I don't know exactly how specific you want to be, but sure, because we've come across this countless times in C++, which suffers from the exact same problem.
Suppose you're writing a program that's mostly high-level, say some kind of concurrent server, and it's large-ish, say around 1MLOC (most C++ programs I've worked on were significantly larger). Because the language is also a low-level language, it has low-level constraints, so:
1. It needs to use an AOT compiler, and consequently to get good performance you need to use less general mechanisms, such as direct (as opposed to dynamic) dispatch and even manual monorphisation (with generics/templates). These are viral, so they have to be carefully chosen (you can't monomorphise everything or you'll get machine code explosion). Five years later you need to make a big change that requires more generality, and then you either have to reconsider all of your manual optimisations, which is expensive, or go for more general constructs (dynamic dispatch) and the program gets slower.
2. It needs to use machine pointers (i.e. you can't enjoy a moving GC), and so you try to use the stack as much as possible (which you can't really do for anything dynamic), or suffer the high cost of malloc/free on individual objects. As the program evolves, you need to make things more general, and objects that could live on the stack now need to go on the heap, and objects that lived on the heap now may need to be shared among threads, in which case you often add the additional cost of refcounting GC. Of course, you want to use arenas in many cases, but they're very, very hard to use in C++ and Rust.
You'd be better off - performance-wise and maintenance-wise - with a good optimising JIT and a moving GC. This was exactly a problem with many C++ programs that didn't really need a lot of direct hardware interaction - everything worked great for a few years, and then the evolution and maintenance costs became really high (or the programs became slow).
Now suppose you're writing something low-level, i.e. you really need to interact with the hardware and/or OS directly a lot, and want to control everything - where everything is in memory, exactly when it's initialised, exactly when it's freed, exactly which operations are executed and when. But now you have a language that's also high-level, so it has a lot of implicitness that hides from you the things you want to see (and in Rust's case, you lose the safety). Best case scenario, you rely on disciplne and avoid implicit features, but then you also need to avoid much of the standard library.
Anyway, combining high and low level in the same language was C++'s dream: one language for everything. Of course, for a while we didn't know about the maintenance problems, as those appear only years down the line, but more importantly, there weren't really high-performance high-level languages back then. These days, with lessons learnt and with more options, I prefer a language that focuses on being high-level for high-level stuff, and a language that focuses on low-level for low-level stuff. If you really need both kinds, use two languages.
> I've been doing low-level programming professionally for 25 years
You haven't been doing any Rust though. You seem to think you can extrapolate your C++ experience to Rust. That's preposterous. The actual Rust programmers can't recognize this theoretical problem in their Rust programs.
Could that be because the language is fairly young? You don't see the "20-year-old legacy system" in Rust because it doesn't exist yet ;)
And if you look at other comments in this thread, many engineers have this mentality of "just use a crate, it's probably optimised already". They might not have performance problems immediately or obviously but it's more like ten thousand papercuts - a few allocations here and there, a few extra copies here and there and you've got a way slower program than it should have been.
Well, the problems don't start after 20 years but after 5 or so (depending on the size of the codebase and the rate of the application's evolution), and the reason there aren't many large and oldish Rust codebases isn't because the language is too young for that (work on it began twenty years ago, and it's been stable for over a decade); that's middle-aged for a programming language. When C++ was of a similar age, there were thousands of >1MLOC programs written in it. One reason is obviously because when C++ was of the same age, there weren't as many suitable high-level alternatives, and people just don't pick a low-level language for most large applications anymore. But most Rust fans at least on social media, have not actually had much experience with it or with low-level programming in general; I'm guessing most haven't worked on Rust projects with more than 10 full-time people on them (this isn't normal in the industry, BTW, as a lot of software lives in large programs). And again, there are people who can certainly live with these issues, but they are real, and many certainly find them troubling.
It's not theoretical, it's one of the main reasons many large applications abandoned C++, and there's absolutely no reason for it to not exist in Rust. All low-level languages suffer from expensive evolution for fundamental reasons - the reliance on an AOT compiler and the lack of movable pointers impose serious performance tradeoffs in large programs. Optimising JITs and moving GCs were invented, in large part, to address this very real problem, familiar to many low-level programmers who have maintained large codebases for a long time. It's also why large runtimes like TCMalloc were invented to assist as much as they can.
Most actual Rust programmers haven't maintained a large Rust program for a long time. Now, don't get me wrong - there are many C++ programmers who are fine with it, but many who aren't. What I find annoying is people without much experience in Rust assuming that everyone or almost everyone should like it, even though that's never been true for any language.
> > which is that over time, as program changes and evolves over years, things tend to drift toward the more general mechanisms that rely on malloc/free on an individual objects, and the program gets slower and slower
I think the idea is that a small program can organize its allocations and data structures to minimize number of calls to malloc, e.g. with preallocated workspace structs, or slab allocation, and similar approaches. But as a program gets bigger, there's a pressure to have looser coupling, to have subsystems with simple convenient APIs which leads to them doing on-demand malloc calls internally, rather than having consumers pre-allocate their needed workspace. Because that kind of workspace management results in more complex APIs and more burden on the consumer.
That said, I don't really believe it either, at least for the kind of codebase where it would matter (scientific computing, in-memory DB server, etc). A codebase that places an emphasis on minimizing heap operations in hot codepaths can do so by consistently using workspaces and allocation-avoiding APIs. I don't think it's so difficult really, but it does take a conscious design decision to do so. But writing something like a web browser in this way could be annoying due to most data having wildly variable sizes, and zig's arena concept would be very handy -- but rust has crates like bumpalo for that purpose.
My personal mantra: "Think in FORTRAN, code in Rust/Julia/C++". But I'm mostly working on HPC-style code where I don't have to do with wildly varying input or output sizes.
> but rust has crates like bumpalo for that purpose.
Except that's not composable, the exact same issue we have in C++.
> But I'm mostly working on HPC-style code where I don't have to do with wildly varying input or output sizes.
There you have it. The problems arise more quickly in concurrent rather than parallel code, and when there are lots of features added over the years that touch the hot paths.
> in-memory DB server
Actually, here there can be big problems (as it's also about concurrency rather than parallelism). Last week a colleague of mine looked at Moka and saw that it could only offer half the throughput as Java's Caffeine at the same latency. When he looked into it, he saw that over 40% of the program's CPU was spent on the epoch-based reclamation.
> not only do you need specialised data structures, but all (transitively) allocating calls need to be specialised
Most crates for containers will be written such that the container types take an optional allocator type parameter that defaults to the global allocator. You can set it and it transparently uses the other allocator.
To improve the ergonomics, you'd define local aliases that use that allocator.
When that is the case (and it isn't yet; and remember that it's not only the containers, and strings, that need to be parameterised, but any routine that allocates them, transitively), then that's what Zig does. But the question was doesn't Rust solve memory management already, and this is an important aspect it clearly doesn't solve just yet.
there is nothing stopping you from using custom allocators with your own code or with calls to thirdparty dependencies
but custom allocators are rarely used in rust because they're simply not needed the vast majority of the time. if your language is not memory safe and you need to manage memory yourself, they're more important. but this isn't the case with rust.
c and zig folks are obsessed with arena allocators particularly because they can group lifetimes of individual objects, reducing the amount of malloc/free calls and thus the amount of use after free, double free, nullptr derefs, or leaks that can occur.
in rust this isn't a concern so custom allocators are only used for performance reasons.
but it turns out that in performance sensitive areas, you generally use custom data structures or those that already have their own allocation strategy baked in, like the generational_arena crate.
most of the time you are not calling thirdparty crates that allocate in performance-sensitive regions. either the crate is designed for this usecase and already uses a performant allocation strategy, or you're writing your own code here.
and in the rare case, you can trivially vendor the crate and pass your own allocator into it, or toggle the global allocator for callers.
but you also need to benchmark first before choosing an allocation strategy because it's not clear that a custom allocator will always guarantee better performance anyways.
and btw zig doesn't guarantee this anyways. you could pull in a dependency that instantiates their own allocator. at least in rust almost all crates use the global allocator as a default which lets you swap it out. if a zig dependency uses their own allocator the only recourse is to fork it.
rust doesn't have a performance problem, so any claims about it's custom allocator support leading to poor performance is unfounded. and thus so are claims about the superiority of zig's approach to allocators.
> i find it fascinating how big of a rust hater you are. willing to outright lie to make your point.. because you can do this with_allocator
You say I outright lie for not mentioning something that doesn't exist??? I guess you're saying you could create such a mechanism (or that some libraries have such ad-hoc ones), but that's not the point.
> there is nothing stopping you from using custom allocators with your own code or with calls to thirdparty dependencies
I didn't say there's anything in the language stopping C++ and Rust from having such a standard library and ecosystem of libraries. They just don't have that yet.
> if your language is not memory safe and you need to manage memory yourself, they're more important. but this isn't the case with rust. c and zig folks are obsessed with arena allocators particularly because they can group lifetimes of individual objects, reducing the amount of malloc/free calls and thus the amount of use after free, double free, nullptr derefs, or leaks that can occur. n rust this isn't a concern so custom allocators are only used for performance reasons.
This is simply untrue. I won't call it an outright lie because you clearly don't have much experience.
First, I'm trying to point the problems we've had in C++, most of which only became apparent when evolving large codebases over time. People who have not had experience evolving large C++ or Rust codebases over years simply don't know about these problems and certainly can't claim they don't exist. Writing smaller programs in C++ or even large but young programs has always been a pleasure. The language is expressive and productive. Some of the biggest issues only arise years later, when the program gets either expensive to maintain or slow.
Second, experienced C and C++ folks cannot be "obsessed" with arenas for the reasons you mentioned because until maybe 20 or even 15 years ago memory safety wasn't a widespread obsession. It was a correctness issue like all others, and its exceptional prevalence as the cause of security vulnerability wasn't known (or wasn't widely known).
Lastly, you don't pick Rust for safety. Most software in the world today is already written in languages that are as safe as Rust if not more so. These days, you pick C, or C++, or Rust, or Zig when you want to do something that's largely low-level. Things that are low-level often also need to be fast, or at least reasonably fast, and large low-level codebases that evolve over years tend to suffer serious performance issues because of memory management (this is why companies with actual experience with long-maintained large low-level codebases make huge runtimes like TCMalloc to help them, which you also may not have needed yet), and arenas are the primary way to get memory performance similar to what you see with modern moving GCs (and even somewhat better).
Now, you can say that C++ only started moving in that direction with pmr in C++ 17, and that's true. But the need was recognised as early as 2005, traditionally C++ codebases didn't rely on many libraries so interoperability has typically not been a large concern, and the number of large C++ programs that would benefit from such a thing declined over the years because of the low-level maintenance issues I mentioned and the growing availability of super-fast high-level languages.
> There's a formal proof asserting that if you keep up the safety invariants within an unsafe region then that will not infect other code, even in the presence of arbitrary other correctly-written unsafe blocks.
In general "unsafe" does not compose.
"if you keep up the safety invariants within an unsafe region"
I'm curious why you think that statement is doing heavy lifting. It's much easier to write and verify that a few lines of code are correct than it is to write and verify that an entire program is correct. But that's the norm in C and Zig, and historically people haven't been very good at it. That's why we try to do as little as possible.
Many more C programs have been verified than Rust programs. Also, Zig's spatial and memory safety is as good as Rust's, so it's not really similar to C at all.
The reason it's not "the norm" is that (especially with spatial safety taken care of), not every line is equally dangerous at all. Still, there's no doubt that more guarantees help, but that is only when all other things are equal. If you pick a low-level language for mostly low-level things, so Rust doesn't offer safety for the trickiest code, and furthermore it makes certain things harder to see because the language is more complicated, then things become much less clear. Obviously, when the vast majority of the trickiest, most important code doesn't need to be low-level, Rust would probably be safer on the whole, but in such situations I see no reason to choose either Rust or Zig. You need to choose a low-level language if the core of what you're doing needs to be low-level.
That's because Zig offers spatial memory safety (e.g. buffer overflows and index out of bounds), but no temporal memory safety (e.g. use-after-free). I suppose the "and" before "memory safety" is a typo.
You must reason about the invariants in unsafe code on a global level. In particular, you could have unsafe code in crate A, whose data are then used by crate B. It could be fine. But then crate B changes its implementation which now violates the invariant expectations of crate A.
This is true. In Java, we have a notion we call "integrity", which is a generalisation of memory safety and includes a host of properties guaranteed by the platform. It includes memory safety, but also things like "a non-public method cannot be called or a non-public field cannot be accessed (even reflectively) by code in another module".
To address the problem that once integrity can be violated anywhere, only global analysis can prove that nothing bad happens, we've done two things:
1. We require the application to explicitly permit any integrity violation by a module; i.e. a library can't allow itself to violate integrity. This is a principle we call "Integrity by Default" (https://openjdk.org/jeps/8305968).
2. We try to minimise the need for potential integrity violations (this is very different from Rust, which requires unsafe even for things like benign write/write races, which are fairly common, and various basic data structures). Over the years we've offered safe replacements for things that used to require Unsafe. In other words, clearly demarcating unsafe code isn't enough if it's needed at all in many situations.
It isn't perfect, of course, as some libraries do require unsafe operations for direct interaction with native code or with memory, but their number has been greatly reduced, and they cannot do this without the application's explicit approval. Interestingly, this has annoyed library authors who want to do unsafe things but don't want to application authors to be alarmed because "we know what we're doing," and it's also annoyed some application authors who want to use such libraries and are forced to explicitly add permissions. But I think that the community, as a whole, has eventually accepted this because the harm done to those who don't care is small (they just need to add the permissions), to those who do care it helps a lot, and because fewer and fewer libraries require "integrity-busting" permissions, many applications need to do absolutely nothing and get important guarantees for free.
> this is very different from Rust, which requires unsafe even for things like benign write/write races, which are fairly common, and various basic data structures
I know this paper [0] is quite old at this point, but the mention of benign data races reminded me of it. Would you happen to know how applicable it is to modern memory models?
Benign write/write races are quite common and useful, both in parallel algorithms and in lazy initialisation. Useful benign read/write races are far more rare to the point I'd say it's ok to assume they don't (or shouldn't) exist.
> In particular, you could have unsafe code in crate A, whose data are then used by crate B.
Is this backwards? If B consumes data from A then to me that does not imply that A depends on anything from B; for a more concrete example that sentence reads to me like A is basically "throwing data over the wall" to B and whatever B does with said data is of no relevance to A. As a result, if B changes that shouldn't affect A.
Also for what it's worth I get the impression you and treyd might be talking about slightly different things when talking about whether unsafe code composes. I believe treyd is referring to the RustBelt series of papers [0, 1], for which the statement "unsafe code composes" means (at a high level) that adding a module with a memory-safe API to a memory-safe system will result in a memory-safe system as long as the implementation upholds the safe semantics. Yes, the last bit can be a rather significant caveat, as you said.
What you're talking about seems more along the lines of needing to look beyond the boundaries of unsafe blocks to prove that the unsafe block upholds its invariants, which is also true. I think you only need to check within whatever safe encapsulation boundary is relevant, though, rather than globally.
> Is this backwards? If B consumes data from A then to me that does not imply that A depends on anything from B; for a more concrete example that sentence reads to me like A is basically "throwing data over the wall" to B and whatever B does with said data is of no relevance to A. As a result, if B changes that shouldn't affect A.
This is a specifically crafted bad idea, but you could have module A use unsafe to craft a Vec<u8> that is safe to use to read or write, but not to grow or shrink. You declare an invariant that the receiver shalt not grow or shrink the Vec.
If B only reads and write, you're good. But if a future B breaks the invariant, bad things happen. As I said, specifically a bad idea; there's a much better type to use if the thing can't grow or shrink...
No real world example, because I don't think we've run into memory safety issues with unsafe in the Rust code base I work in... but we only use unsafe where it's required (syscalls and other FFI).
Fair. I suppose that even in such a scenario you shouldn't need truly global analysis to prove safety - in principle an analysis of A should reveal the soundness precondition on a safe API - though that's probably easier said than done.
This hasn’t been the finding of the R4L project. Go look at their code, it’s shockingly safe outside of the parts that interact with extern “C” symbols, which naturally need to be unsafe.
> partial evaluation (comptime) as a single coherent mechanism that replaces a myriad of other partial-evaluation mechanisms (macros, templates/generics, constexprs).
So sad dlang never gets the credit it deserves. None of these ideas in zig are novel.
Yes. I don't really understand why Zig gets more attention than dlang. It seems that it is more to do with the personality of the authors, rather than the technicalities. I get the impression that Walter is more laissez-faire, while Andrew is more single-minded. I'm grateful to the contributions both have made.
Pardon any corruption in my memory. But I think D in it`s early days had non-optional garbage collection. Which seemed like a bad choice at a time when I think many of us wanted a better C, not a more inconvenient C#. Then they made garbage collection optional, which split the ecosystem into garbage collected and manual libraries. Not a great choice either. Then there was a drama and split because of two competing standard libs. And also the compiler was closed source which turned some people of in a time where open source compilers had become an expectation. So all in all, a lot of bad decision made it lose momentum and I don`t think it can recover in todays climate.
For me it's because Zig presents a novel and even revolutionary new coherent design for low-level programming, whereas D is mostly a collection of many ideas, many of them are good, but they don't coalesce into a simple design philosophy. I think somebody once said that good (or coherent) design is what happens not when there's nothing left to add but when there's nothing left to remove.
Zig, for better or worse, have a strong messaging and isn’t afraid of making big claims. That they don’t always pan out (”faster than C”, ”colorless async” etc), doesn’t matter. Zig was the first C alternative to spring from the interest Jon Blow ignited with his language dev and that marketing lead has been superbly well maintained by Andrew.
Odin and C3 has at least a magnitude fewer people knowing about them. More modest claims, less marketing potential.
So for D, it was to my memory never sold as a silver bullet either. A better C++ certainly, but with enough humility to not market itself as some game changer.
Compare Zig, V and Rust marketing with D’s and you’ll see what I mean.
It is entirely novel. As I wrote, what makes the design revolutionary isn't the partial evaluation mechanism itself, but how the language is organised around it. It's like what made the iPhone's design revolutionary wasn't that it had a touchscreen, but that it didn't have a keypad. Partial evaluation mechanisms aren't very interesting in themselves; what's interesting is building a language around a unified partial evaluation mechanism and little else. But yes, D gets credit for adding more partial evaluation features.
Few ideas are new under the Sun. But the inventor and the popularizer are rarely the same. We should be happy that the ideas got more exposure instead of litigating novelty.
> but the fascinating part is its use of partial evaluation (comptime) as a single coherent mechanism that replaces a myriad of other partial-evaluation mechanisms (macros, templates/generics, constexprs). That one mechanism is the core of the language, like macros are in lisps, and that design - whether you like it or not - is revolutionary.
That IS cool. I loved partial evaluation the first time I heard of it - Futamura projections, here we come! And if it can be used to obsolete language features I never was happy with (macros), so much better!
But until now, I never heard of it as a selling point for Zig. I assumed Zig was just yet another "C replacement but we don't want to deal with Rust's borrow checker". I will certainly have to take a closer look on Zig now. But this should be up and front in their self-promotion!
It is! "A fresh approach to metaprogramming based on compile-time code execution and lazy evaluation" is the second selling point after simplicity: https://ziglang.org
Obviously, they can't call it "partial evaluation" because not many people know what that is.
As far as I understand it comptime is basically runtime compilation and integration into the running program (just-in-time compilation). That gives it great flexibility of course but it also requires each Zig application to carry a full compiler inside of it.
I don't understand your C++ example about it getting slower and needing to "move pointers."
> As far as I understand it comptime is basically runtime compilation and integration into the running program (just-in-time compilation). That gives it great flexibility of course but it also requires each Zig application to carry a full compiler inside of it.
Well, it isn't that and it doesn't require that.
> I don't understand your C++ example about it getting slower and needing to "move pointers."
Because low-level languages need to use machine pointers, their dynamic heap allocations have a high CPU overhead; it's that overhead that moving GCs are designed to reduce, but because they move pointers, using them requires an FFI between these pointers and machine pointers. This is why in low level languages we try to avoid dynamic allocations when we can, but that increases long-term maintenance costs.
If you're writing ultra low latency code you basically want everything allocated from an arena, with different arenas for different kinds of objects. Zig comes with this built in, while Rust makes it extremely unergonomic to do safely (due to the lifetime system).
Arenas in Rust work very well with lifetimes. In fact arenas benefit greatly from lifetimes, because lifetimes allow them to uphold the usual Rust safety guarantees about preventing use-after-free. For example, here's the bumpalo crate in action:
As someone who primarily writes in TS, Go, Python, I agree with your assessment: it looks like those languages. The reason I like Go is the reason I like Zig: the language is relatively simple and feels like C.
The C interop is a huge win as someone who wants to do more posix/wayland projects.
> So a nicer-written C that inherits all the issues from C?
And inherits all of the benefits of C. C is the foundation of the computing world. "C, with 50 years of improvements from lessons learned" is, by itself, a tremendous value add to a programming ecosystem that has largely abandoned attempts to write a truly performant language in favor of handicapping programmers with fully automated safety.
Rust is a low-level language for people who don't write low-level code. Zig is for those who do.
The main advantage of Zig for me and many other programmers, is the simple mental model you need to keep while reading the code. All control flow is expressed by keywords, never symbols. That along makes code much more readable. There is nothing like Rust macros, things can't do "magic. Zig comptime are regular functions, so you can clearly see and follow the trail. There are no exceptions, no recoverable panics, all code will run linearly as you see it written. Error handling in Zig is close to perfect, in my view. Obviously, Zig is not a language to use for high-level jobs. It makes no sense to write CRUD web app in Zig, you will be fighting the resource management for no good reason. For me personally, I feel that I'm too stupid to use Rust effectively. I started moving away from C++ purely because I don't want to deal with exceptions. And Rust is just a more complex C++ with memory safety.
One of the things I liked about Zig is that it made choosing which memory strategy (malloc, stack allocated, Arena, Bounded allocation, combo) you needed for a particular code second straightforwarded and well supported.
Microbenchmarks are cool, but better algorithms are where you get faster code.
C++ solved the memory issue at compile time too. I don't understand why even use Rust when modern C++ can do everything you can do in Rust but better and more ergonomic. New languages to solve old problems.
The V pitch was a bunch of features that seemed implausible like automatic C++ translation and near-GC ergonomics with near-manual-memory behavior, without Rust-style explicit lifetime annotations or pervasive reference counting. so basically the pitch was the holy grail of systems, which caused drama as the project was met with extreme skepticism.
That was 7 years ago and since then the project has not delivered the grail, instead settling to be more of a mashup between go and C.
Bend is completely different it was just announced last week. It doesn’t have any users and has a weird ai integration so I done see how it’s relevant as a contender in the systems space at all.
Never heard of them. V looks neat in some ways, but there's still no 1.0, installation tells you to compile the compiler form source and it still uses and depends on a C compiler behind it. And defaults to GC. That's a ton of reasons why various people would drop it on sight. Also, inertia, of course.
Zig suffers from much of those, as does Nim. Rust OTOH appears well integrated and stable and I'm even seeing a lot of corporate use. I'd love to see V get to that level as well. But seems it's not even here in the present, let alone the future.
while (i < cursors.len) {
if (actual_index < arr.items.len) { cursors[i] = .{...}; i += 1; }
else iteration.remove(i);
}
I think you have to be a special kind of person to call Rust "more readable."
The thing about Rust is that if you can appreciate zero-cost abstractions on iterators then the language feels like the only right way to program. But many programmers either don't use this sort of programming at all, or don't care if it has a cost in languages like JS, Python, Java, etc.
Personally I like Zig a lot because it feels like you're doing low-level programming but without having to program C which is... well, https://xkcd.com/918/
I'm not exactly sure what you mean, that seems like a pretty phrase to me, is it considered an LLMism now? Kinda feels like the same thing with the em-dashes; I can't use em anymore because people will then assume I'm an LLM, even though just a few years ago it was a perfectly normal thing to do.
It's entirely possible I'm just getting worse and worse at picking out LLM writing these days too, who knows.
It’s a very common tic of recent Claude models specifically. Don’t worry - for better or worse, the labs are trying to train away AI writing smells (for example, Anthropic talked about Fable 5.1 having more natural writing), so this will probably become outdated as an LLM smell relatively soon, just as em dashes already have. And then people will forget about it.
Given that the author mentions both using helix and zig encouraging larger files with more content I’d be curious to know how they navigate these files in helix. The thing that keeps me from using it is lack of code folding, which I notice I use a lot when navigating larger files to zoom out.
items.iter().enumerate().filter(|(_, i)| cond(i)).map(|(idx, i)| Pointer::idx(i, path, idx)).collect()
while (i < cursors.len) {
if (actual_index < arr.items.len) { cursors[i] = .{...}; i += 1; }
else iteration.remove(i);
}
I’ve been slowly learning Rust, and this style is my main gripe against it, because I feel like I’m being gaslit. Its proponents praise its readability and ease of use, and just… no. It looks deranged. A simple loop is immediately obvious to anyone who’s programmed in any language. Even Python’s list comprehensions are loop-ish.
.len doesn't work on a list (because it might mislead you about the efficiency of the operation if it did exist), so it's better to use iterators - so then you can change the underlying type without changing your code.
But then, saying while "let Some(item) = iter.next()" everytime is tedious, so they give you .iter() - for any type that is efficiently iterable.
Nothing stopping you using a manual loop that you need to update if you change the container type.
Zig had a breaking change half year ago and since then ZLS only works for std and local code, not for any deps. Zig is nice language but dev experience is a real nightmare at the moment. I say that as maintainer of three non-trivial libraries, and I have one Zig project in production.
That said, I've been re-evaluating my choices lately.
Learned today why there is no "zig metadata" command.
A dependency can be marked lazy, so whether you even need it depends on the build flags you pass.
So ZLS has to actually run your "build.zig" to see the modules
That is why it breaks when Zig changes the build system.
I've been also very skeptical about Rust in the past, and all my points still hold (traits vs. accidental complexity, no introspection, barely usable macros, borrow checker awkward to use with arenas, orphan rule) but what's changed is that I won't be writing any of that myself, and LLMs are good in generating all sorts of glue and they are incredibly good at figuring out what's wrong, they just never get tired. so lots of Zig benefits are simply not relevant IMO.
one extra thing - Zig doesn't have interfaces, we often use comptime duck-typing and it works great, until it doesn't. it's not common but when you hit such case, it really sucks. I've realized that during my `serde` rabbit-hole and I had to revert the whole feature and scratch it. it was not obvious to me when I started but it's now clear to me that Rust really is higher-level PL than Zig. another example is Zig IO, I think it's screaming for interfaces, yet Zig is trying to pull all sorts of awkward hacks just to avoid it. it's sort of like when Go was avoiding generics but we all knew it's inevitable.
my current theory about Rust is that it might be bearable for my sorts of projects, given specific code guidelines. sort of like people writing C-style C++.
But with static analysis it's still possible to miss some leaks or to have false-positives. That's why an integrated language mechanism preventing such leaks is much better.
Yes, so you pick "false positives" instead of "missing some leaks" and you build a way to mark code as "unsafe".
This is not fucking rocket science
> That's why an integrated language mechanism preventing such leaks is much better.
No categorical difference, except one is opt-in. You can even design your static analyzer so it analyses the code of dependencies that haven't opted in.
Making things opt-in means that it will happen less often, making them opt-out that they will happen more often. In this case, on the one hand you have destructors that don't run when they should, and on the other you have destructors running at a more granular level than you'd want sometimes. I know which human failure mode I prefer.
They don't play nicely with arena allocators. And arenas is what you reach for if you have clear lifetime bounds: e.g. a single request with arena never de-allocates individual objects, nukes arena when done. That gives you an easy verifiable protection against leaks, data (and cache) locality and deallocation that cost zero cpu cycles.
Have a boxed object have implemented drop, then when the box leaves some scope the Box will clean up it's stuff (drop implementation if there is any) and deallocate it's memory using the allocator (which the arena will treat as noop).
It's technically possible to perform arena-based allocation and still have compiler checks. The compiler just need to track objects allocated with an allocator and prevent destructing the allocator itself as long as there is at least one object using it.
It's like view span objects in rust. The compiler knowns that a span is logically connected to the parent object and don't allow destroying it when such span exists.
242 comments
[ 0.21 ms ] story [ 79.6 ms ] threadAs for me, I’d like to keep contributing to the ecosystem, and I will, whenever I come across a project worth building.”
Idk I don’t write either well enough to have a hand in this but losing out all of this for more Imperative stuff seems like a step back.
“ No functional paradigm
Rust is technically an imperative language, but it draws heavily on functional concepts: zero-cost iterators, lazy evaluation, ADTs, pattern matching, monadic types, traits, closures, and so on. Having also spent time with Haskell and Erlang, I’ve become fairly inclined toward the functional style, and it shows in this library. It leans heavily on FP idioms:
Monadic error control via combinators like Queryable and related types Monadic-style data types like Data<T> with map, flat_map, reduce, and friends Pure, immutable transformations Combinators over iterators instead of loops Closures for local abstraction Declarative macros as a small embedded DSL Sum types and product types”
I get why, and it makes it a better fit for its obvious “C++ reimagined, cleaner, and better” niche.
I'm glad NVIDIA found a way to make GPUs programmable and got us out of the shaders tarpit, but did it have to be C++...
FWIW I've actually worked for 6 months on a large old Delphi project. It was some performance work that, as almost always, mainly required getting the language crap out of the way. In the end I got the job done (100x-1000x speedup) but I wouldn't want to switch back to this ecosystem: Licensing costs, weird language warts there too. A slow moving ecosystem. Ultimately, I just need something that does what I tell it to do, reliably and fast, and that doesn't get in the way.
Even hardware (GPUs) that functional language could trivially exploit, it’s still higher performance to write low level code and manages all the memory imperatively
Why can't people just write shorter bullet pointed lists of things and post that? Just post what you put into the AI!
61% generated by AI 12% assisted by AI
I would have completely expected that.
All of the Handmade "C successor" languages seem to have this obsession, including not only Zig but Odin, C3 and Jai.
For some toy problems you can do clever allocator tricks and get a huge perf win. For example Jai and Odin both seem to really want you to write code which can throw away a "per-frame" arena periodically so they're not paying to track allocations in the arena because they're all thrown away at the same time.
But a lot of real world software just isn't that simple. This doesn't make such features worthless, it just means they're one of a thousand tools the experienced developer could want in their toolkit, not really deserving headline status.
Allocators have been in wide use long before the 2020s but I agree there does seem to be a resurgent interest lately. Although I would argue it's part of a more broad trend of focusing data driven design. Which makes sense because accessing main memory is one of the slowest things your program can do.
They do, but not necessarily together with generic, standard containers.
When you find yourself wanting a nonstandard allocator, you usually want it because you want it to have some interesting property. It's not necessarily trivial to fit that into the interface of something like `std::vector`, or `std::unordered_map`, etc.
Here's my take as a game developer: 99% of use cases for custom allocators are scratch allocators for doing stuff within a frame. 95% of those are much easier to serve by just amortizing allocations by storing things in an `std::vector` (or equivalent) that gets cleared every frame. You can use linear storage to back many interesting data structures, including queues, ring buffers, priority queues, binary heaps, etc., and that's more than enough for a large number of systems in a game.
The overwhelming majority of the time, more complex data structures (like hash maps etc.) have a longer lifetime than the current frame, because the whole point of using them in the first place is to amortize lookup time across frames.
this goes back decades, its not just a feature of the 2020s. as a systems programmer I always want this, and the idea that allocator state should be completely hidden and implicit is shortsighted.
but yes, it is a bit onerous to pass around allocator(s). the real complaint that I have is that if 'malloc' is global and a compiler primitive, then we can do things like coalesce allocations and have compiler managed lifetimes when appropriate.
explicit allocators are an important lever, its not clear to me that we could never find a way to make them possible without the minor downsides.
Zig doesn't force or even tends to prefer one way or another. If I want unassuming heap allocations that can be reclaimed in any order, there's an allocator for that. If I want an arena to discard at the end of something like a request or a video frame, there's an allocator for that. If I want to use a fixed backing buffer for the allocations, there's an allocator for that.
The point is that the standard library doesn't assume one or the other, which seems good if the problem is that "real world software just isn't that simple" in the more general sense that there's no one-size-fits-all allocation strategy.
I also disagree that this is "the focus" of Zig. Technically speaking it seems like a rather minor design choice for the standard library.
The automatic solutions are usually pretty good and usually the right place to start. But if performance is a priority, you want options.
BTW, “per-frame arena” is part of a general pattern of a repeated interval of work doing significant allocation. This is really common in software of all kinds… servers that process requests (like web servers and database servers) and typical command line tools.
I haven’t tried to replicate this for myself. And, even assuming for the sake of argument that it was definitely true back then, a lot can happen in 20 years. But still, it does speak to wanting options when performance really is critical.
You can generalize this far beyond per-frame semantics. Think per-http-request, per-pubsub-message.
Per each, you can create a new virtual.Arena, set it as your context.temp_allocator and use it for the entirety of the request or message. Afterwards, throw it away.
Our HTTP server may have dozens, or hundreds, or thousands of simultaneous requests. But we've decided to give each a specific arena with a size chosen in advance so that we can use arenas. This means that the request to check a CSS file hasn't changed (it has not) and the request to modify the holiday of an employee whose manager is changing mid-way through their annual leave are both given, say, a 10MB Arena. No problem for that CSS check, some text parsing, one OS call, an ALU operation, done. But alas the SQL Transaction for that holiday change used up so much memory now the text parsing code "temporary" allocation in code to queue email for both managers fails and it unwinds the whole HTTP request. 500 error, rewrite your Odin program.
Arenas make lots of sense for video games, but their applicability for these other applications is much more dubious.
It's a fairly common pattern in non-video contexts to preallocate arenas of different sizes for different workload pools (e.g. different routes for a server).
It's also fairly common to allocate per-work-item (e.g. request/session/batch job) arenas for "general-purpose" scratch allocations that are small (your header parsing, auth context retrieval, etc.) and then default to a global manual/refcounting allocator for one-off large data actions like your holiday change. Since most business applications are returning summary/small aggregates over the large data action (in this example, something like "holiday updated successfully", not the entire history of the PTO table or the database connection's internal state), the copy cost of moving data between the global dynamic allocator and the arena for result transmission tends to be small.
That's not a terrible approach in some situations. If the large majority of code only needs the scratch arena and/or some per-handle allocators stored on e.g. the database connection pool, it can work well. But if, over time, the amount of bookkeeping required to maintain data tagging/movement between arenas/allocators becomes severe, it's worth stopping, stepping back, and considering that you've kind of walked backwards into inventing a shitty generational GC.
Sometimes using a global one isn't the best thing (it's often a good idea to specialise base on allocation size, reuse and lifetimes), but I've used them quite a bit in C++ over the past 16 years doing HPC for graphics, rendering and simulation, so calling them only useful for "toy problems" likely shows you just haven't found a need for them in what you've been doing.
Given that both of these are intended for game development, wouldn't that make the most sense?
But I think C3 shows it can be taken further.
Now it all feels so pointless though. Like memorizing rules to do mental arithmetic. Sure, there is still use for language expertise, but not enough to get excited over new concepts and ideas.
I prefer to prototype in Golang since it compiles fast and makes for quick iteration, but at the end I ask the LLM to port the Golang to Rust.
Nice Rust enums for APIs are the chef's kiss.
I absolutely will not write anything in Python or scripting languages anymore. They're too brittle and don't have great devex or deployment stories. Especially when you can just as easily build in a typesafe language with good error handling that compiles down to a single static binary.
I’d push this more towards personal preference of how code is expressed matters much less now than how maintainable it is.
There is the aspect of long type names, they often don’t have much impact when tokenized. The character count of words is nearly negligible - they often become one or two tokens anyways. But, the choice of words may have a greater impact on how the word choice weights an LLMs contextual processing of that word (a human may be able to ignore an inaccuracy in naming a bit more flexibly than some LLMs).
For years I used Rust as my hobby-programming language and loved it greatly. I never managed to land a job working with it full-time, because either the work was too niche or it didn't pay enough, or I was simply too comfortable where I was to change. And now that I finally have enough discretion over my technology choices to run a "proper" project using whatever tools and languages I want, it is not me but the AI that writes all of the code.
There's a part of me that feels a quite sad about all this. It's almost as if there actually all along existed a real final deadline on finding that "dream job". And I missed it. And while I expect this one miss to be just a small piece in the grand picture of things that we're going to lose or have already lost to the zeitgeist of agentic SWE, it feels big to me. It was my professional dream, while I still had professional dreams.
Programming and learning new things can still be fun in the era of agentic coding.
With LLMs, I get to quicky ask: what would this look like? Why do it that way? If you suspect that the LLM isn't doing it the right way, you can still investigate that yourself.
e.g. the other day, https://rhombus-lang.org/ was mentioned on HN. With LLMs, the cost for trying this out is practically much lower.
In 5-10 years, the people who have paid attention to these will be needed to bail us out of the mess that the rest of the slop-addled monke brains have created.
Jokes aside, have you watched 2001 - a space Odyssey?
First I thought you were going to comment about the grating LLM-isms in the article, which made me end up not enjoying reading it.
The main thing to change this feeling for me is the large number of articles on programming languages written by an LLM.
fn run_query(alloc) {
eg https://www.dgtlgrove.com/p/untangling-lifetimes-the-arena-a...
that being said, it's even easier to not manage any lifetimes at all :)
although i suppose some will say that you still manage lifetimes in rust, you just have full support from the compiler to make sure you do it right. that seems better to me than relying on simplification to ensure you don't make mistakes.
For example, bump style allocators allocate very quickly, but at the cost of higher memory usage and therefore sometimes worse cache locality.
The only way to know is to actually measure.
> I think it should be preferred wherever it's an option for that reason most of all.
And that’s far too broad in my opinion.
Sure, if you’re writing in a language that makes memory management difficult, like C or Zig, that might be worth it, but arena allocators apply to many other languages too.
> I respectfully disagree. The technique has its place, and I use it once in a while, but whether it makes a positive difference for performance is highly sensitive to a number of factors.
My argument deliberately and explicitly ignores the difference in performance characteristics. Your concerns about performance are irrelevant to my argument.
Now, I could respond to your new, entirely different argument, but I don't see the point of that if you ignore my response to the arguments you've already made.
Yes, which you didn't respond to until now.
> And that’s far too broad in my opinion.
Similarly, it's far too broad to say that it's "much less error prone" to use a more general allocation strategy. Clearly we both make unspoken assumptions, but in the context of this discussion on Zig, where you initially responded to an example written in Zig, I think you actually understood what I meant.
It makes it much easier to avoid lifetime mistakes in my experience.
Using arena allocation also makes me think more about how much memory I am using and how much memory I should be using etc.
It is hard to benchmark it against just using a global allocator because it is a structural change to the whole codebase.
It becomes clear once you do it a couple of times
1. Tooling (as an extension to the mentioned IDE support point). Zig and Rust are both praised for their tooling and I think rightfully so. The C/C++ interop story and the cross-compiling story in Zig are great. From the standpoint of a working practitioner though I think Rust is way ahead. Not surprising given that Zig is much younger, but something to keep in mind.
2. Compile Time Stuff: Here Zig is praised and Rust not so much. I think this is undeserved. Rust has much higher aspirations for their compile time features, namely that outcome must be identical regardless when the code runs. This is a very useful property but makes the task much harder and fundamentally incomparable with Zig comptime.
https://gist.github.com/corporatepiyush/5382d79192be9737cf3b...
The real problem is that people are using C for the wrong reasons. C is for the development of operating systems and low-level code, not applications.
Zig is more modern but anything that does even one iota more of hand-holding or has anything that looks like a guardrail fails the test.
If you want to write applications use Pascal, Java, C# or Rust.
We're trying to solve a problem that shouldn't be solved. We're continuing and even confirming the usage of systems programming languages for application development.
Postgres and Nginx make sense in system programming languages; they’re extremely performance sensitive and that granular level of control offers them features. Interpreters are sort of the same, they interact with the OS a ton, it makes sense to work in the same language as the OS.
I do generally agree for the app tier of a web app. I wouldn’t build a CMS in Rust, but I also wouldn’t build a reverse proxy in Python.
EVERYTHING ELSE is invalid usage no matter how performance critical people claim their application is. These are just excuses for people to use a grossly unsafe language to get that last 2% of performance whilst costing the world trillions in lost productivity and security breaches.
C and C++ are a liability for many reasons since memory corruption is notoriously hard to detect, localize and fix. And multi-threading is even harder, almost a dark science to get right.
The big one for me is that Rust packages tend to pull in dozens or hundreds of transitive dependencies - C/C++ code tends to be monolithic, so it has fewer maintainers who could become compromised.
i do agree that applications are probably best built in fast garbage collected language. but it also seems like go, java, C#, etc have other downsides that push people towards things like zig or rust even for apps. it also depends on what you mean by "apps". is a server an app? what about an actual native cross-platform desktop application? does go, c#, or java have a good paradigm for that? what if you want to use an oss language not tied to a big tech company? you start running out of suitable languages pretty dang fast.
A lot of the “high level assembly” parts of C are actually compiler extensions and not from the C spec.
libc is even worse.
And most C library calls are actually Unix system calls. Malloc() is an operating system call and not a library call. Yes, they made it a library call later on to facilitate usage in other operating systems but it was originally an integral part of the Unix OS.
https://github.com/Syzygies/Compare
So you're in a restaurant where you don't speak the language, you can't read the menu, but you see three price points for set meals featuring the house specialty. (Say, "Crossing the Bridge" noodles in Yunnan.) Which do you choose? My tour guide, the author Fuchsia Dunlop, later agreed with me this is obvious: The middle choice.
So you're choosing between Go and C23 as candidate successors to K&R C. They both have "royal blood". One got the name. Knowing nothing more, which do you choose?
The answer is equally obvious. The one that got the name also got the warts.
Minimal rewrite due to the breaking changes introduced in C23 versus K&R C, while the others are a complete rewrite.
Even if the syntax is a bit of a kludge there are now ways to indicate bounds on function arguments.
For example, write in whatever language you know best, then translate to a more appropriate language using LLMs once the desired behavior can be checked automatically. It is a tactic that works in some cases.
I've been messing around with a language that I summarize as:
ALOE = Scheme + Smalltalk + Types
https://github.com/dharmatech/2026-09-02-aloe-racket
One of the examples is a very basic computer algebra simplifier:
https://github.com/dharmatech/2026-09-02-aloe-racket/blob/ma...
That simplifier is based on a computer algebra library in Scheme:
https://github.com/dharmatech/mpl
You might want to take a look at Odin as well.
If you’re not stupid it fails because you can find out and then make an informed decision. An informed decision will dominate the suggested strategy in terms of chances of not screwing up.
This really should be obvious.
(In the analogy, your phone has an automatic translator and you use it).
Note that I have made no argument for one language over another.
No.
Find out more.
You could still be wrong but it’s less likely than if you apply hokey heuristics on wilful ignorance hoping it somehow works out sight unseen to knowable problems that can be understood and analysed.
The code in that section is clear in its purpose and broad outline: "Give me a function and data; if the data is bare apply the function to it; if the data is a container apply the function to each item inside it."
You can absolutely do that with immutable data structures in Zig. You just have to pass an allocator to the function (i.e. instead of calling data.flat_map(f), you call data.flat_map(a, f) where a is your allocator).
That the Zig version of the code does mutation is a matter of programmer choice, not something imposed by the language.
(Also, what do monads have to do with it?)
> But the language quickly forces you to diverge from the functional style, mostly because you’re now dealing with allocators directly, and a genuinely pure functional approach means constantly constructing new structures. That’s either expensive in memory or expensive in the manual bookkeeping needed to avoid it.
So I don’t think he’s trying to say that it’s literally impossible. It felt more like the result of a good faith attempt to understand how Zig itself actually wants to be used, and to compare that to how he’s used to using Rust.
I actually liked that he did it. So many other comparisons want to evaluate one language against the other language’s values. But I don’t want to know how well Zig can do Rust; I want to know how well Zig accomplishes its own goals, and what those goals are.
In Zig you end up spending a lot of time writing How to do a thing, where in Rust you only wrote What the thing is and the machine did it. There are edge cases where Zig's explicitness wins for the best programmers, but there just aren't enough of those cases or those programmers for this to net out IMNSHO.
I programmed in rust a bit and can't say I'm an expert, but in my view rust mostly-solved the memory management problem at compile time and without a GC, and it works very well. The biggest con and cost I've always seen repeated so far is that "it's slow to compile", and I get that, if you're past 250 crates the final --release link tends to become noticeable, but there were improvements to incremental compilation.
On the other hand - looking at the syntax from this post - Zig feels a blend of javascript, python and golang syntax that still requires memory management. So a nicer-written C that inherits all the issues from C? From the post: no functional programming, data mutation, memory leak, double-free, memory corruption.
Personally I'd rather trade a couple minutes of final link every time when this is the other option.
Yes, on paper zig has more features than C, but what zig has is explicitness. C has a big murky space of implicitness. For example, there might be 5 different zig features which can be used at various times you might use a void* in C, buy the conceptual space of void* fully contains(and then some) the spaces of those zig features.
Note that AT&T, where UNIX and C were born, the language they were researching as C replacement was Cyclone, not something that is not much different.
Seriously impressive that you’re going to repeat that until the end of time
Because it is a rip off, pretending nothing else has done it before, in fact it could even be called a PL/I ripoff for that matter, which predated C for about a decade, had already had all the bounds checking, proper strings and arrays, was actually used to deliver several IBM OSes, and is used to this day on z/OS. Certainly much larger production deployments than Zig.
Yes I will, as long as I am part of this realm, can be in HN, or whatever is the social media of the day, I started with BBS, USENET, Slashdot and plenty others.
Kelley first created Zig when hr was working on a digital audio workstation and found most existing languages to be awkward for working with particularly hard real time requirements, but still wanted something more modern than C. Im speculating here, but I believe its advantage over Rust for that specific application is that you have tighter control over exactly when memory is allocated and deallocated, and how data is laid out in it. Rust wants to tie allocation lifetimes to scope in a very fine grained way that I would guess is beneficial the vast majority of the time, but does still make it harder to reason about when you’re about to stall out the CPU while the allocator does its thing.
Zig and Rust are equivalently "low level". Zig isn't any closer to the hardware than Rust is.
> but I believe its advantage over Rust for that specific application is that you have tighter control over exactly when memory is allocated and deallocated, and how data is laid out in it.
Rust gives you all this, too.
Zig's primary (possibly only) advantage over Rust is that it has much faster compilation times.
I don’t think it’s an accident that it has succeeded as multicore took over. We are now well past a point that a task that can be split with 70% efficiency into multiple parallel tasks is 5-10x faster than the optimal sequential solution. Expensive multicore machines existed when Rust was a baby but it didn’t really catch on until 4 core was common in consumer hardware. And now I have an ancient laptop with 16 cores.
But Rust is also good for producing correct code to run on those systems. So it benefits twice.
The industry where it seems strongest positioned is embedded. Will it actually break into that domain? No idea.
It's the lingua franca of software development.
Zig is appealing to the "I'd like to work in a C-like system programming language but I'm not a C veteran" crowd.
Not a small crowd by any means, but will always be a niche.
I haven't actually tried to find out if that Zig promise is real, though. Just some trivial little things. No day job excuse and no personal projects where it would fit.
As a long-time low-level programmer, and as someone working on a popular mainstream language, I find Zig fascinating, and I also think it addresses a long-standing problem in low-level programming. I'll get to the problem later, but the fascinating part is its use of partial evaluation (comptime) as a single coherent mechanism that replaces a myriad of other partial-evaluation mechanisms (macros, templates/generics, constexprs). That one mechanism is the core of the language, like macros are in lisps, and that design - whether you like it or not - is revolutionary. It's never been done before (other languages have partial evaluation mechanisms that are almost as general, but they're offered in addition to, not as a replacement of, other features).
> rust mostly-solved the memory management problem at compile time and without a GC
"Mostly" does a lot of work here because 1., if you look at the implementation of very efficient, possibly specialised data structures - the very thing you reach for a low-level language for - they typically require unsafe, and 2., it still suffers from the problem C++ has had for decades, which is that over time, as program changes and evolves over years, things tend to drift toward the more general mechanisms that rely on malloc/free on an individual objects, and the program gets slower and slower (huge runtimes like TCMalloc help, but not enough, because they can't move pointers). To truly compete with the performance of moving GCs, which allocate through bumping a pointer and deallocate objects in bulk, low-level languages need to rely on arenas (which work similarly to moving GCs), and Zig is the first language that makes arenas almost user-friendly and hopefully sufficiently composable to withstand program evolution.
There's a formal proof asserting that if you keep up the safety invariants within an unsafe region then that will not infect other code, even in the presence of arbitrary other correctly-written unsafe blocks.
This means you can build abstractions on top of these low-level primitives to keep it contained, so consumer code never has to even think about or know there's unsafe blocks in it. The type system lets you build very powerful abstractions so these go a long way.
There's a lot of woo-woo scare quoting around how much you actually have to use unsafe code in Rust. It's fairly uncommon to actually have to reach for them in practice. Most of my usage ends up being things like converting a &[u8] to a &str when I know it's already valid UTF-8 so I want to skip the linear-time validity check. Very rarely do I have to build data structures with complicated pointer juggling, because there's often a library that already does what I need!
> which is that over time, as program changes and evolves over years, things tend to drift toward the more general mechanisms that rely on malloc/free on an individual objects, and the program gets slower and slower
What are you talking about? I've never encountered this and I've been using Rust for 10 years.
> What are you talking about? I've never encountered this and I've been using Rust for 10 years.
Okay, but I've been doing low-level programming for 25 years, and have encountered this over and over in large programs (over 500KLOC) as they evolve.
It's just that sometimes some of the abstractions you need to build go outside what the ownership and borrowing system can model. And when you don't need to do that (which is 99% of the time) you also get all the benefits of the ownership/borrow system for free.
Suppose you're writing a program that's mostly high-level, say some kind of concurrent server, and it's large-ish, say around 1MLOC (most C++ programs I've worked on were significantly larger). Because the language is also a low-level language, it has low-level constraints, so:
1. It needs to use an AOT compiler, and consequently to get good performance you need to use less general mechanisms, such as direct (as opposed to dynamic) dispatch and even manual monorphisation (with generics/templates). These are viral, so they have to be carefully chosen (you can't monomorphise everything or you'll get machine code explosion). Five years later you need to make a big change that requires more generality, and then you either have to reconsider all of your manual optimisations, which is expensive, or go for more general constructs (dynamic dispatch) and the program gets slower.
2. It needs to use machine pointers (i.e. you can't enjoy a moving GC), and so you try to use the stack as much as possible (which you can't really do for anything dynamic), or suffer the high cost of malloc/free on individual objects. As the program evolves, you need to make things more general, and objects that could live on the stack now need to go on the heap, and objects that lived on the heap now may need to be shared among threads, in which case you often add the additional cost of refcounting GC. Of course, you want to use arenas in many cases, but they're very, very hard to use in C++ and Rust.
You'd be better off - performance-wise and maintenance-wise - with a good optimising JIT and a moving GC. This was exactly a problem with many C++ programs that didn't really need a lot of direct hardware interaction - everything worked great for a few years, and then the evolution and maintenance costs became really high (or the programs became slow).
Now suppose you're writing something low-level, i.e. you really need to interact with the hardware and/or OS directly a lot, and want to control everything - where everything is in memory, exactly when it's initialised, exactly when it's freed, exactly which operations are executed and when. But now you have a language that's also high-level, so it has a lot of implicitness that hides from you the things you want to see (and in Rust's case, you lose the safety). Best case scenario, you rely on disciplne and avoid implicit features, but then you also need to avoid much of the standard library.
Anyway, combining high and low level in the same language was C++'s dream: one language for everything. Of course, for a while we didn't know about the maintenance problems, as those appear only years down the line, but more importantly, there weren't really high-performance high-level languages back then. These days, with lessons learnt and with more options, I prefer a language that focuses on being high-level for high-level stuff, and a language that focuses on low-level for low-level stuff. If you really need both kinds, use two languages.
You haven't been doing any Rust though. You seem to think you can extrapolate your C++ experience to Rust. That's preposterous. The actual Rust programmers can't recognize this theoretical problem in their Rust programs.
And if you look at other comments in this thread, many engineers have this mentality of "just use a crate, it's probably optimised already". They might not have performance problems immediately or obviously but it's more like ten thousand papercuts - a few allocations here and there, a few extra copies here and there and you've got a way slower program than it should have been.
Most actual Rust programmers haven't maintained a large Rust program for a long time. Now, don't get me wrong - there are many C++ programmers who are fine with it, but many who aren't. What I find annoying is people without much experience in Rust assuming that everyone or almost everyone should like it, even though that's never been true for any language.
I think the idea is that a small program can organize its allocations and data structures to minimize number of calls to malloc, e.g. with preallocated workspace structs, or slab allocation, and similar approaches. But as a program gets bigger, there's a pressure to have looser coupling, to have subsystems with simple convenient APIs which leads to them doing on-demand malloc calls internally, rather than having consumers pre-allocate their needed workspace. Because that kind of workspace management results in more complex APIs and more burden on the consumer.
That said, I don't really believe it either, at least for the kind of codebase where it would matter (scientific computing, in-memory DB server, etc). A codebase that places an emphasis on minimizing heap operations in hot codepaths can do so by consistently using workspaces and allocation-avoiding APIs. I don't think it's so difficult really, but it does take a conscious design decision to do so. But writing something like a web browser in this way could be annoying due to most data having wildly variable sizes, and zig's arena concept would be very handy -- but rust has crates like bumpalo for that purpose.
My personal mantra: "Think in FORTRAN, code in Rust/Julia/C++". But I'm mostly working on HPC-style code where I don't have to do with wildly varying input or output sizes.
Except that's not composable, the exact same issue we have in C++.
> But I'm mostly working on HPC-style code where I don't have to do with wildly varying input or output sizes.
There you have it. The problems arise more quickly in concurrent rather than parallel code, and when there are lots of features added over the years that touch the hot paths.
> in-memory DB server
Actually, here there can be big problems (as it's also about concurrency rather than parallelism). Last week a colleague of mine looked at Moka and saw that it could only offer half the throughput as Java's Caffeine at the same latency. When he looked into it, he saw that over 40% of the program's CPU was spent on the epoch-based reclamation.
Most crates for containers will be written such that the container types take an optional allocator type parameter that defaults to the global allocator. You can set it and it transparently uses the other allocator.
To improve the ergonomics, you'd define local aliases that use that allocator.
but custom allocators are rarely used in rust because they're simply not needed the vast majority of the time. if your language is not memory safe and you need to manage memory yourself, they're more important. but this isn't the case with rust.
c and zig folks are obsessed with arena allocators particularly because they can group lifetimes of individual objects, reducing the amount of malloc/free calls and thus the amount of use after free, double free, nullptr derefs, or leaks that can occur.
in rust this isn't a concern so custom allocators are only used for performance reasons.
but it turns out that in performance sensitive areas, you generally use custom data structures or those that already have their own allocation strategy baked in, like the generational_arena crate.
most of the time you are not calling thirdparty crates that allocate in performance-sensitive regions. either the crate is designed for this usecase and already uses a performant allocation strategy, or you're writing your own code here.
and in the rare case, you can trivially vendor the crate and pass your own allocator into it, or toggle the global allocator for callers.
but you also need to benchmark first before choosing an allocation strategy because it's not clear that a custom allocator will always guarantee better performance anyways.
and btw zig doesn't guarantee this anyways. you could pull in a dependency that instantiates their own allocator. at least in rust almost all crates use the global allocator as a default which lets you swap it out. if a zig dependency uses their own allocator the only recourse is to fork it.
rust doesn't have a performance problem, so any claims about it's custom allocator support leading to poor performance is unfounded. and thus so are claims about the superiority of zig's approach to allocators.
You say I outright lie for not mentioning something that doesn't exist??? I guess you're saying you could create such a mechanism (or that some libraries have such ad-hoc ones), but that's not the point.
> there is nothing stopping you from using custom allocators with your own code or with calls to thirdparty dependencies
I didn't say there's anything in the language stopping C++ and Rust from having such a standard library and ecosystem of libraries. They just don't have that yet.
> if your language is not memory safe and you need to manage memory yourself, they're more important. but this isn't the case with rust. c and zig folks are obsessed with arena allocators particularly because they can group lifetimes of individual objects, reducing the amount of malloc/free calls and thus the amount of use after free, double free, nullptr derefs, or leaks that can occur. n rust this isn't a concern so custom allocators are only used for performance reasons.
This is simply untrue. I won't call it an outright lie because you clearly don't have much experience.
First, I'm trying to point the problems we've had in C++, most of which only became apparent when evolving large codebases over time. People who have not had experience evolving large C++ or Rust codebases over years simply don't know about these problems and certainly can't claim they don't exist. Writing smaller programs in C++ or even large but young programs has always been a pleasure. The language is expressive and productive. Some of the biggest issues only arise years later, when the program gets either expensive to maintain or slow.
Second, experienced C and C++ folks cannot be "obsessed" with arenas for the reasons you mentioned because until maybe 20 or even 15 years ago memory safety wasn't a widespread obsession. It was a correctness issue like all others, and its exceptional prevalence as the cause of security vulnerability wasn't known (or wasn't widely known).
Lastly, you don't pick Rust for safety. Most software in the world today is already written in languages that are as safe as Rust if not more so. These days, you pick C, or C++, or Rust, or Zig when you want to do something that's largely low-level. Things that are low-level often also need to be fast, or at least reasonably fast, and large low-level codebases that evolve over years tend to suffer serious performance issues because of memory management (this is why companies with actual experience with long-maintained large low-level codebases make huge runtimes like TCMalloc to help them, which you also may not have needed yet), and arenas are the primary way to get memory performance similar to what you see with modern moving GCs (and even somewhat better).
Now, you can say that C++ only started moving in that direction with pmr in C++ 17, and that's true. But the need was recognised as early as 2005, traditionally C++ codebases didn't rely on many libraries so interoperability has typically not been a large concern, and the number of large C++ programs that would benefit from such a thing declined over the years because of the low-level maintenance issues I mentioned and the growing availability of super-fast high-level languages.
In general "unsafe" does not compose.
"if you keep up the safety invariants within an unsafe region"
This condition is doing a lot of heavy lifting.
I'm curious why you think that statement is doing heavy lifting. It's much easier to write and verify that a few lines of code are correct than it is to write and verify that an entire program is correct. But that's the norm in C and Zig, and historically people haven't been very good at it. That's why we try to do as little as possible.
The reason it's not "the norm" is that (especially with spatial safety taken care of), not every line is equally dangerous at all. Still, there's no doubt that more guarantees help, but that is only when all other things are equal. If you pick a low-level language for mostly low-level things, so Rust doesn't offer safety for the trickiest code, and furthermore it makes certain things harder to see because the language is more complicated, then things become much less clear. Obviously, when the vast majority of the trickiest, most important code doesn't need to be low-level, Rust would probably be safer on the whole, but in such situations I see no reason to choose either Rust or Zig. You need to choose a low-level language if the core of what you're doing needs to be low-level.
Is there a word missing before "memory"? Seems odd to specifically call out spatial memory safety when memory safety subsumes it.
You must reason about the invariants in unsafe code on a global level. In particular, you could have unsafe code in crate A, whose data are then used by crate B. It could be fine. But then crate B changes its implementation which now violates the invariant expectations of crate A.
To address the problem that once integrity can be violated anywhere, only global analysis can prove that nothing bad happens, we've done two things:
1. We require the application to explicitly permit any integrity violation by a module; i.e. a library can't allow itself to violate integrity. This is a principle we call "Integrity by Default" (https://openjdk.org/jeps/8305968).
2. We try to minimise the need for potential integrity violations (this is very different from Rust, which requires unsafe even for things like benign write/write races, which are fairly common, and various basic data structures). Over the years we've offered safe replacements for things that used to require Unsafe. In other words, clearly demarcating unsafe code isn't enough if it's needed at all in many situations.
It isn't perfect, of course, as some libraries do require unsafe operations for direct interaction with native code or with memory, but their number has been greatly reduced, and they cannot do this without the application's explicit approval. Interestingly, this has annoyed library authors who want to do unsafe things but don't want to application authors to be alarmed because "we know what we're doing," and it's also annoyed some application authors who want to use such libraries and are forced to explicitly add permissions. But I think that the community, as a whole, has eventually accepted this because the harm done to those who don't care is small (they just need to add the permissions), to those who do care it helps a lot, and because fewer and fewer libraries require "integrity-busting" permissions, many applications need to do absolutely nothing and get important guarantees for free.
I know this paper [0] is quite old at this point, but the mention of benign data races reminded me of it. Would you happen to know how applicable it is to modern memory models?
[0]: https://www.usenix.org/legacy/event/hotpar11/tech/final_file...
Is this backwards? If B consumes data from A then to me that does not imply that A depends on anything from B; for a more concrete example that sentence reads to me like A is basically "throwing data over the wall" to B and whatever B does with said data is of no relevance to A. As a result, if B changes that shouldn't affect A.
Also for what it's worth I get the impression you and treyd might be talking about slightly different things when talking about whether unsafe code composes. I believe treyd is referring to the RustBelt series of papers [0, 1], for which the statement "unsafe code composes" means (at a high level) that adding a module with a memory-safe API to a memory-safe system will result in a memory-safe system as long as the implementation upholds the safe semantics. Yes, the last bit can be a rather significant caveat, as you said.
What you're talking about seems more along the lines of needing to look beyond the boundaries of unsafe blocks to prove that the unsafe block upholds its invariants, which is also true. I think you only need to check within whatever safe encapsulation boundary is relevant, though, rather than globally.
[0]: https://people.mpi-sws.org/~dreyer/papers/rustbelt/paper.pdf
[1]: https://plv.mpi-sws.org/rustbelt/rbrlx/paper.pdf
This is a specifically crafted bad idea, but you could have module A use unsafe to craft a Vec<u8> that is safe to use to read or write, but not to grow or shrink. You declare an invariant that the receiver shalt not grow or shrink the Vec.
If B only reads and write, you're good. But if a future B breaks the invariant, bad things happen. As I said, specifically a bad idea; there's a much better type to use if the thing can't grow or shrink...
No real world example, because I don't think we've run into memory safety issues with unsafe in the Rust code base I work in... but we only use unsafe where it's required (syscalls and other FFI).
We could use rust. But it wouldn’t give us anything.
So sad dlang never gets the credit it deserves. None of these ideas in zig are novel.
Odin and C3 has at least a magnitude fewer people knowing about them. More modest claims, less marketing potential.
So for D, it was to my memory never sold as a silver bullet either. A better C++ certainly, but with enough humility to not market itself as some game changer.
Compare Zig, V and Rust marketing with D’s and you’ll see what I mean.
Few ideas are new under the Sun. But the inventor and the popularizer are rarely the same. We should be happy that the ideas got more exposure instead of litigating novelty.
That IS cool. I loved partial evaluation the first time I heard of it - Futamura projections, here we come! And if it can be used to obsolete language features I never was happy with (macros), so much better!
But until now, I never heard of it as a selling point for Zig. I assumed Zig was just yet another "C replacement but we don't want to deal with Rust's borrow checker". I will certainly have to take a closer look on Zig now. But this should be up and front in their self-promotion!
Obviously, they can't call it "partial evaluation" because not many people know what that is.
I don't understand your C++ example about it getting slower and needing to "move pointers."
Well, it isn't that and it doesn't require that.
> I don't understand your C++ example about it getting slower and needing to "move pointers."
Because low-level languages need to use machine pointers, their dynamic heap allocations have a high CPU overhead; it's that overhead that moving GCs are designed to reduce, but because they move pointers, using them requires an FFI between these pointers and machine pointers. This is why in low level languages we try to avoid dynamic allocations when we can, but that increases long-term maintenance costs.
What do you mean? Are you referring to every function that allocates explicitly requesting an allocator by convention?
The C interop is a huge win as someone who wants to do more posix/wayland projects.
And inherits all of the benefits of C. C is the foundation of the computing world. "C, with 50 years of improvements from lessons learned" is, by itself, a tremendous value add to a programming ecosystem that has largely abandoned attempts to write a truly performant language in favor of handicapping programmers with fully automated safety.
Rust is a low-level language for people who don't write low-level code. Zig is for those who do.
Microbenchmarks are cool, but better algorithms are where you get faster code.
It's actually a nicer-C that fixes most of the issues from C.
That's the point.
Rust is philosophically different and goes well beyond the "just be a better C".
What's the pitch?
That was 7 years ago and since then the project has not delivered the grail, instead settling to be more of a mashup between go and C.
Bend is completely different it was just announced last week. It doesn’t have any users and has a weird ai integration so I done see how it’s relevant as a contender in the systems space at all.
Zig suffers from much of those, as does Nim. Rust OTOH appears well integrated and stable and I'm even seeing a lot of corporate use. I'd love to see V get to that level as well. But seems it's not even here in the present, let alone the future.
The thing about Rust is that if you can appreciate zero-cost abstractions on iterators then the language feels like the only right way to program. But many programmers either don't use this sort of programming at all, or don't care if it has a cost in languages like JS, Python, Java, etc.
Personally I like Zig a lot because it feels like you're doing low-level programming but without having to program C which is... well, https://xkcd.com/918/
https://news.ycombinator.com/item?id=49769812
It would be nice if people would note that their posts are AI generated, and put that in the title here to make it easier to ignore
It's entirely possible I'm just getting worse and worse at picking out LLM writing these days too, who knows.
Happy for you.
CLI tooling counter intuitively makes for very less friction especially when you are moving very fast.
But then, saying while "let Some(item) = iter.next()" everytime is tedious, so they give you .iter() - for any type that is efficiently iterable.
Nothing stopping you using a manual loop that you need to update if you change the container type.
Zig has a language server and "The majority of LSP features are supported" (https://github.com/zigtools/zls#features).
That said, I've been re-evaluating my choices lately.
So ZLS has to actually run your "build.zig" to see the modules
That is why it breaks when Zig changes the build system.
https://vectree.io/c/zigs-build-system-protocol-module-graph...
I see you have a blog - it would be interesting if you wrote up the results of your thought process, whichever way you decide in the end.
As a PL nerd, I really enjoy reading well-informed takes from people who have seriously used a language as to its strengths and weaknesses.
I've been also very skeptical about Rust in the past, and all my points still hold (traits vs. accidental complexity, no introspection, barely usable macros, borrow checker awkward to use with arenas, orphan rule) but what's changed is that I won't be writing any of that myself, and LLMs are good in generating all sorts of glue and they are incredibly good at figuring out what's wrong, they just never get tired. so lots of Zig benefits are simply not relevant IMO.
one extra thing - Zig doesn't have interfaces, we often use comptime duck-typing and it works great, until it doesn't. it's not common but when you hit such case, it really sucks. I've realized that during my `serde` rabbit-hole and I had to revert the whole feature and scratch it. it was not obvious to me when I started but it's now clear to me that Rust really is higher-level PL than Zig. another example is Zig IO, I think it's screaming for interfaces, yet Zig is trying to pull all sorts of awkward hacks just to avoid it. it's sort of like when Go was avoiding generics but we all knew it's inevitable.
my current theory about Rust is that it might be bearable for my sorts of projects, given specific code guidelines. sort of like people writing C-style C++.
That's why having no auto-destructors is a dead-end. This is the greatest mistake of such languages like C or Odin.
This is not fucking rocket science
> That's why an integrated language mechanism preventing such leaks is much better.
No categorical difference, except one is opt-in. You can even design your static analyzer so it analyses the code of dependencies that haven't opted in.
Anyways: its possible, I am building it as a very side project.
github.com/ityonemo/clr
Have a boxed object have implemented drop, then when the box leaves some scope the Box will clean up it's stuff (drop implementation if there is any) and deallocate it's memory using the allocator (which the arena will treat as noop).
It's like view span objects in rust. The compiler knowns that a span is logically connected to the parent object and don't allow destroying it when such span exists.