283 comments

[ 1.6 ms ] story [ 261 ms ] thread
Nice balanced writeup! However, I am not convinced one can make something solving all the things Rust solves which is substantially simpler as language.

Whether we look at Zig, Go or Julia they stay relatively simple because they simply avoid solving certain problems. I think that is a fair tradeoff. I think good language design is really about figuring out what problems can we dump on the user which they will be able to handle relatively well.

A reason why there are so many languages I think is because different people have different preferences in terms of what problems the want to avoid dealing with and what problems they are fine dealing with.

Personally I think that if you "solve" a problem by making the language so complex that developers cannot easily read and reason about the code, then you haven't actually solved any problem. Priority must always be placed on the ability of developers to read and understand code.

Unreadable code will naturally be a source of bugs.

> Whether we look at Zig, Go or Julia they stay relatively simple because they simply avoid solving certain problems.

Could you expand on the specific problems that each language avoids?

Till recently, Go lacked generics. Aside from that, it has a GC that is pretty nice to work with. Manual memory management is semi-supported, using C interop instead of native language constructs (from when I last looked into it).
Go is a thoroughly uninteresting language bringing nothing new to the table. D, C#, even Haskell all has and had for decades now GC with manual memory at places. Other than virtual threads, which I have to give credit for, there is zero new thing from a PL perspective.
Erm, Go channels were a pretty big deal in the beginning, as well as the idea of creating a 'better C' (instead of a 'better C++').
I absolutely don’t see anything in Go resembling C from a technical perspective, other than (before generics) being similarly unexpressive. It is much closer to JS than C in any way and shape.
I don't know, I currently write both C and JS on a daily basis, and I just can't see how Go is anything like JS. For me, Go is definitely in the C syntax family, while trying to solve roughly the same problems where Java has been used extensively (backend services).
> Erm, Go channels were a pretty big deal in the beginning

In what world did people not know about MPMC queues?

When Go came around I wasn't aware of other popular languages which has channels as a core language feature (but maybe I was just ignorant).
> other popular languages which has channels as a core language feature

That’s because other languages did not need channels as a core language features.

Go does for the same reasons it needs e.g. hashmaps or append as “core language features”: no generics.

As far as I could see, there was one cool thing Go brought to the table, and it’s select {}. That was (and still is, I think) pretty uncommon, though not unheard of (Erlang’s receive is similar, though it has different capabilities owing to different language semantics).

That's a pretty harsh take on Go. Many people find how it does concurrency, OO without classes (methods assigned to structs), its simplicity, and compilation speed as refreshing. At least in comparison to C, of which few would know how to make a better "successor" language for general use among the masses, than Ken Thompson (who co-developed C too).
Julia mostly ignores static compilation (although there now is work to add the ability to statically compile for a relatively large but limited subset of the language).

The big thing that this allows is for you take the ideas of C++ templates to the extreme. In Julia pretty much every function behaves like a C++ function that is templated on the type of every input. This has the major advantage that it is really easy to write extremely fast generic code. This only works in general though because Julia only has to generate code for the types that actually appear rather than all the type combinations that static analysis can conclude might appear.

Unreadable as in, “this code is unfamiliar and I haven’t talent the time to understand it,” or “this code is syntactically designed to be difficult to parse by humans and I cannot read it?”
What about, I can't read this code. Who's the idiot who wrote it?

git blame

Oh that was me.

This was often my issue. But I remember while learning more Rust I was like: 'now I can go back to my early Rust project to improve it!' and when I did check it: 'oh.. this is actually not bad! How was I able to do it?'
> Nice balanced writeup! However, I am not convinced one can make something solving all the things Rust solves which is substantially simpler as language.

I used to think this too, but my opinions have evolved a bit.

Recall the transition from goto-heavy assembly code to C; it solved all the things assembly solved, and was a substantially simpler language. One of the keys to designing a good language (or any good abstraction, really) is to identify the patterns that already informally exist.

So if we want to see a simpler Rust, we need to look at the informal patterns that exist in Rust.

* Zig did this with Rust's async/await, by making it colorblind. [0]

* Vale did this with Rust's generational indices, by making generational references, a way to get memory-safe single ownership without a borrow checker. [1] It then adds a hardened FFI boundary to get more solid safety guarantees than Rust could. [2]

* HVM did this for Haskell by looking at Rust's particular blend of borrowing and cloning, and making it happen automatically. [3]

* Lobster did something similar for an imperative mutable language, to reduce RC costs lower than ever before. [4]

I think Rust is an amazing first attempt at an interesting new problem space, and a huge step forward for the field. Now, language designers are starting to notice that the building blocks that Rust trailblazed might be combined in new interesting ways, to make new interesting languages.

It's an exciting time for programming languages!

[0] https://kristoff.it/blog/zig-colorblind-async-await/

[1] https://verdagon.dev/blog/generational-references

[2] https://vale.dev/memory-safe

[3] https://github.com/Kindelia/HVM

[4] https://www.strlen.com/lobster/

Saying C solved all the problems assembly solved is a bit too strong; bootloaders and kernels, green thread or coroutine implementations (like libco), and cryptographic code are all implemented in part in assembly.
IMHO if C could solve all the same problems as assembly, it would need to be down on the assembly language level - which it isn't, C is clearly a high level language, it's much closer to any other programming language than any assembly dialect (maybe with the exception of Forth-like languages).

In general, the higher level the language, the more flexibility you need to give up (but that's ok for many solutions).

Good point, the statement was too absolute. Perhaps I should instead say that for a lot of use cases C solved enough of the problems that assembly previously solved.
Wow, this perspective makes me really excited for the future of new programming languages. Thanks.
> However, I am not convinced one can make something solving all the things Rust solves which is substantially simpler as language.

I wonder a lot about this too. As far as I know, rust is alone. It’s the first language to exist which does compile time memory management in a safe way. Every other language either is memory-unsafe (C, Zig) or garbage collected. (Go, JS, etc).

Rust might the first language of its kind, but it probably won’t be the last. I’d be very surprised if we’re still using rust 100 years from now. Subsequent languages can learn from rust’s accidental complexity and clean up the approach.

Personally I suspect it’s possible to have a borrow checked language that’s much simpler than rust. I don’t know what that would look like, but it would be surprising to me if we found the best programming language in this space on our (collective) first attempt at it.

But I don’t have enough insight to figure out what rust’s successor would look like either. But luckily for both of us, there are a lot of smart people thinking about the problem now. Let’s give it a decade and see.

There have been memory-safe non-GC languages before, like Ada SPARK, but they generally disallow heap allocation altogether. If you heap allocate you get a GC. There were also some academic languages, most notably ML Kit, that offered a safe no-GC region-based memory management system that has a lot in common with Rust. Also, there are academic languages like Cyclone that have a very Rust-like memory management system, but they all use GCs in the end. Still, if you know Rust, Cyclone's ideas will be very familiar.
Small point of contention, but it’s actually Rust that has a Cyclone-like memory management system.

Cyclone’s initial release was ‘02 and it finalized in ‘06.

Oh, I'm sorry, I didn't mean to imply that Rust invented any of those things. Cyclone was a big influence on Rust actually. We chatted with Dan Grossman early on, and his presentation "Existential Types and Imperative Languages" is the earliest description of the problem the borrow checker is trying to solve (check it out, it's a great presentation!)

We stand on the shoulders of giants :)

Likeness is bidirectional and doesn’t imply derivedness, though.
I feel like I stuck my foot in my mouth with my comment, but separate from that…

This is an interesting perspective, I can’t think of a reason you would be incorrect, but I know I have some default intuition that likeness is most often used to imply derivedness. Also, that’s a great way to phrase what you meant.

You just (accidentally, no doubt!) well-actually’d someone who helped build Rust and has been invoked in it since long before 1.0. ;-p He’s aware of the direction of influence and was just saying they’re similar, not saying one influenced the other.
No worries, it's my fault for not being clear. The absolute last thing I want to do is to take credit for things we didn't do. Rust wouldn't exist without that academic work by Tofte, Talpin, Grossman, etc.
I said it in my sibling comment above, but to be clear, I was well aware you almost certainly knew Cyclone’s general age. I don’t want you to think I was implying or hinting anything near taking or denying credit.
I’m aware of pcwalton’s involvement in Rust. So it wasn’t accidental in any way. It was however slightly tongue-in-cheek, but I probably could have used a /s to indicate it wasn’t an attempt to score ‘I’m smart points’. I was also very aware that, despite anything else, they were almost certain to be aware of which came first.

I may take serious issue with pcwalton’s assessment of the performance characteristics of OS threads vs other concurrency implementations, but fun HN arguments aside; I have never seen or heard of any lack of acknowledgement of preceding language/implementation work leading to Rust’s development. So I certainly wasnt attempting to imply any stealing or denial of credit/attribution.

I gather that in later versions of the ML Kit, the contents of regions were also garbage collected. The problem they had with the compile-time lifetime / region analysis was that in difficult cases an object would be given a longer lifetime than necessary, leading to space leaks
There's some exciting work already going on in this space, in fact!

* Andrew Kelley is looking into combining escape analysis with allocators to assist in Zig's memory safety. [0] It could be quite a step forward!

* D is adding a borrow checker! [1]

* Cone is adding a borrow checker on top of user-definable allocators. [2]

* We're currently prototyping a region-based borrow checker [3] for Vale, which should get us most of the benefit of Rust's borrow checker with only a fraction of the complexity. It's opt-in, not upwardly infectious, and can stay near the "computational leaves" of one's program.

[0] https://news.ycombinator.com/item?id=31853964

[1] https://dlang.org/changelog/2.092.0.html#ob

[2] https://cone.jondgoodwin.com/memory.html

[3] https://verdagon.dev/blog/zero-cost-refs-regions

It sounds like the "region borrow checker" is fundamentally based on reference counting, which makes it a garbage collected system. Also, statically eliminating reference count traffic from the stack isn't new; it goes back to Deutsch and Bobrow 1976. Actually, I suggest reading the paper; it seems like a quite similar system [1].

[1]: https://www.memorymanagement.org/bib.html#db76

The region borrow checker was originally designed for a reference-counted system, but not any more.

We later discovered generational references, a method for memory-safe single ownership with no borrow checker, reference counting, nor garbage collection. To our delight, the region borrow checker works even better with generational references than with reference counting =)

(comment deleted)
Generational references are best seen as an alternative to RC that makes different tradeoffs. They add overhead to reads/writes thru the managed reference, as opposed to the overhead for changes in ownership you see wrt. RC/ARC in something like Rust. (Note that other languages like C++ or Swift cannot cleanly separate ownership changes to the same extent as Rust, so their overhead is ultimately a lot higher. Especially Swift, with its ARC-for-everything approach.) They look like they might be good complementary approaches.
I'd be curious as to whether my comment here hypothesizing an AI wizard stikes you as "just nuts", or not.
Frankly it seems strange to me to be comparing Vale's generational reference system and Rust's borrow checker directly. They have completely different characteristics and are not direct substitutes for one another.

First, Rust's borrow checker incurs zero runtime overhead for any pointer operations (whether dereferencing, copying, or dropping a pointer) and requires no extra storage at runtime (no reference counts or generation numbers); it's entirely a set of compile-time checks. Generational references, on the other hand, require storing an extra piece of data alongside both every heap allocation and every non-owning reference, and they incur an extra operation at every dereference.

Second, since Rust's borrow checker exists entirely at compile time, it doesn't introduce any runtime failures. If a program violates the rules of the borrow checker, it won't compile; if a program compiles successfully, the borrow checker does not insert any conditional runtime panics or aborts. Generational references, in comparison, consist entirely of a set of runtime checks; you won't found out if you violated the rules of generational references until it happens at runtime during a particular execution and your program crashes.

Finally, Rust's borrow checker applies to references of all kinds, whether they point to a heap-allocated object, a stack-allocated object, an inline field inside a larger allocation, a single entry in an array, or even an object allocated on a different heap and passed over FFI. Its checks still apply even in scenarios where there is no heap. Generational references, on the other hand, are entirely specific to heap-allocated objects. They don't work for stack-allocated objects, they don't work for foreign objects allocated on a different heap, and they don't work in a scenario with no heap at all.

All of these are fundamental differences which mean that Vale's generational reference system is not at all a replacement for Rust's borrow checker. It's not zero-overhead, it doesn't catch errors at compile time, and it's fundamentally specific to heap-allocated objects. In these ways it's more comparable to Rust's Rc, which introduces runtime overhead and is specific to heap-allocated objects, or RefCell, which performs checks at runtime that can result in aborting the program.

It doesn’t have to. If your allocator is the first-class citizen in the language, simply check the allocated memory when allocator itself is manually destroyed (exit its region) is enough for detecting memory leak.
Borrowing, in a way, looks a bit like the anchored pointers suggested by Henry Baker in [1]. I recall reading someone did a static analysis to detect what pointers could be anchored, but forgot which someone. However the "deferral" suggested is very different to the Deutsch-Bobrow deferral; what makes me think it is more like borrowing is that anchoring avoids destruction due to linear types, like borrowing avoids moving or copying. (Regions in Cyclone would be somewhere in between, too.)

[1] https://plover.com/~mjd/misc/hbaker-archive/LRefCounts.html

D is adding everything, it seems.
I wonder if that could actually be hindering its adoption. I think the 'kitchen sink' C++ style is not so popular anymore.
When it comes to language popularity, we have to keep in mind that D is way more popular than so-called up and coming languages (where it's oddly ignored they came out many years ago) such as Zig, Nim, Crystal, etc... Dlang often floats around the top #30 in the TIOBE rankings.

It really is a very tough hill to climb to crack the top 10 among languages. Relatively speaking, D has done well for itself, and arguably is in the position to climb further up and possibly be in the top 20 of language rankings.

Have seen a lot of talk and theories about why D didn't or hasn't become more popular. Some of them have to do with circumstances, not just about the number of features. For instance, Dlang's DMD reference compiler was under license restrictions by Symantec, which wasn't changed until 2017 (now it's under the commercial friendly Boost license). A lot of companies and corporate sponsors get leery about licensing issues.

There were also early squabbles about API design, like Tango versus Phobos, which are mostly resolved these days. But, years ago, it was less clear about the direction or how it would get sorted out.

D, as a contender to climb even higher up the language ranking charts, is still a strong possibility. D might even remain and always be more popular than Zig will ever be (especially if it wants to fight and die on the more syntactically complex "better C" hill), despite this view on D adding so much. Which could even be more of a selling point (kitchen sink included), than an hindrance, and particularity as an already established and developed older language.

If D is in the top 30 of languages according to these rankings that just shows how flawed they are.
Fascinating (and heartening.) I wonder whether this work takes us any way toward an AI wizard "in between" Rust and Zig (say) as I've described in a longer comment here. Am I nuts to hope for such a thing?
There are several languages which use linear types, Rust being the most popular. For example, https://github.com/austral/austral claims to be simpler than Rust and was discussed on hackernews a few days ago.
There is also Vlang [0] which the Zig core maintainer seems to both look up to [1] as well as look down upon [2].

[0] https://github.com/vlang/v [1] https://news.ycombinator.com/item?id=19086589 [2] https://news.ycombinator.com/item?id=31795445

EDIT to reflect critical attitude as indicated by [2].

I don’t have a real opinion in this particular topic area, but I am certain many people will find the Andrew Kelly reference to be not entirely accurate.
Neither do I. The word seems was used as a tentative qualifier rather than an assertion. Feel free to provide context that would indicate otherwise if you deem it salient.
You did nothing wrong in pointing out the difference in attitude. The change in views is arguably indicative to the extent that Vlang was perceived as a real threat, competition, or could become more successful.

Vlang has continually maintained its development pace and popularity (https://github.com/vlang/v/releases), so that pretending to dismiss it as if it was nothing or attempting to smear the author just does not work. It's not going away. Vlang is also not a one-man show, but has numerous other committed developers, new contributors, and loyal long time supporters.

I think the reason Vlang gets so much heat is that they dismiss criticism and make claims that PL people think are misleading.

You can see this in the latest Vlang release thread. I suggest those curious take a look at it and judge for themselves.

Actually, what people can find is the author of Vlang has been shown to welcome constructive and helpful criticism versus outright trolling or blatant smearing. For example- "What don't you like about V / what would you like to be changed?" (https://github.com/vlang/v/discussions/7610).
It was rather a failure to deliver on the promises than perceived as a threat to Zig and the target audience for both differ enough that V doesn't have an effect on the success of Zig nor does Zig have an effect on the success of V. They are both reaching for success within their own domains with some but not much overlap.

I know you're very fond of V but misrepresenting criticism or a negative view of the project as "fear", "perceived threat", and so on is rather dishonest and a disservice to the project. The above is a comparison of before release and long after where there's been a chance to evaluate V for what it is rather than for what it advertised.

If people are still typing words with fingers to make computers do things, in 100 years, technology has failed!
Maybe, maybe not. Technologies that are 'good enough' just stick around forever instead of being replaced by something entirely else. Wheels are still round, we still print words on paper, we still use glass lenses when eyesight starts to fail, rifles don't look fundamentally different from when they first appeared, etc etc...
> rifles don't look fundamentally different from when they first appeared, etc etc...

I would claim that a modern rifle flies at hundreds of miles an hour, and is operated remotely.

Rust is the first achieving commercial success (so to speak), several others have precedded in the use of affine types.

While we cannot, and should not, take away its glory in achieving this, we should also not address it as if all belongs to Rust alone.

Another possibility is Rust continues to get more advanced in ways that remove constraints over time, making it less complex to use in practice. We already see a lot of (small, incremental) improvements of this type, where a bunch of compiler work goes into proving that new cases are actually safe and therefore allowed

Basically what I'm wondering is if Rust has room to become that language that's learned from the ways Rust has been clunky in the past, without introducing dramatically breaking changes

I'm keeping an eye on Vale [0] but will continue to use Rust for hobby projects because the community/tooling that has built around it has made it much easier over a few years than when I first gave it a try in 2018 and especially as support for development on Windows grew. I went from absolutely struggling to get so much as a win32 GUI window to open [1] to writing a basic counting app [2] for a game I play to completely rewriting that app to not suck [3] (even though the code is still awful).

Zig looks nice to read and understand. However at my skill level (more or less a skript kiddie) I don't trust myself to not cause major memory-related bugs. In fact, I can't help but wonder if it's hubris or what when extremely intelligent, college educated, programmers with more than my lifetime of experience who still manage to write programs riddled with memory safety issues and in the very next breathe say memory safety isn't an issue just write safe code. I wonder what skill/knowledge thresholds are where "I don't trust myself to not make those mistakes" turns into "I won't make those mistakes."? Honestly it reminds me of the low IQ/high IQ meme [4].

[0] https://vale.dev/

[1] https://gitlab.com/NadyaNayme/rusty-eyes

[2] https://github.com/NadyaNayme/Dorothy

[3] https://github.com/NadyaNayme/Dorothy-egui

[4] https://kimiwo.aishitei.ru/i/sZaEWnY3NvfXecLm.png

I have to agree. Adding safe, non-automatic memory management to a language will increase its complexity considerably, and I don’t see it changing even with the below mentioned new languages/techniques. Even just fundamentally, manual memory management begets functions that know about the memory architecture of your program, solidifying a given architecture. Changing this latter will result in a recursive refactor, which even if safe, is problematic/complex.

Sure, it is a worthy tradeoff in certain, very niche cases (though other than writing other performant runtimes and realtime audio processing, I fail to see another area where a GC couldn’t fit - including even OSs), for which we need a language, but I really don’t get this recent hate for garbage collection, when it is truly one of the biggest productivity improvements for a PL, while simultaneously it being very performant (quite likely outperforming any naive manual take on more complex problems).

I think you are absolutely right about everything you mentioned, except for one:

> I am not convinced one can make something solving all the things Rust solves which is substantially simpler as language.

You could be right about this, but boy, I hope you are wrong. And I'm trying to prove that it's possible.

I think the answer is structured concurrency [1], and here's why:

1. Structured concurrency can replace everything async/await can do. At least, I believe so, kind of like it's believed that structured programming can replace any unstructured use of goto.

2. If you extend structured concurrency a bit so that the thread tree is actually a DAG [2], you basically get a borrow checker for free because every item is rooted in a thread stack somewhere, and where it is rooted is always before everything that it could own and after everything that could own it.

3. Making the thread tree a DAG also means it's easy to implement RAII and have it work meaning no use-after-free, no double frees, etc.

Put that on top of a language that has memory safety by default, and you will have everything that Rust gives you without the complexity.

This isn't theoretical; I have implemented this in C. [3] (See [4] for a use of it.) I have even implemented bounds checking in C. [5]

However, I'm also implementing a programming language with these ideas (see [6] for the example file and [7] within that file for how structured concurrency might be exposed in the language).

As for readability, I do think that what I have is readable. However, I appreciate comments; readability is important to me too, but I've spent so long with this work that I am blind to what is actually readable to other people.

[1]: https://gavinhoward.com/2019/12/structured-concurrency-defin...

[2]: https://lobste.rs/s/8msejg/notes_on_structured_concurrency_g...

[3]: https://git.yzena.com/Yzena/Yc/src/branch/master/include/yc/...

[4]: https://git.yzena.com/Yzena/Yc/src/branch/master/src/rig/bui...

[5]: https://git.yzena.com/Yzena/Yc/src/branch/master/include/yc/...

[6]: https://git.yzena.com/Yzena/Yc/src/branch/master/src/yao/exa...

[7]: https://git.yzena.com/Yzena/Yc/src/branch/master/src/yao/exa...

> ...Priority must always be placed on the ability of developers to read and understand code.

The importance of this can't be overlooked, which is debatably why people and companies are constantly looking for a way to escape heavily complex languages and syntax. It is part of the thinking behind somewhat newer languages like Go, or newer offshoots like Vlang (https://vlang.io/), which has a focus on high readability and general usability.

If a language is being geared towards the majority or mass appeal, then it has to find that correct sweet spot. There is simply going to be a cap on how far complexity will be embraced or how much specialization will be found useful.

“Genius is making complex ideas simple, not making simple ideas complex.” — Albert Einstein

> ...I am not convinced one can make something solving all the things Rust solves which is substantially simpler as language.

Debatably, such languages making the attempt, are less likely to succeed. Off rip, Rust already occupies that space. So, trying to make a "better Rust", which is already a specialist in that field (safety) and has already got "the push" from large corporate backers (Mozilla, Samsung, Quantum...) will usually mean failure, remaining in niche status, or overcoming incredible odds (like hitting a Lotto jackpot). The more the language specializes on that particular use case, then arguably the less general appeal and general use. And if there is already competition doing it better or being more known for that focus, the harder the hill to climb.

For that matter, trying to make a "better C", is another tough hill. Not only because C is so entrenched and established, but that there is already a long list of quasi "better Cs" out there such as Object Pascal, D, Nim, etc... Add in more complex or confusing syntax, less features, less functionality, or less usefulness... Then the hill gets even more steep. Hitting the bullseye, just isn't easy.

How much, of what is exposed as the complexity of a language, could be realized in the form of sequences of optimizing compiler passes, perhaps automatic or perhaps flag-driven, such that the programmer user experience remains clean-ish yet the sophistication of generated code is on par with a language like Rust?

I don't know the answer, but I find it a curious question.

(And are SBCL internal compilation optimizations an example of this idea?)

> How much, of what is exposed as the complexity of a language, could be realized in the form of sequences of optimizing compiler passes, perhaps automatic or perhaps flag-driven, such that the programmer user experience remains clean-ish yet the sophistication of generated code is on par with a language like Rust?

This is an excellent question, and one the Dylan designers wrestled with in the 90s. How could they make a high-level language with many of the best features of Lisp, while still making it possible to perform like C?

The strategy they chose was:

1. Offer powerful features with simple interfaces, even though performance was poor in some cases.

2. Provide "fast path" versions of these features that handled common cases with high efficiency.

This turned out to be something of a disaster. Compiler implementors needed to implement the slow path, plus code to detect when to use the fast path, and finally the fast path itself. This ended up being 3x the work of just writing the fast path. And to make things worse, users never really knew what constructs would cause them to hit the slow path. (And yes, this is similar to the challenges SBCL faces, but SBCL has had a lot more time to work on them.)

Rust has two major sources of complexity: (a) the ownership model makes certain common architectures much harder to write (particularly with GUIs and non-ECS games), and (b) Rust cares about references and memory layout and lots of system-level stuff like that. The latter is hard to abstract away without losing performance.

I love Rust, and I look forward to seeing a new generation of languages remix its ideas. But I don't think that pairing a default-inefficient language with a "sufficiently smart compiler" is the best way to get there.

I am all in on Zig. I was more producitve with it in a few weeks than I was after a few months of rust. Yes the memory management is "manual" but the std library functions for dealing with it are top notch, you have to free a lot less due to the ease of using arena allocators, and the testing allocator does a valgrind-esque runtime analysis of your code - very quickly and very thoroughly. In practice, it's not an issue, and unlike with rust I have a very clear idea of when and where memory is being freed and allocated.

That having been said - the core team chooses some incredibly dumb hills to die on. Last time I checked the tab character was illegal in Zig code - though I understand that's changing.

More concerning is that un-named parameters are a compiler error and this cannot be adjusted. Meaning that if you want to test on save, you can't if any of your parameters are unused. Enforcing such a thing at the compiler level because of your personal coding style is so mind-numbingly short sighted. The guy who made it is clearly a very smart and insightful, but I acknowledge you probably have to be fairly stubborn to decide you're going to replace C.

> In practice, it's not an issue, and unlike with rust I have a very clear idea of when and where memory is being freed and allocated.

I'm very skeptical that the lack of memory safety is not going to be an issue "in practice". That's what every C++ developer thinks too until they write code people actually try to attack.

One of the very nice things about memory safety is that it frees the programmer from having to think about when memory is freed most of the time. The compiler or runtime system will take care of that. Most of the time, manual frees are just noise--this observation is the entire reason behind destructors.

Learn Zig and get back to me. It's nothing at all like managing memory in C++, not even close.
We've had close to 50 years to answer the question "is memory safety necessary to write secure code under normal industry development practices"? During this time dozens of languages have come and gone, and the evidence still overwhelmingly points to "yes". It's going to take a lot to make a convincing argument otherwise, especially since Zig doesn't really do much different from C here, semantically speaking.
I've explained how Zig memory management is different. You've chosen to ignore that and just compare it C and C++. I've suggested you learn Zig. You've ignored that too, and just zeroed in on the fact it's "manual".

Here's a car analogy - old manual vehicles had unsynchronised transmissions where the clutch had to be depressed twice per gear change. Newer manual vehicles use steering wheel mounted paddle shifters and the clutch only needs to be depressed when stopping or starting.

You're trying to push your weird semi-automatic transmission - that a lot of people find unusable as a daily driver - by ranting about how awful double clutching is.

Car analogies aren't a substitute for actually writing widely-attacked software.

There are systematic reasons why I'm skeptical Zig's approach will lead to better results than what we see in modern C++. The worst memory safety issues from a security point of view tend to be use-after-free caused by complex, untested code patterns, where one programmer wrote, or implied, "free", without realizing that somewhere else in the millions-of-lines codebase some other part of the code, written by someone else, still had a reference to the object. Unit tests don't catch these problems because they're caused by strange patterns that the programmer didn't anticipate. That is the kind of vulnerability that is difficult to solve without either a compiler or a runtime system (GC) to help you, and I see nothing in Zig that solves it. You can, and people do, make raw pointers to other objects in Zig.

In support of the parent who already knows all this: you don't even need to take raw pointers to have these problems! You'll usually get a slice from your allocator. Keeping any copy of that or any slice into that (e.g. a string in a struct obtained by deserializing a json document into a short-lived slab of memory) will get you use-after-frees too.
Zig is not trying to be a better C++, so I don't see how complaining that it isn't a better C++ is relevant.

It's not trying to be for large, widely attacked projects. It's for things where the complexity overhead (whether resources or mental overhead) of Rust is too much, so you'd have to use C.

It's not meant to make memory errors impossible, it's meant to make them much easier to avoid or find and remove than it is in C.

Noone is suggesting anyone make a web browser in Zig, but it's likely useful for drivers or similar where you'd otherwise be using a lot of unsafe code, so rust's guarantees are at best a partial help, and at worst a false sense of security.

They could have used Object Pascal or Modula-2, and would get the same memory safety guarantees that Zig offers.
And it makes no pretense otherwise.

Why bring this up?

One would thought a C replacement in 2022 would have better hindsight regarding memory safety than what was already possible in 1978, which was actually even available in JOVIAL (1958), or ESPOL/NEWP (1961), among many other possible examples.
Guarantee =/= capability.

There are plenty of tools in the language and ecosystem for dealing with memory errors and reducing their frequency, but you're clearly not interested in a rational discussion.

But isn't the position of a "Better C", already covered too, by languages such as D? And I mean literally too, with their "-betterC".

There is arguably no place for Zig to go, along the "better C" route, that hasn't been covered by other languages. And languages like D and Object Pascal, are way up on the charts, so it will be a hard climb to reach them in popularity. Even the much older Nim language (which has been called a C alternative), still hasn't cracked the top 50, among language rankings. Debatably, a language's "claim to fame" (to escape niche status), can't just be about being a more complex C replacement. There has to be other things about it, that it can firmly stand on, for greater mass appeal.

D is garbage collected and requires a runtime.

Object pascal appears to be pascal with classes.

Both of them have exceptions and operator overloading.

Zig is smaller than object pascal or D, and closer to the metal than D, it's arguably simpler than C as it's more regular, has no hidden control flow and has no separate preprocessor.

The unique things it's attempting to do are manual memory management with handrails (not dissimilar to pascal, but maybe with a few more handrails like testing allocator), error handling with better erganomics than go, rust, or unchecked exceptions (but slightly less power), and comptime which afaik is unique and is where it provides much of the power to do things like generics, many kinds of composition, or even implement a borrow checker and lifetimes as user code (with terrible erganomics and poor performance, but it's illustrative of its power).

The thing it stands on for mass appeal is trivial interop with C and the zig compiler is a functional, statically linked C toolchain out of the box and is easier for a beginner to do things like cross compile on than gcc or clang msvcc or similar. Additionally it has the power and simplicity of C with fewer footguns (ie. it attempts to cover some of pascal's strengths, but on the balance I like pascal more as a teaching language). It also shares some of nim's goals like fast compilation and hot code reloading.

The thing I'm personally interested in is the type system and semantics of comptime seem to have all the ingredients required for formal verification with no additions to the language, translations to a prover, or annotations (but it would require extending the compiler's abilities considerably). This is unique if true (but I will lack the skills to find out if it is true for a while yet, and the people closer to it are busy with other things).

Whether it will succeed, or the comptime concept is fatally flawed in some way is as yet unknown, but it does fill out a currently empty niche as a simpler C, it is not trying to be yet another more complex C.

I think its main competitor if it is successful will be go. Go is slightly higher level, arguably marginally simpler and considerably less powerful out of the box (at least last time I looked before the latest generics stuff). The last two are arguably in its favour for being a better C.

The languages which are closer to Go, are Vlang and Odin. With Vlang also having a focus on C interop as well (importing C libraries, C2V transpiler, and ability to be compiled to human-readable C). These debatably have a much better shot, with being viable alternatives to Go, in terms of use cases.

Zig is arguably in a different category and appears targeted to a more limited or lower space, in terms of general programming, which brings it more in the orbit of Rust. It's made clear that Zig's features and size will be purposely kept to the minimum, though its syntax is significantly complex. Which may hamper it, as it appears such complexity does to Rust, in terms of being picked up for general usage.

It's hard to say how attempting to be a "better Rust" or attempts at taking on C, will work out. But, it does seem clear it's not going for more general and wide usage that languages such as Object Pascal, D, or Go cover.

By the way, Object Pascal can be written where it comes quite close to classical Pascal, as classes are extensions of the language and more optional than required (see also advanced Records and/or Objects). Object Pascal is used in embedded systems, along with various dialects of Pascal, specific for the purpose. There are also various variants of Go, aimed directly at embedded development (TinyGo). So variants of Go and Pascal can more directly compete with Zig, and they are already well established (like Rust is). Now how this will all turn out, in say 5 years from now, anybody's guess. But, it will be an uphill climb for Zig, as for any new language trying to displace established ones.

Vlang documentation (https://github.com/vlang/v/blob/master/doc/docs.md)

Odin documentation (https://odin-lang.org/docs/overview/).

Golang by example (https://golangbyexample.com/)

> Zig is arguably in a different category and appears targeted to a more limited or lower space, in terms of general programming, which brings it more in the orbit of Rust. It's made clear that Zig's features and size will be purposely kept to the minimum, though its syntax is significantly complex. Which may hamper it, as it appears such complexity does to Rust, in terms of being picked up for general usage.

I don't understand the basis on which you're saying this. From where I'm standing only odin looks simpler, the other two just make different tradeoffs -- namely trading reduced metaprogramming facilities (and the danger of complexity that entails on user code) for more language features.

> It's hard to say how attempting to be a "better Rust" or attempts at taking on C, will work out. But, it does seem clear it's not going for more general and wide usage that languages such as Object Pascal, D, or Go cover.

D and Go are closer to rust than zig is, and object pascal is also just another OO language. Of those, only Go is even attempting to be a better C, but requiring a runtime prevents it from being a true competitor.

I don't think pcwalton is a stranger to Zig; I'd be surprised if he hasn't played with it yet. Regardless my experience with Zig, is, as far as memory management was concerned, it was less powerful than C++. A UAF is still trivial in the language (the stdlib makes exploiting one pretty difficult); and the lack of smart pointers still seemed like a strange decision in our multi-threaded, multi-core world.

This makes something like the use of sanitizers required for large projects in Zig; It's a race against time until your project gets big enough that someone mistakenly frees something that gets used 200 lines later.

Slightly off topic, but I would prefer to never see smart pointers in Zig. Smart pointers are an inefficient and semantically-unenforceable ‘solution’ for the wrong problem. Smart pointers are supposed to be a way to handle references to mutable data in concurrent/multi-threaded/distributed computation, however I don’t think they form a satisfactory basis to solve the problem they think they solve. Now, I’m not a fan of Rust’s approach in general or it’s approach to this problem, but at least it is a semantically motivated solution.

If a Zig user wants to restrict usage of pointers in certain ways let them make their own data structures, but unless Zig is willing to alter its semantics including a ‘standard’ smart pointer is putting icing on a cardboard box.

/edited to fix a typo/

Rust is a terribly complex language, there are people at FAANG companies threatening to fork the language when attempts to simplify it are proposed. The amount of half-baked features present in the language are footguns in their own right.

I love Rust, but I think Zig solves 70% of the memory safety problem at the benefit of a much simpler language.

> Rust is a terribly complex language, there are people at FAANG companies threatening to fork the language when attempts to simplify it are proposed.

Ironically, the person I think you're referring to is actually me, and that's a strong mischaracterization of what happened. Nobody was ever "threatening to fork the language". Besides, that's irrelevant to this discussion, because none of the complexity of Rust compromises memory safety.

> I love Rust, but I think Zig solves 70% of the memory safety problem at the benefit of a much simpler language.

I don't believe the 70% number without evidence. Again, the problem that we really need to solve is complex UAF caused by unexpected interactions between components of large software systems, and I see nothing in Zig to solve that.

The problem is that solving 70% of memory safety is arguably the same as solving 0% of it.
So basically, using Rust is also entirely pointless as soon as it needs to call into non-Rust code (like operating system shared libraries)? ;)

On a more serious note: IME most memory corruption issues in C and C++ are 'side effects' of the sloppiness of those languages (like not catching overflow, missing range checks, problems arising from implicit type conversions, sometimes just not compiling with warnings enabled, etc...).

Zig enforces much more correctness (to a point where it is sometimes annoying to a C coder, but one quickly gets used to it), and it even does this when building C/C++ code (just by using much more 'picky' default compilation options than C/C++ compilers).

But, is Zig really all that much simpler a language, from the viewpoint of a C alternative? And is that simplicity partially due to lack of features and youth, so that the complexity can be expected to pile on later. We are talking about a language that hasn't hit 1.0 yet, nor is not due to for some years to come.
Andrew Kelley has been pretty explicit that he's going to hold a hard line on not adding a surplus of features to the language.
I’ve met a lot of seasoned C++ developers who tell me the memory safety won’t be an issue in practice. Hell, I’m sure I’ve said it before too.

I’ve yet to meet one who actually didn’t end up with some safety issue in their code. Not all of their code mind you, but even a couple times can be significant.

Like you, I like memory safe languages because they let me think about my problem and trust the tooling to catch entire classes of mistakes for me

> I’ve met a lot of seasoned C++ developers who tell me the memory safety won’t be an issue in practice. Hell, I’m sure I’ve said it before too.

The claim I've heard (and I think also made) is a bit different from that. It's more like: "dealing with memory corruption issues isn't a significant portion of the debugging effort in [idiomatic] C++ code". i.e. the claim is not that you somehow won't run into them; rather, the claim is that they form a small fraction of the actual bugs you run into (either count-wise or time-wise), if you're writing C++ "properly". (If I had to make up a number, I'd say < 5%.)

This obviously isn't to say memory safety isn't an issue, or that security is a negligible property. Rather, it's to point out that most issues are of a different kind, and if you think that memory safety will decrease your workload significantly, that's unlikely to be the case if you're writing C++ with the correct idioms.

I think this response is usually in response to claims about how (say) a language like Rust results in N times faster development than C++. If that's truly the case, it's unlikely to be due to memory safety per se, but due to other factors.

To add to that, there are some applications for which memory unsafety just doesn't cause that much harm.

I worked on a very large C++ codebase that compiled to webassembly, which runs sandboxed in people's browsers. It only talked to one (trusted) server.

Like any project, we had plenty of logic bugs every release. They would often result in odd artifacts on the screen and sometimes a crash.

Only 5% of our bugs were related to memory unsafety, and they also resulted in odd artifacts on the screen and sometimes a crash.

In other words, memory unsafety wasn't much of a concern in practice. It was as bad as any logic bug.

This was the main reason we stuck with C++, rather than spend SWE-years migrating to a more memory-safe language.

(I know this is ironic coming from me, who designs a memory-safe language full-time!)

Running inside a memory safe virtual machine is obviously safe though, that doesn’t mean the language itself is safe.
The issue isn’t that patching and fixing code to be safe takes a ton of your own time, but it’s about the risk to everyone using your code.

From security risks to simple crashes, C++’s unsafe nature has caused a lot of issues in production in my experience. From renders and apps that crash to subtle security bugs.

Not having to focus on those when shipping software to customers means I can focus more on the actual logic and the associated bugs instead. I’m not getting back crash reports and CVEs that sucks up my time.

My users in turn aren’t getting affected by my crashes and security issues. Every person who reviews my code can also understand the language guarantees and stop passing my code through that lens and instead focus their mental energy on the other aspects of my code, like catching logic bugs.

The effects of a memory safe language extend very far

Honestly, I think what you describe is a much more defensible claim--in fact, I suspect it's true that the vast majority of bugs in C++ code aren't memory safety. I've often seen the stronger claim that memory safety problems don't come up at all in idiomatic C++, though, and that isn't true.
Fun fact: There's been a number of spectacular memory safety bugs in Rust programs where you have no option but to go unsafe. Which, in real systems programming, is quite often...

Zig is vastly safer than Rust unsafe. That's worth noting.

So while Rust's memory safety features is something I love, I absolutely hate the Rust community pushing mem safety as a panacea.

In my line of work, as an embedded programmer, Zig is vastly more appropriate.

Rust and Zig can co-exist just fine.

The problem with memory safety bugs is that they are uniquely exploitable. Even if memory bugs only take 5% of your time as a dev, there have been numerous studies showing that for large projects, memory bugs form roughly 2/3ds of CVE causing bugs.
As one of the people who often make this argument, this is spot on: I continually see people who say they are spending some ridiculous amount of time actively debugging memory safety issues as part of their normal development, and to me that indicates they are either an amateur or an idiot... like I just can't imagine what that must be like for them, as pretty much the only time I have to spend any serious amount of time tracking down an issue with memory corruption in my projects is when I run into some weird build system issue (which is sometimes caused by actual compiler bugs in clang/gcc that I have to painstakingly bisect and report upstream, but is sometimes an issue with an SDK or linker) that results in incorrect code being executed. I am not, under any circumstance, claiming that my software is free of bugs! I am 100% kept awake at night by the idea that someone will find a serious and exploitable mistake somewhere; but, almost by definition, that obscure issue isn't something that is taking any of my development time because, if I knew about it, I would already have fixed it and that would result in never having exploitable bugs in production software... put differently, unless you are being actively negligent, any dangerous/exploitable memory errors in your code aren't issues you are actively debugging: they are unknown sleeper issues (and then: issues which I will then claim are likely to be trivial to analyze and fix once seen). The point of using a memory safe language isn't to make your development faster: it is to make your development correct.
Not GP.

> I'm very skeptical that the lack of memory safety is not going to be an issue "in practice".

For what it's worth, I agree with you. And I'm a C programmer that managed to put out a non-trivial piece of C code ([1]) that has had no memory safety issues triggered in the wild after 1.0 (which was released on 26 Oct 2018, so almost 4 years, yay!) that I know of.

The truth is that I am not able to write such code; I just fuzz the ever-living daylights out of my code with lots of asserts to test all of the assumptions that my code makes, including preconditions, postconditions, everything. I also use static analysis, -Weverything on Clang (without -Wpadded; that's a stupid warning), and a massive test suite with near 100% coverage. In my new projects, I'm even using fuzzing to generate almost all of my test cases so that my path coverage is as good as possible.

That's a lot of work, and it's not fun.

But what would make it even more effective would be to effectively automatically add asserts for me to test the assumption that an index is in range whenever it is used! And that's why memory-safe languages are the only way to go: not only do they save people from bad code, they help programmers turn bad code into good code with the tools at their disposal.

I think the Zig people know this, which is why they have ReleaseSafe for testing and for those cases where you have to be sure. But I also don't see them test their code enough for me to trust that it's not needed. And I doubt that Zig users test their code enough for it too.

All of this is to say, I completely agree with you despite (so far!) being the exception to the rule you articulated, and I dread the day when my reign of "being the exception" comes to an end. I really want to stay being the exception.

[1]: https://git.yzena.com/gavin/bc

Congratulations for being part of the minority that cares,

On my experience, only small teams with top devs do happen to care for doing the security work you describe, as tends to be proven by surveys regarding the use of unit testing or static analysis tooling,

https://www.jetbrains.com/lp/devecosystem-2021/c/

https://www.jetbrains.com/lp/devecosystem-2021/cpp/

So yeah, pity that memory-safe languages have to fight back to regain what was already possible before UNIX took over the computing world and brought C for the ride.

This is why I would like that Zig could offer more than Modula-2 already did in 1978, in regards to memory corruption.

Yeah, I agree, and I like C!

I think the lack of bounds checks where they are not a problem is a massive design flaw nowadays. Zig should have made bounds checks universal and spent effort on compiler passes to elide them.

Allowing people to remove bounds checks is not smart (though Rust's unsafe is a good compromise), especially in such a coarse way as a build option.

In fact, a supposedly memory-safe language, Julia, has memory bugs because it allows users to turn bounds checks off. [1] This should never have happened.

[1]: https://yuri.is/not-julia/

The goal of the Zig project isn't to get rid of that work. It's to make it easier.

ReleaseSafe is supposed to be the 'normal' way to compile (the same role O2 takes now albeit with a performance penalty compared to O2), with releasefast or releasesmall for those times performance is unacceptable and where you've satisfied yourself with the level of testing you've done.

The philosophy of the project is to build some of those static analyses and runtime checks that help fuzzing into the compiler and stdlib. And build a culture and toolchain where it's easy to start using the rest as part of the status quo (rather than them being the exotic thing used by weird people). Finally they get rid of some of the most egregious footguns ( ostly relevant to beginners), passing slices is much more ergonomic and less error prone for a beginner, the error handling is best in class, and there are optional types and less use of void pointers.

Of course doing the actual hard work is always going to be hard work. But you can turn it from hard and gruelling work where the majority of people have no idea of the things they should even be doing and where you uave to keep a laundry list of gotchyas in your head into hard but satisfying work that feels fathomable.

It's a different philosophy from rust, but one that is no less valid.

The error handling is so far from best-in-class that it's not even funny. Lisp conditions and restarts, which can be implemented in C, are best-in-class. Beyond that, does Zig allow arbitrary values as errors now? Anything that doesn't is not best-in-class; they would be worst-in-class.

Footguns are never just relevant to beginners. I say this as something close to an expert C programmer. I hit footguns all of the time. I just don't release them because I always assume I hit them and hunt them down.

Making things easier to fuzz is table stakes; even C can do that now. I set up fuzzing for my code with a simple build option. It's not hard. It's also effective at finding test cases for you, if you use it right.

Does Zig do Design by Contract? That would make fuzzing easier. Does it do Property-Based Testing? That's a more focused version of fuzzing. In fact, it's the same to unit testing as fuzzing is to integration testing. Until Zig has Property-Based Testing, I don't believe it's making testing easier.

For that matter, as far as I know, Zig only makes unit tests easy. Unit tests are okay, but not really effective. The test suite for my code is only integration tests, which makes things easier to refactor. And yet it is effective at finding bugs.

tl;dr: What Zig has is table stakes in 2022. It is nothing special for helping people write correct code.

> The error handling is so far from best-in-class that it's not even funny. Lisp conditions and restarts, which can be implemented in C, are best-in-class.

Adding gotos is not an upgrade to readability and debugability, and comefrom was supposed to be a parody.

I suggest you read up on Lisp conditions and restarts. They are not exceptions. There is no goto other than a function call and a loop back to the spot the programmer picks. And there is no comefrom.

The C equivalent would look like this:

    do
    {
        err = some_operation_that_can_return_errs();
    }
    while (err != NULL && handleError(err) == ERROR_HANDLED);

    if (err != NULL)
    {
        panic("Could not handle error: %s\n", err_str(err));
    }
That is not goto or comefrom. Far from it. And of course, the panic at the end is not necessary; you could then return the error, or throw an exception, etc.
I'm struggling to understand how your example is not just something any language with error types can do trivially.

Every explanation of conditions and restarts I can find explicitly mentions searching the call stack for a handler and jumping into it, then reserving the capability to return to the function the error originated in.

This is explicitly non-local and hidden control flow (ie. the much maligned goto of exceptions).

The only advantage I can see asserted of having handlers (exceptions or conditions) that way is that you don't have to look for the error and pass it up manually but ! does that anyway so it doesn't seem worth throwing out your ability to reason about invariants in the context of a language with mutability and such.

Then there's returning to the stack frame you started in (the part I somewhat unfairly called comefrom). You need to drag around all the machinery required for continuations in order to do this. If the goal is to have available some of the context of an outer stack frame, you could pass the relevant references down and just handle it where you wished to resume.

Further thoughts: In some ways errdefer is the polar opposote of this concept. It allows the function to clean up and preserve invariants whilst passing control up the call chain.

> Every explanation of conditions and restarts I can find explicitly mentions searching the call stack for a handler and jumping into it, then reserving the capability to return to the function the error originated in.

> This is explicitly non-local and hidden control flow (ie. the much maligned goto of exceptions).

No, it's a function call. That's how you jump to code while reserving the capability to return.

I mean, you explicitly "raise" an error, right? What's wrong with code like:

    raise AllocationFailure;
desugaring to a function call? I thought function calls were local and non-hidden control flow, so why do you think otherwise?

> The only advantage I can see asserted of having handlers (exceptions or conditions) that way is that you don't have to look for the error and pass it up manually but ! does that anyway so it doesn't seem worth throwing out your ability to reason about invariants in the context of a language with mutability and such.

One example is that perhaps you do want to ignore certain errors, but let others through.

I have code that returns an error on EOF. Usually. In reality, it calls an error handler and lets the handler decide.

When I open and read files to the end, I'll encounter EOF, but I want to. So I'll push a handler onto the stack of error handlers that ignores EOF but handles all other errors. If I did that with exceptions, the call stack would be destroyed. Not good.

Another example is memory allocation failure. Maybe one part of your program caches allocations and needs to be told to give them up if allocation failed. So in the handler for allocation failure, it calls a function to release those cached allocations, and when the handler returns, the allocator can try again. Hence, the restart.

> You need to drag around all the machinery required for continuations in order to do this. If the goal is to have available some of the context of an outer stack frame, you could pass the relevant references down and just handle it where you wished to resume.

No, you don't. If you did, I wouldn't have been able to implement it in C, but I did. [1]

> Further thoughts: In some ways errdefer is the polar opposote of this concept. It allows the function to clean up and preserve invariants whilst passing control up the call chain.

Passing control up the call chain on error should be a last resort because you're destroying context, but I agree, it should still exist.

The cool thing is that you can do this in C too, even with cleaning up and preserving invariants. See [2], [3], and [4].

[1]: https://git.yzena.com/Yzena/Yc/src/branch/master/src/concurr...

[2]: https://git.yzena.com/Yzena/Yc/src/branch/master/src/alloc/s...

[3]: https://git.yzena.com/Yzena/Yc/src/branch/master/src/alloc/s...

[4]: https://git.yzena.com/Yzena/Yc/src/branch/master/src/alloc/s...

> raise AllocationFailure;

This is exception semantics which is hidden control flow present in every language with exceptions. Even in checked exceptions it is often obfuscated (ie. a function call that can raise is not distinguishable from one that can't wkthout manually looking at the types, and if they are distinct, then you just have something equivalent to error types). The point is that, upon calling a function that might transitively call a function that raises, you have no idea if the next line will execute or whether you will wind up in some other piece of code altogether.

It's not equivalent to calling a function because it destroys the stack context and violates the invariants and ability to reason about control flow for every function above it in the call stack.

I am starting to grok algebraic effects, and how they can make exceptions-but-not-objectively-incorrect by preserving context during the interrupted control flow and returning everything how it was left, but they come at a price.

You could guarantee that the stack will be reconstructed and execution will continue where it left off. But now 'raise' implicitly does a bunch of stuff including allocating memory on the heap and copying every stack frame between your error and your handler onto it. How does this function if your error is that you are unable to allocate?

Even then, you don't know where control will end up without hunting for the registration of the handler, which may not even be near the call site.

Sure you can build a stack on the heap in userland in C and dump everything there so as to reconstruct it (I skimmed your code and it seems like some implementation of coroutines or similar, I'm notnsure if this is part of the error machinery). There's nothing in zig that prevents you from doing this, or even using that exact library.

> The cool thing is that you can do this in C too, even with cleaning up and preserving invariants. See [2], [3], and [4].

I either can't follow your code with the amount of effort I'm willing to expend (likely) or there isn't anything resembling the descriptions I can find for conditions and restarts there.

Are you saying that the following can happen?

I can have some function foo that is regular C code with the exception of registering that it handles some condition.

Then foo can call, bar, which calls baz which calls qux, all of which are regular C code.

Then qux can call something which raises a condition of the kind foo registered.

Then control flow passes to foo which handles the error using the context in its current stack frame.

Then control passes back into qux with information provided by foo's handling code.

Then qux returns, followed by baz and bar returning without having to have any knowledge of or participation in your system.

And that all of this happens without constructing a closure over foo's context and sneaking it into qux via a global.

> This is exception semantics which is hidden control flow present in every language with exceptions.

This is not exception semantics. A language can make the keyword "raise" do whatever it wants.

Should I have used some other keyword? Fine:

    handle AllocationFailure;
In fact, let me expand it a bit:

    restart
    {
        err = allocate(size);
        if (err != NULL) handle AllocationFailure;
    }
This code calls the AllocationFailure error handler when allocation fails, and has the option of "restarting" (i.e., going back to the restart keyword and trying again). There is no hidden control flow, unless you think `errdefer` is hidden control flow.

> It's not equivalent to calling a function because it destroys the stack context and violates the invariants and ability to reason about control flow for every function above it in the call stack.

No, it doesn't. Again, I implemented this in C. I don't use longjmp(). It does not destroy stack context. You seem to have no idea what you are talking about.

> You could guarantee that the stack will be reconstructed and execution will continue where it left off. But now 'raise' implicitly does a bunch of stuff including allocating memory on the heap and copying every stack frame between your error and your handler onto it. How does this function if your error is that you are unable to allocate?

Yes, by doing a function call. It's a function call. Really.

> Even then, you don't know where control will end up without hunting for the registration of the handler, which may not even be near the call site.

The registration of the handler does not need to use the stack. In my code, it uses a separate heap-allocated stack, which is in the thread-local data.

Did you not read the actual C code I sent you? It's all in there.

> Sure you can build a stack on the heap in userland in C and dump everything there so as to reconstruct it (I skimmed your code and it seems like some implementation of coroutines or similar, I'm notnsure if this is part of the error machinery). There's nothing in zig that prevents you from doing this, or even using that exact library.

It's not coroutines. It's simple function calls. It's using function pointers, yes, but it's just function calls.

> I either can't follow your code with the amount of effort I'm willing to expend (likely) or there isn't anything resembling the descriptions I can find for conditions and restarts there.

Because your mental model of conditions and restarts is completely wrong.

> Are you saying that the following can happen?

No, that is not what happens. This is what happens:

You have some function foo that is regular C code with the exception that it registers ipsom as a handler for some condition until foo returns.

So ipsom is a registered handler until foo is done executing.

Then foo can call bar, which calls baz, which calls qux, all of which are regular C code.

Then qux can call something which raises a condition of the kind foo registered.

Then control flow passes to ipsom, which gets a new stack frame, and which handles the error using the context in its new stack frame.

Then control returns back into qux with information provided by foo's handling code.

Then qux returns, followed by baz and bar returning without having to have any knowledge of or participation in your system.

And that all of this happens without constructing a closure over foo's context and sneaking it into qux via a global because foo's context is never used as the error handler!

Again, it's just function calls and a possible jump to the beginning of a marked block.

To show this, I have fully implemented restarts now ([1]), which I didn't do because I didn't need them yet.

Now, if you had a hypothetical foo that registers ipsom as...

Then you haven't made conditions and restarts.

You've made error values. This is just one trivial thing you can do with error values without adding keywords. As you said, it's just a function call.

This is the error system that languages like zig and rust have.

Ie. the best paradigm for error handling you can get without all thenoverheadnof a pure FP language.

Except Zig has a bunch of erganomic enhancements to make them far more pleasant to deal with and reduce the mental overhead and scope for mistakes. You can't get the value out of a failable function without acknowledging and passing up or handling the error, and you don't need the verbiage of if err != nil adding noise everywhere. And it comes with batteries included so you're not going to use code that uses something stupid like errno instead without noticing. And you get errdefer to mitigate one of the biggest downsides to saying 'I can't handle this here, passing an error up'.

They can't carry arbitrary data, because where would you put it in a language that has an error system capable of treating allocation failure as a handleable error? There's nothing stopping you from putting some state or context elsewhere, but you have to make those decisions.

Similarly you can have the error get handled in a parent scope without unainding as I demonstrated if that's really really important, but it shouldn't be the default.

> You've made error values. This is just one trivial thing you can do with error values without adding keywords. As you said, it's just a function call.

Well, yeah, with some "ergonomics." Like walking a stack of error handlers. Across threads. Go back and read my code; that's what it's doing: a stack walk across threads, followed by a jump backwards depending on certain conditions.

Does Zig do that? Not builtin, for sure.

Could you implement that in Zig? Sure, but then you're outside its "best-in-class" error system.

> This is the error system that languages like zig and rust have.

So you're admitting that Zig does not have any better error handling than C? Because C has return values.

At least Rust lets you use Option values and Result values.

> Ie. the best paradigm for error handling you can get without all thenoverheadnof a pure FP language.

Sigh...I just implemented something from a functional programming language in C. Overhead is minimal.

> Except Zig has a bunch of erganomic enhancements to make them far more pleasant to deal with and reduce the mental overhead and scope for mistakes. You can't get the value out of a failable function without acknowledging and passing up or handling the error, and you don't need the verbiage of if err != nil adding noise everywhere.

That's just manual exceptions with the compiler complaining if you get it wrong. I mean, sure, it's great that the compiler will check your work, but Clang and GCC can give me warnings about unused return values, so...that's not much better than C.

And they are still exceptions. You're still destroying stack frames until some function higher in the stack handles the error. When it does, all of that context is gone.

Using conditions and restarts means that you can make the decision to tear down the context after* handling the error. Exceptions are much less worse in that case. I've actually found them to be useful in certain cases, but only if the error is handled first.

Also, you can add those ergonomics in C. Like so:

    #define try(var, expr) do { typeof(var) ret_ = (expr); if (is_err(ret_)) return ret_; var = ret_; } while (0)
> And it comes with batteries included so you're not going to use code that uses something stupid like errno instead without noticing. And you get errdefer to mitigate one of the biggest downsides to saying 'I can't handle this here, passing an error up'.

I will say that errdefer is a good idea. I did implement basically that.

But no, Zig's error system doesn't come with batteries included because it can't carry arbitrary data. And it doesn't do a walk of a stack of registered handlers. And it doesn't do restarts. And it doesn't have Option or Result, nor interfaces so you could implement the same thing for other types.

Whatever you might think of conditions and restarts, if you seriously think that Zig's error system is best-in-class when Rust's does the same thing and more, while also allowing user code and types to use the same infrastructure, I just don't know what to tell you.

And I'm no fan of Rust. But it's better than Zig.

> They can't carry arbitrary data, because where would you put it in a language that has an error system capable of treating allocation failure as a handleable error?

Preallocate on thread startup. This is not hard.

And on allocation error, every function that allocates is controlled by the language (through the standard library), so that's not a problem in practice. And even if something tries to wrap an allocation failure in more data, and allocation fails again, just panic(). Really. You can even document that behavior in the language.

> There's nothing stopping you from putting some state or context elsewhere, but you have to make those decisions.

Yes, but that's an admission that Zig's error syste...

> And it doesn't have Option or Result, nor interfaces so you could implement the same thing for other types.

It has both of those. The included error types are exactly result with added erganomics (passing up one frame is the easiest thing and is erganomically the same as checked exceptions)

Maybe learn what something is before arguing about it?

Actually, come to think of it, I'm pretty sure this can be achieved with a combination of async/await and error types.

If the inner function yields either a value (or none) or an error, the outer scope handles the error amd returns control to the async function (optionally with state gleaned from the handler) you have all the pieces.

I believe yield lacks any kind of syntactic sugar (planned), but is possibly in current async functions.

Viewing it through that lens, I can see it as a viable alternative to eschewing exceptions altogether.

Adding async to your error handling would just mean that you now have two problems.
That you said that seems to heavily imply that you already agree with the point that I'm trying to make about control flow.

Here's my best attempt at demonstrating conditions and restarts with zig-native tools.

Edit: fixed my dumb. https://zig.godbolt.org/z/creoT47Mj

Note that it'd get substantially more erganomic if yield were finished with all of the usual niceties of the monad-ish error keywords.

Also edit: Upon further reflection to be actually correct conditions/restarts, you'd have to unwind starting from inside the inner function if the handler gave up. This is doable by passing some state back down (handled/unhandled) from the outer scope, and then just returning an error from the bottom scope to begin either unwinding or let a non-restartable error handling mechanism do its thing.

Additionally I don't think this is compatible with also using async for its intended purpose without comptime shenanigans.

No, I don't really agree. I don't like async because of something you accused conditions and restarts of: hidden control flow.

In your example, I don't see any, which is great. But async can have hidden control flow. (I wonder why Andrew Kelley added it when he is so against even hidden function calls.)

On top of that, I don't think async is a good paradigm, but that's neither here or there to the conversation.

That's the entire point I'm making though...exception style semantics are the worst way of handling errors. Algebraic effects make it a little better. Error values are much better as a default paradigm.

Ie. Zig's error system which is not based on algebraic effects is best in class.

Async does carry that same cost, but it comes with capabilities you can't get otherwise and is optional. Any objections to it are a demonstration of my point, not a disagreement with it.

Error values suffer from the pit of failure. If you forget to handle them correctly, you have a bug. See [1].

Exceptions are better than async because they do not "reify" stack frames, an incredibly terrible idea. I mean, it works, but it's not a good model, mentally or otherwise.

[1]: http://joeduffyblog.com/2016/02/07/the-error-model/

> Error values suffer from the pit of failure. If you forget to handle them correctly, you have a bug. See [1].

Then you put your value behind the error in the type...like rust or zig where you cannot ignore them.

You keep describing the upsides of Zig's error system as reasons why something else is better.

> More concerning is that un-named parameters are a compiler error and this cannot be adjusted.

Would you be willing to expand this a bit? This would save you from giving a dummy name in a function?

I think it’s a typo and they meant unused parameters.
Yes, I meant unused parameters. Makes it very hard to do TDD.
I have an editor macro that inserts

_ = myvar; // TODO remove

easily for all function arguments when prototyping.

I agree it can be cumbersome, but I am not sure it is a bad thing.

Not that I write Go anymore, but this was definitely one of those things that regularly made my blood boil. Any kind of experimentation/swapping around data structures always invoked the wrath of the compiler when I am just trying to quickly iterate on an idea. Always felt like it disincentivized improvements.

Should have been a warning. Baked in from day-one, easily makes that a CI check without hampering development.

TS with this lint enabled as well.

When these languages tout fast iteration cycles, yet for a single-line code you have to do a recursive, n-step compilation of everything that might also get unused is literally insane and indefensible position.

(comment deleted)
I agree. Zig is great in many ways but I am also so frustrated by some of the obviously horrible unergonomic decisions the core team makes. To this day, you are forced to declare loop index outside the loop body, despite many requests from the community.

https://github.com/ziglang/zig/issues/358#issuecomment-49100...

I don't understand why fixing issue 358 in the Zig compiler isn't trivial. I don't understand the reluctance. I don't understand why:

    for (var i: usize = a; i < b; i += 1) { foo(); }
    for (var i: usize = a; i < b; i += 1) { bar(); }
Is worse than:

    var i: usize = a;
    while (i < b) : (i += 1) { foo(); }
    while (i < b) : (i += 1) { bar(); }
Which is what happens without the for-loop scoping of the variable. (ie when I cut and pasted, I introduced a logic bug where I forgot to initialize i for the second loop).
Maybe there is a good reason. I'd just like to know what it is. My guess is that fixing issue 358 with syntactic sugar would mean that the debug info would show scope blocks that don't correspond to {} in the source, and that is annoying.
I also see that this is a bike-shed issue [1]. It probably isn't as important as some other language issues that are complicated enough that I don't even know about them. However, people's first impression of the language matter, so fixing the bike shed matters.

[1] https://en.wikipedia.org/wiki/Law_of_triviality

> More concerning is that un-named parameters are a compiler error

I've been ranting about it many times. Most recently: https://news.ycombinator.com/item?id=32145520

There is an upstream issue about it where we should probably get our voices heard, it's encouraging to see many people in that thread that are opposed to that change, but it doesn't seem the core devs are open to considering any alternative, rather they just hope we can learn to live with it.

https://github.com/ziglang/zig/issues/335

This is by far my biggest annoyance with Go, more so than writing if err != nil.

I feel I write code in a different way than other people. Some are very slow, thoughtful and methodical in writing code, while I tend to experiment, throwing stuff at the wall and iterating quickly, and only after going back to polish and refactor. Stopping every iteration during my experimentation phase to lint my code because the compiler is pedantic and stubborn just slows me down immensely and turns me off a language.

EDIT: went and shared my criticism upstream, in a constructive and reasoned manner, I hope. My quixotic fight against unused variables being errors carries on.

I don't think there's any point addressing this through the Zig community. It's self-selecting for people who think this is perfectly fine.

Public discussion of it is much more effective.

Best case scenario, they take notice at the wider world meme'ing Zig for such irritating quirks and realise how many potential users they're losing. An OK case scenario is someone maintains a fork. Worst case scenario is they don't change, no one bothers with a fork, and very few people bother with Zig and general.

Ball is in their court. I would hate for them to cut their nose to spite their face, but I suspect they will unfortunately.

I think this is missing one major difference in the goals of Zig vs. something like rust.

Zig wants to make it possible (and as easy as is practical without ballooning the language) to make a 100% correct program. That is no panics, no UB, no failed allocation, 100% deterministic behavior.

Whereas Rust wants to make 70% or so of bugs that exist elsewhere impossible.

That’s a good goal! A 100% correct program which cannot guarantee a lack of memory safety bugs is not a 100% correct program, though, right?
The absence of a proof of correctness is not a proof of incorrectness, strictly speaking.
Rust still needs unsafe blocks for full generality. Additionally there are many sources of panics that are unavoidable. The memory safety guarantees it makes are also not all bugs. It is equally possible to write a memory safe program in both (although far easier in rust), but impossible to make a panic-safe program in rust without avoiding a large portion of the language.

A guarantee of the absence of certain classes of memory bugs is of no use to creating a 100% correct program if it also comes with a guarantee of panic on allocation failure and standard libraries that are full of panics.

They are for different things. Rust is for keeping you completely safe from the worst things while you build something big like a web browser where the program doing something you didn't want is the worst failure state, and crashing is a mere inconvenience. It is there to replace C++

Zig is for making it possible to make yourself safe from all bugs if you try hard enough. It is for making small things that you understand fully where crashing is as bad or worse than memory errors like the firmware for a space probe. With the addition of some static analysis tools it could maybe replace or be useful in addition to something like Ada one day, and definitely replace C and some hand coded assembly.

How does Zig protect against stack overflow? To make a "100% panic-safe program" you must disallow recursion.
Zig basically does plan to disallow recursion: https://github.com/ziglang/zig/issues/1006#issuecomment-5346...

More specifically, it'll force you to allocate memory for your recursive functions on the heap (and thus handle the possible resulting OOM condition).

Do you disallow function pointers/closures/virtual methods too, since these can be used to smuggle recursion into the semantics without the compiler being able to statically detect it? You'd have to heap allocate every call to a function pointer, which would be very slow (see how much benefit apps get from PGO which for a long time mostly amounted to optimizing virtual calls).
> Do you disallow function pointers/closures/virtual methods too [...]

Maybe? Of these features, AFAIK Zig only has function pointers, and they already don't seem to interact very well with other parts of the language. I wouldn't be surprised if they restricted them to comptime-only usage or made "how much stack space does the function need" part of the pointer type or something.

Disallowing recursion is one possibility (IIRC misra guidelines forbid it).

In some circumstances, static proofs are also possible. IIRC it's still an open problem in zig as to what the possible language features will be while keeping this guarantee, but the goal is to be able to have recursion in some circumstances and still provide the possibility of stack-overflow-safety with a performance tradeoff, and to know when you've stepped out of the bounds where that is possible.

Here's a thread I'm at least a year out of date on https://github.com/ziglang/zig/issues/1006

The general philosophy is to give the programmer tools to know what the consequence of a choice will be as far as that doesn't hamstring them overly. Ie. the compiler can tell you when a stack overflow is possible, and it's up to you to decide what to do about it (or whether it is wrong because you know something it doesn't).

The trouble here is the same problem that affects tons of static analyses: what do you do about higher order functions. I'm pretty skeptical that heap allocating every call to a function pointer/closure/virtual method is going to be fast enough.
What do you mean by saying heap allocation? Do you mean syscall or libc? Zig has a extremely explicit requirement for any allocation. You can have a preallocated region of memory in the allocator for recursion and only move the needle for each “heap allocation”.
Ah, I see, it's a split stack. Well, we experimented with those in Rust for Rust->C calls and they were not fast enough (also, they cause problems with debuggers and tooling). Perhaps if virtual calls are uncommon, though.

At that point I have to wonder what the advantage is over just using explicit checks for overflow on the machine stack though.

(comment deleted)
You're still forcing a mindset onto the language that isn't present.

It's not 'a split stack' or any other particular solution. You pass in an allocator and it does it whichever way your chose allocator picks.

The goal is to explore sections of design space you're completely ruling out from before you even start.

I'm pretty confident that zig will eventually get a static lifetime analysis tool.

Such a thing should be writeable, because a human can reason about code. We also know that for example SEL4 does "sort-of" static analysis on c and gives memory safety guarantees.

This is incorrect in two ways. First, Zig certainly has undefined behavior. Any non-memory-safe language trivially has UB.

Second, this implicitly assumes that terminating on OOM is not "correct" behavior, and handling OOM is the "correct" thing to do. But if there's nothing better that the app can do than to terminate on OOM, then terminating is the correct behavior. Many apps fall into this category. There's also the fact that OOM code paths tend not to be well-tested, so empirically aborting on OOM is a safer default.

There's a difference between a language allowing undefined behavior and a particular program written in that language exhibiting undefined behavior.

The post you're replying to wasn't claiming that no Zig programs have undefined behavior, but rather that it should be be feasible to avoid it in the programs that you write.

(I don't know if that will actually be true, though.)

Well, that weaker sense of "correct programs have no UB" is true for Rust as well, then.
Rust has hidden allocation, no planned static stack analysis or limits on recursion, little to no control over how allocation happens, no ability to guarantee a panic won't happen and thus no ability for the programmer to guarantee eg. essential external resources are cleaned up.

The guarantees that zig seeks to enable (this being the key phrase, it does not guarantee them just by its use, nor has the project solved all the problems this entails) the programmer to provide are non-goals of rust.

Ie. the projects have completely different goals. The things zig want to provide would make rust unusable if it tried to provide them and vice versa.

Rust has had to implement limits on recursion already as part of async. It seems feasible to extend this to other code. And local allocators are also being worked on.
That's cool. Linky to discussions? I can't seem to search it.
You've missed the point in at least two ways.

The goal (not the present reality, although it's closer than most) is for the programmer to be able to get a particular program that is 100% defined behavior if they do everything right. Ie. it's a 'there exists' relationship. It's also an aspiration rather than an absolute (ie. make the tools progressively better for that purpose rather than making it an absolute constraint from day 0).

Memory safe languages forbid certain classes of behavior preventing some bugs altogether. Ie. a 'for all' relationship.

The correct behavior on OOM could also be defined as either checking whether allocation succeeded before moving on or as knowing how much memory you need and getting a guarantee it is available. You could be in a state where unpredicted termination has severe negative consequences (mission critical software, resources that need to be cleaned up). By pre-selecting a particular allocation strategy and response to failed allocation, and making it almost impossible to even know when allocation is attempted without reading every line of the massive stdlib you're making it impossible for the programmer to make that choice.

(comment deleted)
It sounds like aggitation excerpted so, but the write up does a wonderful job setting up for it, making this stunner of a line not seem so bad:

> For many C developers looking to upgrade their language, Rust looks a lot like a better C++, not a better C.

What a line!

The whats-included/what's-not takes me back to one of our legendary/epic (and at 9000 words, epic in length) posts, one I love to cite, Larry Wall's Perl, the first postmoderm computimg language[1]. A cut up early & favorite section:

> When I started designing Perl, I explicitly set out to deconstruct all the computer languages I knew and recombine or reconstruct them in a different way, because there were many things I liked about other languages, and many things I disliked. I lovingly reused features from many languages... To the extent that Perl rules rather than sucks, it's because the various features of these languages ruled rather than sucked.

> But note something important here. I left behind more than I took. A lot more. In modern terms, there was a lot of stuff that sucked. Now, on the feature set issue, Perl is always getting a lot of bad press... I think people who give bad press to Perl's feature set should have more angst about their reporting.

> I picked the feature set of Perl because I thought they were cool features. I left the other ones behind because I thought they sucked.

It's silly & basic plainly said like this, but the post goes on & on about finding creative use & also at length about deciding to leave things behind. Walls postmodernism is infectious. And this element of pruning, pairing down, is one that seems like an axis where elegance & grace can often take root & grow in todays busy, complicated world where so very much has accrued & accreted.

[1] http://www.wall.org/~larry/pm.html

Another thing which makes Zig interesting is it’s approach to allocation, making a failure to allocate memory a catchable error, and enabling developers to easily assign their own allocators.
How can it guarantee that the OS doesn’t just lie about having allocated that space? As far as I know they are unallocated until a write to a given page (by default on Linux at least), and thus any write could potentially fault.

Unless it zeros every page it allocated, I don’t see it being able to defend against it in practice.

Yes, but I think you missed the point of the parent comment.

> enabling developers to easily assign their own allocators

Through a combination of Zig's convention to pass around allocators instead of using a global allocator, the allocator interface's ability to return errors, and the Zig language forcing you to handle all errors (unless you explicitly discard them), a lot of Zig code is written with allocation failure in mind.

This means that you can seamlessly pass a custom allocator, say a bump allocator backed by a fixed buffer, to a function in some third-party library and still handle the case where the buffer runs out of space.

Unfortunately no, there's nothing that can be done about an ABI that insists on lying to you. This is one of the reasons I think the Linux default is a really terrible idea.
I never tried zig but I want to address something the article wrote

> People struggle with Rust for a number of reasons ... the borrow checker!

The borrow checker was the easiest thing for me to learn and the only thing I liked about rust. Except it's broken. I have to constantly reborrow after calling a constant function. The reason I actually bailed on rust is because it's a worse C++ (compiles slower, executes slower, isn't compatible with C's __thread etc etc), it's a worse webserver (I still use PHP and C#), it's worse for database heavy code, it's not very good for one offs (C# and python are better), it basically cost too much to switch for the broken borrow checker

Also the article at the end doesn't make it clear what he thinks. Usually I read the start and end to see if I'm interested in the topic and I'm not sure if this article is in secret a rant

The borrow checker has been through a couple revisions now, and it's got better each time.
I tried it again in 2021 and it still failed to deal with constant functions. Here's it again in 2022 https://play.rust-lang.org/?version=stable&mode=debug&editio...
I don't know any rust but the error description[1] only talks about borrowing mutable from an immutable, which to me seems like an obvious error. However the compiler complains about the reverse condition. Again, I don't know any rust, so it's not clear why that's an error.

So is it a documentation bug or a compiler bug?

[1]: https://doc.rust-lang.org/stable/error-index.html#E0502

I only understand it because I learned how not to shoot myself in the foot with the canon that is C++

I'll sum up "container invalidation". In C or C++ if you append an array you may cause the array to grow which means it might have a different pointer. You basically can't take an address of an array you're planning to grow. All containers are like that except list I think which I never use

In rust, it's roughly the same idea. The problem is I borrowed something from vec (you can think of it as taking a reference or creating a pointer). I then call a function that is const (you can see here it's &self and not &mut self https://doc.rust-lang.org/src/alloc/vec/mod.rs.html#1911). But that invalidates the borrow like push_back/resize/reserve and such in C++. Which is absolutely ridiculous because it's doesn't modify anything.

It makes me so mad because even the one thing I like about rust is annoying to use. Using an annoying checker, slower compiler, harder to optimize language is too much. I don't shoot myself in the foot in C++ so I'm unable to get a win in rust. For me using rust is more pain than practical

Can you share any compiler benchmarks showing Rust compiles slower than C++?

I can see something totally opposite in my projects - particularly recompiling after a change is very quick. And cargo check often runs in less than 1s. I never got such quick incremental recompile times in any of my earlier C++ projects, particularly when using templated or header heavy libraries like STL ;)

As for the borrow checker thing, it is not broken but it's simply a limitation. In a strictly statically checked it is usually possible to run into a situation of a valid program that gets rejected by the compiler. Here this limitation comes from the compiler following a pretty simple rule - only one reference is allowed if it is mutable. This simple rule is what makes many of Rust safety guarantees possible, and I guess relaxing it conditionally might be very tricky. NLLs have already made it a bit nicer recently, do maybe it is possible to improve it further.

> As for the borrow checker thing, it is not broken but it's simply a limitation.

Limitations are when the borrow checker is not smart enough to understand things like sub-scope borrows (fixed by NLL) or the interaction of mutable borrows and loops (which hopefully will eventually be fixed by Polonius aka NLLv2).

But here they want to acquire a shared reference to an object with an outstanding unique reference. That is quite literally the first of only two rules references have:

1. At any given time you can have either a single unique reference or multiple shared references.

2. References must be valid.

In the given example they are not using the shared reference at all during the whole lifetime of the immutable reference and the operation on the immutable one does not change the object at all, so it would be actually safe. This is the situation where those two rules are a bit too strict and reject a valid program.
> In the given example they are not using the shared reference at all during the whole lifetime of the immutable reference and the operation on the immutable one does not change the object at all, so it would be actually safe.

The compiler does not work at that level of resolution. It’s unclear that it should, even for a point query: there is no telling how much a weirdo ISA would bungle that if the two accesses came from different threads, for instance.

> This is the situation where those two rules are a bit too strict and reject a valid program.

Compilers will always, necessarily, reject valid programs. The alternative is to allow invalid programs.

> The compiler does not work at that level of resolution. It’s unclear that it should

The correct solution is to never ever use rust. It's a failed experiment that noone noticed failed

> Can you share any compiler benchmarks showing Rust compiles slower than C++?

You got to do this yourself because YMMV

My projects don't use many includes and little templates. So C++ is more than 4x faster. I think in one case it was 10x faster but other people might claim I'm cheating because most people don't write code that way. If you often write template heavy code or need to include many large headers you'll get different results

There is nothing broken in this example, it's functioning exactly as it's intended to. What makes you think the borrow checker is broken here?

It appears like you don't really understand the rule about mutable and immutable references. Asking for the length of the vector requires an immutable reference, but you already have a mutable reference, which is explicitly not allowed by design (that's the whole point!).

So, you can only have one mutable reference to something if there are no immutable references, and multiple immutable references if there are no mutable references, you can never have both immutable and mutable references to something at the same time.

I don’t know Rust, just wondering: why is it forbidden to know a length of vector to which you hold a mutable reference? What could go wrong?
I imagine you can have one thread modifying the vector while trying to read its length from another thread.
Noone should imagine that. You definitely don't want two threads accessing the same vec or array at one time. Maybe if there's a lock
If it's read only, you can definitely want that, right? As long as the elements are Sync. And it should work (You might have a problem with the cleanup, but Arc or scoped threads should handle that). The problem is when one is a mutable reference, like in this case. AIUI you could have a single mutable reference to an element be used from another thread (For example, a scoped thread) if the elements of the Vec are Sync
The problem is not “a length of a vector”, Rust is not that fine grained. As far as Rust is concerned, what can happen is that you acquire an immutable reference to an object while there’s a mutable (unique) reference outstanding, you can invalidate the immutable reference via the mutable one, and thus have a dangling reference, which is UB.

Hell, even here one could imagine two scoped threads (so they can work with references), one thread keeps modifying the vector, and on some ISAs the other might get torn values for the length, or possibly worse (I don’t know if worse is an option but I wouldn’t be surprised).

OP is just holding a reference to a single entry in the vector. You could write whatever you want to that entry, but it's not going to change the length of the vector. In some sense the problem is that Rust is not "fine grained" enough to figure out that OP's access pattern is valid.
It's not just about that. What rust sees is you are calling two functions: the len() function with a shared reference, and the indexing operator implementation for the Vec's [] (Which is just a function) with a mutable reference, which returns a mutable reference. The compiler does not look at the actual implementation of these 2 functions to figure things out, it only looks at the signatures (If it did look at the implementations, it would probably be slower, and what's worse, the caller's code could stop compiling if you change the implementation! That would be specially bad across a library boundary). The implementations are irrelevant AIUI to the borrow checker, only the signatures matter.
>I have to constantly reborrow after calling a constant function.

Not sure what you mean by this, do you have an example?

Not sure? I wasn't aware people didn't get this problem. Maybe its the domain I'm in. Uncomment to fix. I have to repeat myself all the time and it's obnoxious (IDK how to show code on this site but heres a link https://play.rust-lang.org/?version=stable&mode=debug&editio...)
Oop, I'm aware of this, it is very annoying, I think I just hit it just the other day; just had no idea what you were referring to from the description.
What do other people call it? > 50% of the time people don't know what I mean until I show code
Good writeup. Also I enjoyed your podcast back in the day!

My current thinking is that if you want to make a "small" language with anywhere near the guarantees of rust you need to make the features smaller too. A good start is cooperative multitasking, aka basically the async model, to radically simplify concurrency. Communication between processes must then be done with channels. Another is memory allocators, I think arenas are a good start, and I'll have to look deeper into Zigs approach.

I think one of the possibly-good moves Zig makes, similarly, is using `comptime` for generic types. I don’t have a good sense of how that interacts with parametricity and ability to constrain those (and I suspect that’s an important question!) but it’s an interesting example of a possible trade off that lets the language size space.

(And: thanks!)

Rather than traits like what Rust has, the approaches I've generally seen for polymorphic behavior are explicit "interface" structs that hold a vtable, or comptime "duck-typing". I'm still rather new to Zig so there may be other approaches, but this is what I have seen so far.

One positive side effect is that there is only one "language" to learn as opposed to two different syntaxes like Rust does. While most people only use a very constrained part of the Rust type system, at the end of the day it is Turing Complete[1] (or possibly close to it? I don't know enough theory to know the rigor of what constitutes Turing completeness) and trying to keep all of it in mind can be taxing.[2]

[1]: https://sdleffler.github.io/RustTypeSystemTuringComplete/

[2]: https://hirrolot.github.io/posts/why-static-languages-suffer...

I'm not sure exactly what is meant here, but you just have the whole language available and you can express whatever constraints you want on the types of things.
The problem is that you have to do that by hand, and thus it will be possible to get out of sync with the actual requirements of the generated code, leading to C++-like template error messages that have to include the entire stack for you to make sense of what went wrong.

Or in other words, there are two parts to checking constraints: checking that the caller satisfies them, and checking that the callee doesn't accidentally rely on anything beyond those constraints. Zig's approach only gives you the first one.

(comment deleted)
I don't know much about Zig, but it shares a problem with Rust in that neither have a built-in REPL.

I'd like to learn Rust, but every time I try to, I am a bit overwhelmed by its complexity, both in syntax and overall feature set.

Do you mean you want an interactive environment to just write small code samples while learning? Or do you want to do your development after learning in a REPL? I think those are two very different goals.

As to Zig and Rust’s lack of REPL, they are both ahead of time compiled to executable languages. It is not super common for REPLs to be developed with any real frequency or faithfulness. (this is a little different in the functional world for some reason, Haskell, ML, etc have very thorough REPLs, and Koka’s REPL is great).

REPLs make a lot of sense in the functional world where everything is an expression. These languages tend to encourage writing lots of small, referentially transparent functions, which are easy to write and test in a REPL. Because the referentially transparent, when you're happy with them, you can copy what you wrote directly into your code.

I think you don't see it used as much in the imperative world because each line of code tends to depend a lot on context that isn't immediately available. When you make a mistake in the REPL, you probably altered state, which means you have to run everything again from scratch to know if your corrected flow actually works. If you're going to run everything again anyway, you may as well just be working in a file with a proper text editor and a good test framework.

(An exception to this might be Ruby, whose Smalltalk heritage gives it a good interactive programming legacy to build on.)

Yet strangely Vlang has had REPL since at least back from 2020 (https://youtu.be/Pvbv4b9TibA), a year after the language was released. Don't think it's as impossible as it is being made out to be here.

Just like comparatively, Vlang had a self-hosted compiler the first year it came out, but Zig did not achieve this until this year (which is around 6 years after its release).

Interesting perspective. I’ve never used a REPL much. I did a bit of commercial Python and rarely used it there and I built my career on PHP, which kinda has one, but not a very usable one. I found the REPL to be a distraction. I just want to write code in my favorite text editor and then interpret or compile it.

I’m a bit old school, I realize.

REPLs are awesome, but I have to admit that interactive notebooks are even awesome...r.

For Elixir, I use its REPL called IEx frequently, but it can be cumbersome iterating on functions, especially since Elixir requires functions to be in modules and the REPL does not define a top-level module like some other languages' REPLs do. Although Elixir does allow one to recompile modules without having to restart IEx. But I have found that using Livebook, Elixir's interactive notebook application, is much more efficient for creating test code or prototyping modules. Then I can just copy and paste the module and/or functions back into my Elixir project. Livebook also allows you to attach Livebook to a Mix project.

.NET Interactive, which is a polyglot notebook and supports F#, is what I use for F# development as well. I personally feel any modern language needs REPL and notebook support, of some kind.

Notebook technology is pretty underutilized in my experience, particularly for learning and documentation.

Rust doesn't have a repl (well it kind of does: the rust compiler slack bot that runs code you ask it to...), but if you use rust-analyzer the feedback cycle is just as fast. There's also https://play.rust-lang.org/ for testing short bits of code in a persistent editor.
The evcxr project https://github.com/google/evcxr is implementing what amounts to an experimental Rust REPL. (Though there's inherently something a bit weird about a REPL for C/C++-like languages, where the phase separation between compile- and run-time is so inherent to the programming model.)
AFAIK good live coding support (e.g. hot code reload) is a goal for Zig which arguably is much more useful than a REPL for a compiled language.
I use godbolt as a repl for both Zig and Rust. I cannot recommend it highly enough.

It's not a true repl, but it evaluates your code on the fly and shows the generated assembly. It's useful in a lot of the same ways a real REPL is.

I work with garbage collected languages all the time. Do you know how much time I spend tuning garbage collection and worrying about its performance? None whatsoever. Honestly, it's not a thing and it's mostly fine.

GC mostly just works really well. Occasionally you get some poor performance and then you find that you are doing something silly and then you fix it and move on. Or you throw more hardware at the problem.

GC has a bad reputation but the reality is that there is a lot of software being written with languages that aren't exactly famous for even having a particularly good garbage collector. Python, ruby, go, etc. They are alright but certainly nowhere near best in class when it comes to garbage collection. If that matters to you, you can pick python and ruby implementations that have way better performance than the default implementations provide. But the thing is, it does not matter. It's genuinely not a problem for people using those languages.

I\ve used each of those languages and also use Kotlin both on the JVM and in the browser. So, you'd think I spend a lot of time obsessing about what the javascript garbage collector does in the browser or how to tune the jvm? Nope. I just let it do its thing. We have no garbage collect related issues. So why spend time on it?

My observation is that engineers love talking about optimization but rarely realize how expensive they are relative to just throwing more hardware at a problem. And the thing is, some modest hardware goes a long way these days. We run our Spring/Kotlin JVM servers on cheap vms in Google cloud and in another data center using openstack. 2GB and one VCPU. About 25$ per month. That's around 10 minutes of my time at my freelance rate. Those are European rates, not Silicon Valley rates. Those are more like 3-5 minutes. We could go cheaper and I'm pretty sure I can make it run with half an GB if I work at the problem. But the savings are negligible and I like a little safety margin. We have many issues but our hosting bills are pretty awesome. Cutting them in half is literally not worth more than 1-2 hours of my time per month.

I think it's great that C/C++ are getting some competition from modern languages now. Rust has a pretty innovative borrowing mechanism and it seems to genuinely be better at certain things that have been the exclusive domain for heavily optimized C/C++ systems for decades. That's great. Zig seems like the more reasonable choice for people that just want something simple that works that isn't ancient and awkward like C but gets you ballpark in the same league in terms of performance. I learned C a long time ago and I love Linux. But the never ending exploits and security problems are a thing that I just don't need in my own projects. Having a garbage collector and some other safety has a tiny cost but eliminates whole classes of bugs and exploits. Completely worth it IMHO.

I don’t disagree that various automatic memory management strategies can be very effective and efficient along various metrics. However, there are cases where ‘throwing more hardware’ at a problem is not a viable solution. Typical examples, that I think are broadly understood and commercially available, are soft-real-time simulations with advanced graphics and audio (fancy words for video games) and audio creation/performance/recording. These two areas are typically bound to be using a large portion of consumer computing device resources and GC is typically just too much overhead in certain performance metrics.
My company offers a data feed over WebSocket that could reach 70MB/s if we allowed you to subscribe to the entire feed at once (we don’t). No one ever planned for it to handle that much data, but here we are. Because WebSocket servers do not scale horizontally we had to consume the entire feed on one machine. (Edit: ofc you can scale the # of clients handled, but not the data throughput!)

Initially our Go app could only manage reading 10MB/s from Kafka, which is pretty sad. After switching from Shopify’s “Sarama” Go library to librdkafka (a Kafka client written in C), as well as some other arcane optimizations, we were able to 20x our throughput and handle up to 200MB/s, despite the overhead of interop between C and Go. On top of this, we used a library that wrapped librdkafka in Go bindings (confluent-kafka-go) and it turned out to have a much simpler interface than Sarama, in addition to it outperforming Sarama 20x.

While we didn’t end up rewriting the thing in Rust, we did end up leveraging a library written in C for major performance wins, in a case where scaling out was not possible. And let us also remember that even when scaling horizontally is possible, it is not necessarily even desirable in all circumstances: distributed systems, aside from introducing some amount of overhead, are basically Pandora’s box wrt. failure modes.

Go is nice but it certainly has its limitations. I've seen heavily optimized jvm based things run circles around the kind of numbers you are talking about. 70 MB/s is not a number that should scare anyone running things like Kafka at scale.

I've worked with Elasticsearch clusters indexing close to a million documents per second from kafka. That takes a lot of CPU, memory, bare metal, etc. and it taxes the JVM garbage collection quite a bit. But it can be done. We basically maxed out CPUs across 30 or so nodes. Basically, when you get close to maxing out the disk IO, you know you are doing a decent job.

People reach for C for a lot of reasons. And not always the right reasons. But if it works, it works. A big reason seems to simply be that interfacing with it from various other languages is pretty straightforward. That's why python developers don't worry about performance. They can always plug in something native if it becomes an issue.

Again, scaling out to 30 nodes was not an option for us. It had to run on a single machine, unlike the rest of our data pipeline. While 200MB/s is nowhere near the theoretical limit of our hardware, it is near what Kafka’s stress testing tools achieved in the same setup, particularly with our record size (100 bytes).
I am quite sure those arcane C optimizations are also possible in Go, it is a matter to learn how to properly use value types and the unsafe package.
I meant that we did optimizations in Go, on top of using a Kafka client written in C. When language constructs like channels degrade performance, and when goroutine scheduling takes 20% of your cpu time, it begs the question of whether you’re using the right tool for the job. If you are using unsafe to thwart the garbage collector, this is probably even more true.
Same reasoning applies to C when using inline Assembler then.
You are not wrong of course. But it's interesting to see many commercial 3d gaming engines use things like Lua, C# and other garbage collected languages as the main way for game developers to use their engines. I grew up in an era where all that stuff was done in assembly. That stopped being a thing decades ago.

The vast majority of commercial software development can use off the shelf native components and pair them with garbage collected languages these days.

For what it's worth, I totally agree with you that GC is a perfectly reasonable memory management solution for most apps. "GC is bad" is not the takeaway anyone should have from new systems languages like Rust and Zig.
Thirded. I think “pick a GC lang” is an eminently reasonable default stance! I use JS/TS all day every day and am on record on the main ongoing Rust podcast just a few weeks ago saying most folks shouldn’t be teaching for Rust just because. There are things which require it, though, and cases where even if it isn’t a hard requirement it makes a big difference in experience whether you have to boot a VM or not! I also think there’s a high value to really great performance even in cases which don’t demand it, but in those cases it’s really a matter of whether and how the productivity trade offs work in your specific context with your specific team.
But zig can also use a GC if you want that as an allocator. As a proof of concept, libgc bindings were made by someone for zig. Very easy to use. Zig just allows you to use whatever memory management technique you need for the problem at hand
There are applications where you cannot have a garbage collector. The software on the PLC that controls when a saw stops itself and doesn't slice your finger. This would be horrible if a GC pass happened during this. I don't think realtime rust is a particularly fun idea, either, though you could certainly give runtime limits at compile time I suspect the macro system will make debugging timings on such code difficult if you ever need to use "other people's code"... One reason why I'm unsure about crypto in rust.
Same here, specially since many GC languages also offer the capabilities to deal with value types, and if it really needs to come down to it, manual memory management.

Somehow it gets lost in the discussions as everyone associates their GC experience with Python or whatever.

(comment deleted)
(comment deleted)
I am working with zig for embed code and it works well. It's mental model is easier than rust and it's easy to use static memory with std lib.

Also I had a project where I could not get cross compilation to work and compiling on the system itself was too slow in rust.

As I said earlier, both language can thrive and they both have a place in my toolbox.

How is it looking on embedded?

The real make or break for me is debugging / GDB. I’ve assumed that a debugger will be able to point to and step around native Zig code, now or eventually. But I wonder how it would work with C code you bring in? If that could be debugged seamlessly along with Zig code, that would really be something.

Can't speak specifically for the embedded use case, but when debugging my mixed Zig/C/C++ projects in VSCode with CodeLLDB or the MS C/C++ extensions (which are frontends for gdc and lldb on Mac and Linux) I can just step from Zig code into C code and back (the same works for other compiled languages that support DWARF debug output btw, there are no 'language barriers' in the debugger).
Well that sounds like the dream!

I haven’t used CodeLLDB, but if it’s GDB access and display is as good as what I get with STM32CubeIDE (Eclipse + CDT) then it really sounds like it’s getting there.

It works but there are still some progress to be made, especially on low memory chip when some vars gets optimized out and you cannot inspect them without GDB jutsu involving manual address.

Navigating source alongside C works fine.

I'm a decades-long C hacker and I'm not sure why I should use Zig anywhere that I use C. What's the killer feature/reason?
A few big benefits off the top of my head:

* A concurrency mechanism that's very easy to use, because it doesn't have the function coloring problem of async/await.

* Extra assistance with memory safety, via its "release-safe" mode. Not a silver bullet, but it does help!

* Good tooling; zig cc can cross-compile from any major OS to any major OS.

* Comptime, a pretty brilliant system that does what generics does for other languages, with a fraction of the complexity.

I've never had a problem with concurrency in C, really. async/await in other language is fine and all, and I've had my fair share of them there, but I wonder if the difficulty in reasoning about them is worthwhile wherever I use C. I'm more likely to want something declarative and explicit than async/await.

Again, memory safety has never really been a problem for me where I use C; and if I want it, I'd want something as strong as Rust's borrow checker or at least the smart pointers of C++.

zig cc can't cross-compile on all platforms, particularly not esoteric and proprietary embedded targets or game hardware, and so that's a nothingburger for me.

Zigs async/await is not really a concurrency construct (though you can use it as the basis for a work stealing scheduler), it's something much more fundamental and close to the metal.
From what I hear 1. No headers (you can use headers to import C functions. None needed for other zig functions). 2. defer 3. Comptime
AFAICT, comptime is the single most interesting feature.

Tagged unions _kind of suck_, and the alternatives are preprocessor abuse, _Generic, or similar. Just not as appealing as comptime, really.

So are you still unsure? Since you said from where you use C headers would absolutely top a reason for me. I hate that bs gruntwork
Among all the other suggestions, I appreciate the refinement to the type system that zig has over c. Like bools and ints are distinct types, built in slice types to associate a pointer and length, arrays have a length, well-defined integer sizes (i32 for example), optional, errors, etc. it’s little things like that which make a language much easier to use in the long run.
C has an enormous weight to it. If you've been carrying it for decades, you are likely somewhere between being so used to carrying it it's hard to notice the burden, and being so at ease with carrying it that it's not actually a burden. Imo, Zig doesn't have /a/ killer feature. Instead it has hundreds of small design choices made with the insight of decades of programing language evolution, along with a few clever ideas on its own.

I would say it's well on the way to becoming the best language for giving you control as a programmer over the computer. Compared to say Rust focusing on safety first, execution speed second, ergonomics third; Or Go on maintainable systems at large corp scale.

To list some specific examples:

   - No headers.
   - No complex build and linkage systems.
   - Fast compilation (and no explosion of compile time as headers import headers.)
   - A small and distinct set of fundamental types, with true enums, unions, and arbitrary size integers.
   - Code formatting, which is a feature that feels kind of invisible, but means arbitrary code you look at will be formatted in a familiar way.  A subtle but powerful difference.
   - It doesn't try to remove undefined behavior.  Instead it does a really good job of catching you hitting it when building in a development version.  Imo, often code wrote simply will have undefined behavior that is meaningless because it will never be called with that behavior.  It's fine to code that way in C until you accidentally hit it and have a hell of a time figuring out why something isn't working.
std.ArrayList

std.HashMap

std.ArrayHashMap

std.MultiArrayList

std.heap.ArenaAllocator

defer/errdefer, try, catch

Debug mode: stack traces, safety checks, segfault handler, error return tracing, valgrind integration, undefined bytes set to 0xAA, GeneralPurposeAllocator finds leaks, double frees, and use-after-frees

Comptime instead of macros

Unit testing

The killer feature of Zig is polish - not yet in implementation(teething issues still abound if one tries to take it into production) but definitely in design. It goes down the list of every language wart or make-do tooling situation in C and addresses it with a reasonably specified and generalized feature, without making the language much bigger(if it is at all - it's not a big language spec). It actively works on some really grody systems-level problems with building binaries that most languages will address with "compiles to C" and C itself addresses with "fight the build system" - Zig aims to be a one-stop shop that clarifies this process. And it integrates C code compilation into its build system, so existing projects can be migrated.
I’ve only dabbled in Rust, I’ve read about Zig with earnest interest but no bandwidth to even dabble. My primary experience is with higher level languages. It’s quite possible my intuition here is very wrong, but I’m sharing it because my experience with Rust’s lifetimes felt so awkwardly unnecessary to me.

Lifetimes for memory make sense if you might modify memory. What if you don’t? My sense of a “smaller Rust” from my time exploring it was “why does everything assume it has a longer lifetime if I’m never going to access it again, if I’m never going to grant access to any other function, as soon as my function exits?”

I’m coming from a functional programming perspective, which I thought I’d found when I started with Rust. But it seemed to want to guard me from things I wasn’t even trying to do, in anticipation that in some unrelated part of the codebase I’d suddenly abandon all of the values I’d been respecting and start interrogating into their bits.

What if the “smaller Rust” is just “Rust with lexically scoped lifetimes unless you say otherwise”? I can imagine a borrow checker that defaults to the generally FP semantics of Rust’s syntax, rather than defaulting to the generally DSL-over-machine-code that’s typical of low level languages. I’m not sure what the language and implementation trade offs would be, but my instinct with Rust is that it would be simpler for most use cases, and not much more complex for the cases closest to C.

I don’t really see what you mean, perhaps you tried Rust many years ago?

Because for stack-allocation there is zero lifetime annotations, it just works with as high-level code as you would get from a managed language. Rust’s complexity happens when you want to introduce non-tree-like memory patterns.

Anyone looking for a pleasant systems language check out Nim.

It’s the most fun I’ve had programming in a long time!

I like Nim. WASM is not natively supported by the core team, and the biggest of all. Cyclic Imports. Rest, its a great great language.

If these two issues are resolved. I am all in for Nim

> a C developer might say. “But Rust is still a very large language, and only growing larger over time.” The C developer would be right.

Nice article. As someone who uses Rust on embedded, this is a concern I share - especially regarding async/await, which this article mentions specifically. I see my (and for others of a like mind) path forward is using an explicitly-defined-at-project-level subset of Rust... In the vein of what we see in C++. This use of a dialect has drawbacks for newcomers to a code base, but I prefer it to the alternative of not mitigating the downsides of a large language.

Perhaps this prediction won't turn out to be true, and the Rust language will slow its expansion.

Other than banning some universally agreed upon deprecated feature from a project, I don’t see these dialects worthy. The rules also often fail to change with times, often causing the detriment of the project if anything (thinking of some of these rules making C++ projects stuck pre-C++11)
It also makes a lot more sense in C++ where each feature can interact with every other feature in non-obvious ways, meaning you need to know which features are used in order to know how the program behaves.
Why all the new languages aren't white space based lang like Python ? Why they still need braces , braces are always hard to read the code and all modern code editor can work well with white-space based languages .

A native , non inerpreted , high performance whitespaced lang with memory safety , as simple as python would rule .

I find whitespaced code harder to read. Autoformatters like Prettier make brace-based langs really easy to read and parse.
Zig has a canonical format where braces match indentation.

You could probably make some simple tooling such that you never saw braces.

That's entirely subjective, there are a lot of people who have the same negative opinion about 'whitespace matters' languages. I switch between Python and 'curly brace languages' all the time, it's really not an issue after the first three hours or so (but I still miss sometimes that I can't throw in temporary debug code into python with different intendation so that it stands out).

Same with semicolons btw, some people make a big fuzz about it, but in the real world it really doesn't matter, especially with how common real-time linters are nowadays (which helps me a lot because I sometimes accidentally end a line in python with a semicolon).

Because code formatters cannot work on whitespace languages like Python. They can't adjust the indentation for you because they have to assume that the given indentation is intended.

It also makes a lot of other automatic tools for dealing with source code harder to write, if not impossible.

This is simply not true.There are highly-used and highly-usable formatters for Python, Elm, Haskell, F♯, etc., all of which are whitespace-sensitive/indentation-dependent languages. "Adjusting indentation" is far from the only thing formatters do, and even things like breaking or wrapping expressions over multiple lines can certainly be done in all of these languages.
adjusting indentation may be "far from" the only thing your formatters do, but it's the only thing i ever want mine to do
I think you might have got that flipped around. I've known many more people that found white-spaced based languages harder on the eyes and more annoying to deal with.
> Why they still need braces , braces are always hard to read the code

I'm of the opposite opinion. I very much prefer braces or some other block delimiter over having to slap a ruler on my screen to know which scope I'm in.

Reading this, I can totally see why I fell in love with Golang. The language is so dead simple that it's generally a pleasure to read any codebase, and very fast to learn/write.

That being said I write mostly in Rust these days and really like it too. There's definitely a number of issues in Golang that I wish they would address (sum types for example)

I liked the article, and as an outsider to the discussion (I do embedded C for a living, and C++ as in "C with classes") I might add:

What I've seen so far is that Rust tries to lure C developers with a sort-of "memory-management shaming". I've even been called "old curmudgeon" here in HN for not showing interest in Rust, and not using it right now. I think if hear one more time that "70% of bugs are memory bugs; here is the link" I'm going to explode. For which I answer: "My applications have a button and a display, what exploits are you talking about?".

Spoiler alert: "memory-management shaming" doesn't stick with us. C is vast and has tens of millions of applications. Not every project is wired to Ethernet, BT or WiFi. If there is a bug, it might be hard to find, but that's it. Hey, there is people that even pays you to solve problems you've generated yourself. What's better than that?

Luring C developers is about something else and Rust doesn't get it yet. At the same time Rust tries also to lure millions of high-level languages developers with features like async-await, super clever one-liners, package management, etc., which personally as a C developer, I don't really care.

You can't make everybody happy. Make happy either C developers or JS developers. Pick one.

Another note: even if the language it's been there for a while, I often see "hey! I've built a build-essentials replacement! It only works like the 20% of it". You go to see the repo and it's been idling for the last 6 months.

So even the Rust's momentum is already starting to decay, there are really just a few of top "99% complete" reference projects. What does it means?

Zig on the other side, shows itself as a more humble language, with simpler syntax. I don't really like the syntax but it's not a big deal. Zig has, or at least, it users don't show bigger expectations. It's taking a careful and slow road and I like that very much.

That said I'm following both languages very closely. I only hope Zig doesn't become as bloated as C++ and Rust today, to be really in that sweet spot I'd like for embedded.

I'm more or less in the same professional spot as you.

Responding to your criticism of Rust all I can say is that you can only convince yourself, try it, if it isn't rhetoric you're interested in. Suddenly your code, when it runs, is bug free. It's a magic experience. More importantly it gives the ecosystem superpowers. That's an indirect response to your:

> If there is a bug, it might be hard to find, but that's it.

Which is damning. But typical of embedded. It's such a C shit hole. We debug most of our time, many of the tools are crappy. So you perhaps constrained yourself to 32-bit ARM devices, isolated yourself as much as you could, and created your own comfort zone? That's a symptom.

There are many advantages to Rust, that run from things as basic as assuring code runs the same on a 8-bit MCU as it does on a 64-bit PC, to the trust one can have in the ecosystem, and the hive mind behind it.

That said, though I see Rust becoming inevitable because of these reasons, I would very much like Zig to succeed, as it uniquely has the flexibility to construct the optimal solutions embedded craves for. Rust is more rigid in that sense, so Zig appeals more to the creative of minds.

I'm not so critical of the language itself (it's just another language), but how it's managed and promoted. I'm also critical about some of it evangelists. For example, from your comment I can learn I work in (or with?) a C shithole, that I don't want to try Rust (which I am learning, at my own slow pace) and that I don't want to use Rust because I have a confort zone, and "a symptom".

Tell me, how on earth I am going to be motivated to learn Rust, by reading something like that?

> We debug most of our time

I can't remember the last time I chased a memory bug of some sort. On embedded, to me, debug means chasing a logical bug or an internal peripheral not working, or finding workarounds for some external peripheral that is not working or answering as it should. So it really feels bad when I read the following sentence:

"is bug free. It's a magic experience"

What are you talking about? Perhaps free from "memory bugs"? As I said, I rarely deal with memory bugs.

> your own comfort zone

I develop and maintain 8, 16 and 32 bit projects, from Cortex-M down to MSP430/STM8. Also I'm in charge of embedded Linux and Android customization and builds, 32 and 64 bits (former Windows CE developer). I wish I had a comfort zone... sometimes.

We get that Zig doesn't have memory safe features. How much of a big deal is that anyway - what proportion of issues are caused by memory safety errors? And as an aside - how many Rust codebases use unsafe code?

(I don't know the answers to these - I'm a humble C programmer who intends to look at Zig soon).

> The Chromium project finds that around 70% of our serious security bugs are memory safety problems.

https://www.chromium.org/Home/chromium-security/memory-safet...

Similar observation at MS: https://msrc-blog.microsoft.com/2019/07/16/a-proactive-appro...

> ~70% of the vulnerabilities Microsoft assigns a CVE each year continue to be memory safety issues

That’s actually an understatement, on the graph with this caption 70% is closer to the low ebb / minimum in a given year (2012, 2018). During “bad” years (2009, 2014) it climbs as high as 80.

It's worth noting when talking about how Zig isn't "memory safe" that comparisons to C aren't very illustrative because C almost goes out of its way to let the programmer shoot themselves in the foot. Zig was designed with a lot of features to prevent the kinds of errors C damn near encourages.
For a couple of years, I’ve been thinking about whether there could be a simpler language that gets rendered into safe Rust the way Nim is translated into C and compiled as C. I’ve concluded that it is possible, but only if there’s a substantial chunk of AI sitting “in between” that analyses the code you’re writing in the simpler language and queries you, the coder, from time to time.

I’m not sure how coherent I can be describing what I have in mind, but for example: let’s say you’re happily typing and the AI chimes in to say to the coder: “Hey, it looks to me like you may be assuming, in what you’ve written so far, that your process A will always end before your process B. Maybe - maybe - in the real world that’s so. But I can tell you that logically it ain’t so. What Rust code do you want me to write, here: 1) one that just goes from your process A to your process C whether or not your parallel process B has finished or not, or 2) Rust code where B always waits for A, A always waits for B, or both wait for each other, or 3) unsafe Rust code that leaves this possible race condition intact ‘cause your sure- you’re willing to bet the company - that not just this version of the program, but all possible future modifications of it, are never gonna encounter a real-world instance of B being delayed? And do you want any telemetry (asserts) (”fries”) with that?”

Exactly how the AI can borrow check without you doing much if any borrow checking is maybe harder to describe; but might involve informing you that the code you’ve written so far allows simultaneous updating of a variable, and what would you like to do about that, if anything? Would you like some telemetry to let you know if such nonsense begins to happen during a run? (Fleshing this out may be non-trivial.)

The AI would also bring stuff like badly formed loops allowing buffer overwrites to your attention, of course.

I’m quite sure that creating such an AI programming helper/translator into Rust would be difficult. But I suspect it’s possible, and I want one. I’m retired, I won’t be building it.

See verdagon's very interesting comment here (and the replies to that) for what's already being done at least a little along the same lines.

My problems with Zig:

- the language ergonomics sucks

- syntax to play with arrays sucks (zeroing for example)

- lack of operator overloading for my math types

- too many repetition and casting, the compiler is not smart at all

- slow build speed

Things i love:

- build system, all in zig

- comptime

- module is basically a struct, so you can wrap your stuff in a file and import it as a struct, lovely

- the language remains simple enough to pickup, learn and to build any kind of software, it is enabler

Overall it's way too rigid, i miss the fluidity and liberty of C

It'll find its niche, it'll serve as good build system, but it definitely not a good contender for a C alternative

I couldn't care less about the illusion of memory safety with rust, chasing that language is a waste of time imo, and it's very dangerous, we'll see ton of new developers thinking their software is safe because the borrow checker didn't complain

> ... and it's very dangerous, we'll see ton of new developers thinking their software is safe because the borrow checker didn't complain

Would new developers be capable of writing secure C?

No, but that's not the point of C

The problem is Rust advertise itself as a safe language due to the borrow checker, wich is a false and dangerous claim

There are different definitions of safety

If what you mean by "safe" is "memory-safe", then you're wrong. It is definitely truthful to say that Rust code is guaranteed memory safe (within safe code sections, barring compiler bugs, etc).

If what you meant by "safe" is "bug-free", then it never claims to guarantee that your code is free of bugs- that would be nonsense (though it does a lot to help out!), and I don't think people would get that idea.