Zig's toolchain work is continually impressive. While I still don't plan to write software in it, given that I believe memory safety is table stakes, all of this stuff is very, very good. Before the incremental work, it was the toolchain and cross-compiler work. The toolchain stuff has continually been fantastic. I'm very curious to see what they come up with next!
> Semantic analysis is the most difficult part of the compiler to handle incrementally. Perhaps unsurprisingly then, this is where language design starts to matter a lot: while I am pretty confident that most modern languages could support incremental compilation similar to how we do, certain design decisions can make that much more difficult. Zig has had its design tweaked over the years (sometimes controversially) specifically so that it is easier to support fast incremental compilation.
This is something I wish that we had done with Rust. It is impossible to do all of the things at once, though, and we already had a tremendous amount of things to do. This is also part of the "when do you ship 1.0" tradeoff; for our goals with the language, 2015 was the right moment to launch, but if had a few more years to bake things, maybe we could have made compile times way faster. Software engineering is hard.
pron 1 days ago [-]
> I believe memory safety is table stakes
I'm not sure what that means. Java lets you do many things programs may want to do in a memory-safe way but not everything. Rust lets you do fewer things than Java in a memory-safe way, but more things than Zig. Zig lets you do fewer things in a memory-safe way than Rust, but more than C. So among these four languages we already have four levels of memory safety, none of them is 100%, all of them give up something in exchange for what they offer, and different programmers have different preferences for the compromise they prefer, and even that preference is context-dependent. Which of those less-than-100% memory safety compromises is the table stakes? And given that all of these compromises require something that could be quite substantial, depending on the circumstance, in exchange, and consequently programmers with the highest level of knowledge and expertise choose every one of those four in different situations, to me it seems pretty obvious that none of these is "table stakes".
steveklabnik 1 days ago [-]
I'll say the same thing I said to you as I said to Andrew, last time he and I talked about this: the way that everyone talks about memory safety (with maybe two exceptions, one okay (go) and one I dislike (fil-c)) is that "memory safe language" is about there being a clear delineation between what is memory safe and what is not, and that the unsafe aspect is a superset. Rust and Java both are memory safe, except where explicitly demarcated as not (unsafe in Rust, JNI or sun.misc.unsafe or whatever in Java). Zig and C have no such separation. When I (and others) talk about wanting memory safety, this is the important aspect of the design. This is what enables the "I know statically that a large part of the code is safe, and I also know where to check if something goes wrong" aspect of things.
mi_lk 1 days ago [-]
Would you mind sharing some thoughts about fil-c? AFAICT its claims mostly check out so besides implementation details (GC?) it seems directionally good.
josephg 20 hours ago [-]
My main problem with Fil-C is that it combines the worst parts of C with the worst parts of a GC language.
In a language like C (or Zig), you need to manually manage memory. This makes programming a lot more complex, and it's really easy to accidentally mess up. Especially in large projects which have a lot of separate modules.
The biggest advantage of using a garbage collector is that you don't have to think about freeing memory. The GC automatically frees objects when they're no longer referenced. This makes programming much much easier. The downside of using a garbage collector is that it hurts performance at runtime. GC languages are slower and use more RAM.
Fil-C is the worst of all worlds here. Like C, it forces you to manually manage your own memory. But you still pay the performance cost of having a runtime garbage collector. And that cost is (apparently) really high. The only performance numbers I've seen showed ~2x worse CPU performance and ~4x worse memory performance. There's no way Fil-C can compete with C, Zig and Rust for performance.
So with Fil-C, you have a language that's much slower than C, and much more difficult to program in than C#, Java, Go or Typescript.
Fil-C still has some wonderful uses. Fil-C could be a fabulous debugging tool for C programs. It could be a wonderful teaching tool if it had nice visualisations on top of the GC's view of the world. And it could be a great way to run legacy C code.
But it's not a "rust killer". Fil-C programs run too slowly to be able to compete head to head with rust. And Fil-C doesn't offer the language benefits of a GC that you get in C#, Go, and friends. It seems like a really bad deal.
archargelod 20 hours ago [-]
> GC languages are slower
This is not necessarily true. It depends on a language, e.g. Go is slow, Nim[0] is extremely fast with conventional GC and slightly faster with ARC/ORC[1].
GC programs can be faster than manually managed ones in some cases. It's just manual memory management gives you more control of where and when free is called. And a good type system is a privelege that gives Nim more control with destructors.
Another scarecrow of safe languages is GC pauses, which is also not a thing in Nim, see table in [2].
You provide no trustworthy benchmark for your claim that Go is slow, or that nim is faster than that. Many engineer hours have been put on the go GC and compiler
10 hours ago [-]
archargelod 9 hours ago [-]
You can find a number of benchmarks with similar results, but I really like this one[0]. I didn't check all algorithms for differences, but just looking on mandelbrot bench - both are version 1 (without hacks or some manual loop unrolling thrown in) and both are implemented in semantically identical code. Nevertheless, Nim version runs 7x faster and, literally, on par with C.
I haven't seen data that would confirm that the Nim ARC/ORC approach is faster than manual memory management (C/Rust/Zig/...) on programs with non-trivial lifetimes (for trivial lifetimes, Nim elides RC). I'd also be somewhat surprised if it was consistently faster than GC – most languages with automatic memory management use GC over RC because RC adds significant overhead, especially in multithreaded programs.
seanmcdirmid 19 hours ago [-]
You can always use things like arenas in C and get similar speed ups without GC overhead. If you know your memory lifetimes in advanced, avoiding granular malloc/free calls is pretty straightforward. A GC language doesn’t usually offer such options.
pjmlp 15 hours ago [-]
Sure it does, D just to give one example.
foldr 12 hours ago [-]
You can easily use arena allocation in a GCed language. With modern GCs there's not usually much performance benefit, so it tends to be limited to hot paths.
upboatsy 16 hours ago [-]
[flagged]
josephg 19 hours ago [-]
Good to know! Would there be any way languages like Go or C# could adopt Nim's new garbage collector? If it's better, what stops other languages from using it?
> GC programs can be faster than manually managed ones in some cases.
I've seen poorly written programs in C/C++/Rust which are slow because they allocate millions of tiny objects. Its true that generational GCs can be faster in this case. But you usually get much better performance again by using arenas and such. The reality is that I know more about the lifecycle of my data than my compiler. If you know what you're doing, you can take advantage of this information to write better programs.
If you don't want to think about memory management, then I agree - you're usually better off using a language with a GC. Personally I do a lot of my prototyping in typescript because I can iterate faster when I don't have to think about lifetimes.
Maybe some day Fil-C will run general purpose C code at native speeds, without a high memory overhead. But we're not there yet. I'm not holding my breath.
galangalalgol 11 hours ago [-]
Even if you don't think about memory management, and do the naive thing in rust or rc in some other systems language, gc only comes out ahead in that many tiny objects case. Which is very domain specific. I don't ever run into that situation, or if I do, they are homogeneous in type so I handle them in bulk, not individually.
grottoov 15 hours ago [-]
[flagged]
KingMob 15 hours ago [-]
complains about vote manipulation
user created an hour ago
rustybrains 15 hours ago [-]
[flagged]
mbrock 9 hours ago [-]
> Fil-C is the worst of all worlds here. Like C, it forces you to manually manage your own memory.
Can you expand on this?
johnlorentzson 1 hours ago [-]
As far as I know calling free is optional with Fil-C. It can prevent situations where some memory is no longer needed but isn't freed due to some remaining pointer to it sitting around somewhere, but it's not required. The GC will act like a GC if given the opportunity.
underskay 14 hours ago [-]
[flagged]
steveklabnik 1 days ago [-]
I'll come back and say some more in a bit, but in short: I like the idea of fil-c, but I also have some issues. I think it's overall a good and interesting project, but with some caveats.
norman784 1 days ago [-]
Not OP, but AFAIK one big issue with FIL-C it does the checks at runtime, adding overhead, don't quote me on this, but IIRC is around 20% slower.
josephg 23 hours ago [-]
2x slower and 4x less memory efficient were the numbers I heard a year or so ago. Reaching within 20% of full native performance while using a garbage collector sounds too good to be true.
Do you have any actual benchmarks?
steveklabnik 1 days ago [-]
Okay so: in general, as a rule of thumb: anything that makes stuff have more memory safety is good. And experiments towards that end are also good.
What I do not like, primarily comes down to how the project is talked about and marketed. First, because it promotes an "us vs them" mindset, instead of a "we're all trying to improve memory safety" mindset, and second, because in doing so, it also overstates its case.
These things are sort of intertwined. Let's talk about the overstatement first. Fil-c has its own definition of memory safety that is slightly different than others. For example, I saw this recently:
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
struct User {
char name[8];
int is_root;
};
int main(int argc, char*argv[]) {
struct User* user = malloc(sizeof(struct User));
strcpy(user->name, argv[1]);
if (user->is_root) {
printf("I am root!\n");
} else {
printf("I am not root :(\n");
}
return 0;
}
This, when invoked with "012345678" passed in, will print "I am root!". In my understanding, this is deliberately allowed.
But beyond corners like this, fil-c's author will go on about "Rust has unsafe as a hatch, fil-c does not" while if you control-f for "zunsafe_" on https://fil-c.org/stdfil you get ... escape hatches.
The author regularly erases the difference between "traps at runtime" and "is prevented at compile time", which are legitimate tradeoffs where one or the other may be better depending on what you're doing. But they're presented as either equivalent, or one is superior, and I find this muddles the discourse. The performance issues also tie into this, "add a GC" is absolutely a valid way to handle these sorts of issues, but it is not the same thing as what Rust does. And that's okay! But presenting it as purely superior means that it's just hard to talk about.
Speaking of muddling the discourse, the author regularly trolls on X, providing tons of bad faith arguments and generally trying to rile up a "fil-c vs Rust" war that I think reduces our ability to talk about these differences in a calm, engineering focused context.
Finally, due to its design, fil-c is effectively Linux only. That's great for Linux, but many people also use other systems, and so it is not a meaningful option for them.
Anyway, after saying all that: I still think that it is a good project, and that it should exist and continue to be worked on. I just wish that the heat was turned down, and people could talk about the various approaches and their tradeoffs without turning it into a culture war.
mirashii 1 days ago [-]
I generally agree with all of this, but I'll add a few additional remarks. Because it's come up a bunch lately, I decided to do a bunch of code review/audit of the Fil-C codebase, and I'd say while it's got a lot of good bones, there's a long way to go to being a foundation I'd be ready to build on. I've reported a few UAF's upstream, and I've got a few PRs I'll add on, but if it only took me a day or two to find some of these big holes, I'm sure there's more lurking under the surface. I'd consider it to at this point be more of an engineering demo that this approach is feasible and tractable, but not a production ready language that I'd want to ship code in.
On the muddling the discourse, I'm not on twitter and don't engage there, so I don't have an opinion on that, but I did come across https://news.ycombinator.com/item?id=49044561 recently, and I just don't see how the author can make such bold claims while examples like the one Steve provided above are still in the language. Corrupting memory in Fil-C is still easy, type confusion is still easy, intra-object overflows are still easy. Fil-C prevents a range of classes of bugs from being exploitable, but it doesn't stop the bugs from happening.
josephg 22 hours ago [-]
Yeah. In that thread the Fil-C author said of typescript, go and C#:
> Those languages rely on a much larger pile of YOLO C/C++ code for their runtimes and standard libraries than Fil-C does. So Fil-C is safer than those
Given the relative immaturity of Fil-C, this seems wildly wrong to me. I’m not sure how to take his claims about his runtime seriously.
This is a simple fact. The Fil-C runtime is tiny compared to TS and C#
mirashii 4 hours ago [-]
> The Fil-C runtime is tiny compared to TS and C#
Sure, this is a simple fact.
> So Fil-C is safer than those
This is not a simple fact that follows, and a good example of why you seem to be catching so much criticism for overly bold claims. One could state that a smaller runtime is easier to audit, and so the investment needed to reach similar levels of safety is lower. One might even argue that after similar levels of investment, that the probability that it's safer is higher. But jumping all the way from lower number of lines => safer is a simple fact is a huge leap.
pizlonator 4 hours ago [-]
I have worked on both a major JS runtime and a .NET runtime. I have fixed hundreds of security bugs in JSC. Based on that, I have no doubt that what Fil-C does is safer. Just the absence of a JIT makes it safer in a way that I don’t think is seriously debatable. Even with JIT disabled, a JS runtime has massive attack surface due to the language relying on a large native library to do anything useful, not to mention a mind boggling amount of language implementation corner cases.
By contrast Fil-C has a small number of rules and largely obviates the need for “native” code.
pizlonator 8 hours ago [-]
First of all, it’s incredible that on a HN thread about a language that isn’t C, there are 46 mentions of Fil-C! You guys are obsessed!
I make bold claims because they hold water.
- You can at worst corrupt only the capability you’re pointing to.
- intra object overflows are almost never useful for memory corruption exploits unless they let you corrupt a pointer, and Fil-C prevents that from being useful because you cannot corrupt the capability.
- the zunsafe api is basically unused. One library uses it (OpenSSL) for good reasons. This is in contrast to widespread use of the unsafe keyword in Rust, beyond just one library for a narrow purpose.
Thanks for reporting bugs. Worth noting that they require doing things that extant C code never does. It’s good to fix those, but the true threat model of any memory safe language is not to sandbox a malicious programmer, but to protect the program of a normal programmer against a malicious user
pron 1 days ago [-]
> it promotes an "us vs them" mindset, instead of a "we're all trying to improve memory safety" mindset
But you did the same thing when, on the spectrum that ranges from C to ATS, with Zig, Rust, and Java somewhere in the middle (though all closer to C than to ATS), you declared the exact compromise that Rust makes "table stakes"! [1]
Zig improves on C's memory safety when it comes to spatial safety, possibly the more impactful kind, so it, too, could be part of the "we're all trying to improve memory safety", yet you exclude it.
You're trying to draw some hard line that passes exactly between Rust and Zig on the C to ATS spectrum, and I'm trying to say that that line isn't there (your attempt at a definition of delineating safe and unsafe code also applies to C). Obviously, C, Zig, Rust, Java, and ATS all make very different tradeoffs, all of which may be more or less attractive to different people and in different circumstances, but there is no sharp line, at least not one that is meaningful enough to be "table stakes". Your personal inclinations place a premium on the things Rust offers and Zig doesn't while mine are the opposite, but I make no claim to universality.
I'm happy to accept that not everyone shares my aesthetics and can understand why some people prefer Rust, but those claims to or hints at universality annoy me (as they did when they were made by Haskellers, and I actually find Haskell's aesthetics quite pleasing), as they are simply unsupported. I've spent a lot of time studying formal methods and software correctness in general (https://pron.github.io) and if there's one thing we know in that field is that things are never that simple (and, bringing this back to this posts topic, even something like incremental compilation can contribute to program correctness).
(Now, you may argue that you're only talking about "memory safety" and not correctness in general, but what gives memory safety value is that violations are causes of many dangerous vulnerabilities; but once, say, Java eliminates all of them, 100% of bugs/vulnerability - which are still numerous - will be caused by other problems, all potentially avoidable with ATS, so why isn't ATS table stakes? Of course, the answer is cost, but all the languages on the spectrum differ in their costs.)
[1]: I assume that you meant Rust's compromise, because you implied that Zig doesn't pass that bar but Rust does.
steveklabnik 1 days ago [-]
> But you did the same thing when,
I do not go around posting "omg Rust is SO MUCH BETTER than zig or fil-c, which are TRASH." I talk about engineering tradeoffs, and what matters to me personally. I do not say "if you use Zig, you are a bad person." I am not saying that any comparison is bad. I am saying that the way that the comparison is presented is bad. That is different.
> you declared the exact compromise that Rust makes "table stakes"
Table stakes for me.
> Zig improves on C's memory safety when it comes to spatial safety,
I agree that it's an improvement on C!
> yet you excluded it.
I said that it is not pursuing a design that I personally find compelling enough to use to write software. That doesn't mean that I think it's worthless. This whole thing started off with me talking about how much I respect the Zig project! Yet you're trying to turn this into something where I'm talking shit. I presented a specific technical tradeoff that is important to me. That is very different.
> You're trying to draw some hard line on a spectrum that passes exactly between Rust and Zig, and I'm trying to say that that line isn't there (your attempt at a definition of delineating safe and unsafe code also applies to C)
I don't believe you've shown that. And my "attempt" does apply to C: it fails the bar, because it does not delineate between a safe subset and an unsafe superset.
> you may argue that you're only talking about "memory safety" and not correctness in general,
I am in fact talking about "memory safety" and have been this whole time, yes.
> what gives memory safety value is that violations are causes of many dangerous vulnerabilities; but once, say, Java eliminates all of them, 100% of bugs/vulnerability - which are still numerous - will be caused by other problems, all potentially avoidable with ATS, so why isn't ATS table stakes?
This is just an entirely different question. Yes, there are other forms of safety that are important too. That's just not what we're talking about here.
eaurouge 13 hours ago [-]
> I do not go around posting "omg Rust is SO MUCH BETTER than zig or fil-c, which are TRASH."
> While I still don't plan to write software in it, given that I believe memory safety is table stakes
What exactly am I missing? You know you don't have to hijack Zig threads (repeatedly) with your thoughts on memory safety, right?
steveklabnik 5 hours ago [-]
> What exactly am I missing?
My point in bringing this up is to strengthen my compliment. Even though I disagree with aspects of Zig's design, the stuff talked about in this post is excellent.
mbrock 10 hours ago [-]
> I do not go around posting "omg Rust is SO MUCH BETTER than zig or fil-c, which are TRASH." I talk about engineering tradeoffs, and what matters to me personally. I do not say "if you use Zig, you are a bad person." I am not saying that any comparison is bad. I am saying that the way that the comparison is presented is bad. That is different.
Oh, who is saying things like that? Do you mean to imply that this kind of vitriol is characteristic of the Fil-C project?
steveklabnik 5 hours ago [-]
Yes, this kind of rhetoric comes out of the project itself and its supporters, regularly.
mbrock 5 hours ago [-]
Not really. I mean if you want to make that claim, use actual quotes.
pron 1 days ago [-]
> Table stakes for me.
Ok, so if you meant "table stakes" as an expression of a personal preference and suitability to the programs you write without making an unsupported universal claim such as "this leads to better correctness" or "the price is almost always worth it" then we're good :)
> Yes, there are other forms of safety that are important too.
The thing is that they can be at least equally important, and some affordances for memory safety could potentially _harm_ them. To me, Rust offers little safety in the programs I want to write in a low-level language, but the price it charges in language complexity and implicitness ends up in a negative balance (I can't prove it, of course; as I said, software correctness is very complicated, and some of the greatest researchers in the field were proven wrong on how to best achieve it).
pizlonator 8 hours ago [-]
Steve, be reasonable.
I never said Fil-C is “so much better” (let alone with all caps) than anything.
I never called Rust “trash”.
I think you’re taking this all too personally
steveklabnik 5 hours ago [-]
I will let others be the judge by reading what you've written.
Maxatar 1 days ago [-]
There's a distinction between claiming something is objectively better, which is what Fil-C claims with respect to its "idea" about memory safety compared to Rust... and claiming a personal preference for one approach versus another approach, which is what OP is saying about their own personal preference about how Zig reduces errors compared to how Rust reduces errors.
It is absolutely possible that one language might actually have an objectively better approach to memory safety than another, and in such cases it is usually possible to argue for this using sound technical or empirical arguments. But the way the author of Fil-C presents their arguments it often comes across in a kind of antagonistic manner, like he has a chip on his shoulder.
pizlonator 7 hours ago [-]
I never claimed that Fil-C is objectively better than Rust.
As a long time PL researcher, I can, should, and will point out interesting corner cases of languages. Including in Fil-C or Rust
pron 1 days ago [-]
> But the way the author of Fil-C presents their arguments it often comes across in a kind of antagonistic manner, like he has a chip on his shoulder.
You may well be right. I've yet to learn about it, but I'm planning to.
jibal 21 hours ago [-]
> But you did the same thing
So your defense is a tu quoque fallacy? Note that "the same thing" is an admission.
rkangel 9 hours ago [-]
> First, because it promotes an "us vs them" mindset
I was always intrigued by the maturity about how the Rust team approached this sort of thing. IIRC years ago you and I had a back and forth on me thinking it would be helpful to have a "Why Rust is better than C++" type page.
Seeing an alternative approach from Andrew Kelley in recent weeks has really hammered home the value of the approach the Rust team took in terms of community building.
pizlonator 7 hours ago [-]
Is this a joke?
The Rust community is lobbying to make programming in C illegal
surajrmal 6 hours ago [-]
I don't think there is anything wrong with increasing liability for writing preventable bugs. The floor should be raised as our tools improve and the negative outcomes of these bugs increase. Making c illegal is not a goal of anyone I know of.
landr0id 2 hours ago [-]
Comments like these are why, even if Fil-C has cool technical accomplishments, I find it hard to speak positively about it. You unfortunately seem to forgo all nuance and push disingenuous arguments which fuel the cycle with similarly disingenuous people.
I'll do you a favor though and cite a few things related to "lobbying"
Neither have anything to do with making "programming in C illegal".
rkangel 6 hours ago [-]
Can you provide a citation to this fairly major claim?
ModernMech 7 hours ago [-]
No one is lobbying to make programming in C illegal, let alone the Rust community.
mbrock 10 hours ago [-]
There is an unsafe call primitive in stdfil that was used to support constant time crypto functions when Fil-C didn't have support for inline assembly.
Fil-C now has support for inline assembly, so it's not needed anymore, and I believe indeed the intention is to remove it, since Fil-C is not supposed to have any unsafe hatches.
Fil-C is not Linux only by design, that's completely false.
By the way, if you don't want a culture war, you gotta stop warring.
MatejKafka 5 hours ago [-]
> By the way, if you don't want a culture war, you gotta stop warring.
I find it hard to understand how could steveklabnik's comment be seen as warring.
steveklabnik 5 hours ago [-]
> Fil-C now has support for inline assembly, so it's not needed anymore, and I believe indeed the intention is to remove it, since Fil-C is not supposed to have any unsafe hatches.
See, this is nuance! Nuance is good! But you can't go around saying "fil-c has no escape hatches" when it has one, even if that one is planned on being removed.
> Fil-C is not Linux only by design, that's completely false.
Can you explain to me how it would work on other platforms? It currently does not, and I don't see how it can. Or at least, not without more "escape hatches."
mbrock 5 hours ago [-]
It would work basically the way it works on Linux? What makes you think it's somehow fundamentally Linux-specific?
steveklabnik 5 hours ago [-]
On non-Linux platforms, you must go through a system provided shared library rather than make syscalls directly. fil-c has its own ABI, and so it has its own libc that uses it. You cannot recompile those other systems's libc (or equivalent) with fil-c, therefore, the strategy used on Linux cannot work.
This is one reason why Rust has unsafe: you have to interact with inherently unsafe APIs in order to do anything meaningful with the operating system. fil-c needs an equivalent of some kind, and so isn't better or worse here, it's just the reality of how these systems work.
landr0id 3 hours ago [-]
>you must go through a system provided shared library rather than make syscalls directly
Just to add to this, on Windows for example you're really only supposed to invoke syscalls via ntdll as the syscall table is not stable so their numbering changes over time. You cannot guarantee forward or backward compat if you do not use the library.
Can I install Fil-C on Windows without WSL or Cygwin?
mbrock 9 hours ago [-]
Fil-C's understanding of memory safety is not "its own" idiosyncratic made up definition. Memory safety in the whole tradition that includes CHERI etc is defined in terms of objects and allocations. In C structs and arrays are not object boundaries. So CHERI will have the same semantics as Fil-C in your struct example, unless you enable a compatibility-breaking mode, which Fil-C could very plausibly acquire too, at the same cost of breaking semantic compatibility with the C/C++ semantics.
steveklabnik 5 hours ago [-]
C absolutely has struct, array, and subobject boundaries. The example is already undefined behavior in C.
mbrock 5 hours ago [-]
In the ISO standard, yeah, but that's not how C actually works.
eaurouge 13 hours ago [-]
> What I do not like, primarily comes down to how the project is talked about and marketed. First, because it promotes an "us vs them" mindset, instead of a "we're all trying to improve memory safety" mindset, and second, because in doing so, it also overstates its case.
Kinda rich of you to knock another language for its "marketing" when here you are once again on a Zig thread marketing Rust as a memory safe language. And speaking of "us vs them" mindsets, guess which language that reminds me of?
brabel 1 days ago [-]
I’ve been watching this debate online and in my opinion both sides are guilty. Fil is intentionally trying to be funny or at least “interesting “ when he makes his points and I, for one, enjoy his humor, which includes having a go at Rust and other languages. It seems Rust people just can’t take a little criticism, even when it comes from a clearly trolling language! Yes it’s true Rust has an escape hatch, and we’ve seen serious memory safety bugs due to unsafe Rust in the wild. Fil-C does not have one, what you post seems to be internal or even temporary stuff given the author clearly has a goal of not providing one? I would say you and others need to just relax and not treat all and every Rust criticism as an offense to you.
steveklabnik 1 days ago [-]
Maybe I'm just old, but I want to focus on engineering outcomes, not "trolling." If that means "can't take a joke," that's fine, but also "haha I'm just joking" is often what people use to try and hide behind their actual intentions.
I don't even work on Rust anymore, and in fact started this thread with a criticism of Rust. There are lots of good criticisms of Rust. There is a difference between "this criticism isn't good" and "every criticism is an offense."
pizlonator 7 hours ago [-]
I don’t troll.
I just post facts. It is interesting that Rust is marketed as memory safe and yet has some large holes and that use of `unsafe` is quite widespread. That’s worth having a grown up discussion about.
Maybe you don’t like that this was pointed out so you felt trolled. But then that’s on you.
Saying something that you don’t believe isn’t trolling.
ModernMech 6 hours ago [-]
You said "I just post facts". It is not a fact that the Rust community is lobbying to make programming in C illegal. Do you understand why some people in that community might consider that trolling?
chaz72 1 days ago [-]
Yes! I think Fil is great so far but I think as he gets a bigger audience he should, well, consider that and focus on clarity a little more than humor. You can see Andrew’s growth in that respect.
Rust folks, this whole thing is a thread about Zig’s new feature - not even a memory safety-related feature! - and we cannot spend the whole damn time talking about Rust.
Steve, even you - I don’t believe I have ever seen you say an unkind word. But have you considered that it may be unkind to have written more than half of the words on a thread about a Zig performance feature?
NobodyNada 19 hours ago [-]
I think the derailment of this thread into "is Rust's memory safety good or not" is unfortunate and tedious (this debate must have happened hundreds of times by now on this site alone), but I also think it's unfair to lay the blame on Steve for this. Steve left a thoroughly glowing comment praising Zig's work on compiler performance, and comparing to his perspective on the early days of Rust. He mentioned in passing that he's not a Zig user due to preferring to work in memory-safe languages, which was polite, brief, clearly his personal position, and in my opinion an acceptable way to disclose his relationship with Zig without derailing the thread to be about memory safety.
This comment spawned two subthreads. One of them was focused on the differences between Rust and Zig's compilation model, which is directly relevant to the article and illuminating regarding the engineering tradeoffs.
In the other subthread, pron posted paragraphs and paragraphs arguing about what memory safety really means and whether or not Steve is right to have his opinion that Rust is "safe". This tangent had essentially nothing to do with the content of Steve's comment; it (and not Steve's initial comment) was the point where the thread was derailed from the topic of Zig's incremental compilation model. Steve responded politely in this thread to comments and questions directed at him, but did not fan the flames or take the thread further into off-topicness. If the moderators collapsed pron's comment or detached it and pinned it to the bottom of the page, this comment thread would be much better and much more respectful to the Zig project.
I think the RESF trope is just about dead now; it's given way to the Rust Detractor Strike Force showing up to turn unrelated threads into tangential arguments about why Rust is bad.
steveklabnik 1 days ago [-]
> But have you considered that it may be unkind to have written more than half of the words on a thread about a Zig performance feature?
Inherently? No! I commented specifically because I was really glad to see this post. This work that Zig is doing is very good, and I wanted to call that out, in part specifically because I am on "the other side" in whatever sense that is. Why would it be unkind for kind words to be coming from me?
chaz72 1 days ago [-]
I do see that you often have kind words for Zig and some attempt to more precisely define the differences.
What I mean is a bit different though, it’s that these arguments you get drawn into end up drowning out any real discussion of Zig’s progress. I don’t think that’s your intent but it is frustrating. I should be clear, I don’t think it’s wrong for you to defend yourself from accusations etc., I just wish it didn’t look like this.
I wonder how much better HN would be if they took a page from other forum systems that said “you know what, this whole branch of stuff should be moved over here and renamed so the original topic can move on”.
Sorry, all this may be unhelpful, I don’t know where the line should be, I’m just thinking out loud about the problem.
steveklabnik 1 days ago [-]
Ah, I see what you're saying. Oh, trust me, it's very frustrating for me as well. It is especially frustrating in these specific circumstances because I know that Ron and I will not come to an agreement, so...
imtringued 11 hours ago [-]
>Steve, even you - I don’t believe I have ever seen you say an unkind word. But have you considered that it may be unkind to have written more than half of the words on a thread about a Zig performance feature?
You're accusing steve of what pron is doing... I mean look at how much text pron wrote, it is much more than steve wrote, and how pron is constantly skirting the edges of the HN guidelines.
chaz72 27 minutes ago [-]
My reasoning, which you may not agree with, is that I think that I have seen pron say unkind words before and I thought I had a better chance of persuading Steve to let it go. (Sorry pron, that’s how I was thinking of it. No, I have no specific examples.)
The fact is that every thread about Zig is filled with this kind of battle, and I’m tired of it, and it doesn’t represent any community, it’s so much worse on HN than anywhere else, and it’s all just defensiveness and crap.
upboatsy 16 hours ago [-]
[flagged]
hardwaregeek 1 days ago [-]
[dead]
slopinthebag 1 days ago [-]
> It seems Rust people just can’t take a little criticism, even when it comes from a clearly trolling language
I think this is a case of people who can dish it out but can't take it. As far as I'm concerned if you troll someone you should expect to get trolled back.
ethin 24 hours ago [-]
Might get downvoted but was thinking this exact thing when reading this debate. Rustations have this very bad habit (IMO) of pushing the "my language is better than yours" to an extreme that I haven't seen elsewhere (but I don't frequent a huge number of language circles so...). Yet when it is done to them they get all upset about it.
josephg 22 hours ago [-]
> Rustations have this very bad habit (IMO) of pushing the "my language is better than yours" to an extreme
It’s funny, I’ve heard people
claim this about rust developers for years. But I’ve seen very little evidence of it. Where are all these toxic comments? Look at Klabnik’s comments in this thread. He’s lovely.
—-
A son comes home to his poverty stricken family with a spring in his step. “Mum! Dad! All that time at community college paid off! I got a job!”. Dad immediately snaps - “so what, now you have a job, you think you’re better than us?”
What happened? Dad is unconsciously projecting a belief onto his son. Something like “unemployed people are shameful”. Then dad feels judged by the projected belief and he attacks the son for it. But it wasn’t the son’s belief in the first place. He just wanted his parents to be proud.
How does the son respond? It’s a tricky one. If the son defends himself by talking up how great it is to have a job, he reinforces the projection and dad will get more angry. If he says “there’s nothing to be proud of for having a job” then he’s lying about his values. It’s a trap.
When I’m feeling uncharitable, I project this same dynamic onto rust and C/C++ devs. “Mom! Dad! I figured out a way to get memory safety without sacrificing native execution and performance!” C: “So you think your language is better than ours? Why are you so toxic about it?”
I’m not really sure how to respond to comments like yours. I think you’re mad at ghosts.
pizlonator 7 hours ago [-]
This thread is evidence of it.
The OP is about Zig and now there are 40+ mentions of Fil-C initiated mostly by Rust folks and those comments are largely criticizing me personally.
That’s toxic AF!
mustache_kimono 4 hours ago [-]
Please.
Troll gets criticized. Cry me a river.
toxicrust 15 hours ago [-]
[flagged]
15 hours ago [-]
KingMob 15 hours ago [-]
Neither of those links involves Klabnik.
And the second one (about swatting a linux dev) says "Others think someone from the Rust (programming language, not video game) development community was responsible due to how critical René has been of that project, but those claims are entirely unsubstantiated."
--
Also wild to complain about botting when I've seen half a dozen accounts that didn't exist an hour ago all pop up to criticize Klabnik.
peterfirefly 12 hours ago [-]
And the first one is about Christoph Hellwig who has a long history of being awful to communicate with. He should have been kicked out of the Linux community two decades ago for that reason alone. For some reason, he has decided that he doesn't like Rust and he does what he can to keep it far, far away from any code he has anything to do with. If that requires 10x more work for others, that is a price he is willing to pay.
slopinthebag 23 hours ago [-]
It's funny because my comment was intended the other way, i.e. the Zig community & core team is antagonistic towards Rust so they shouldn't be surprised when they get pushback, like in this thread. But it really does go both ways when you look at how the Rust community has acted historically.
But hey, nerd holy wars have existed since the internet began. I use vim btw...oh you use emacs? You're an idiot. Etc etc.
imtringued 11 hours ago [-]
I'm not sure this is even about Zig or Rust. I'm honestly getting the feeling that the actual problem is that pron is turning into a troll and it's because he has a history of working on the JVM and he sees everything from the lens of writing language runtimes that ignore the Rust memory model altogether and the troll part is that he is not saying out loud what his niche is.
When he brought up the universality claim, he came up with a niche counter example that he kept inside his head and he makes it out to be the general rule by being extremely vague about literally everything.
dnautics 1 days ago [-]
zig creates an ir that you can use to do data dependency analysis and borrow checking.
steveklabnik 1 days ago [-]
Yes, it is absolutely possible to build a language that uses Zig's compilation model and do borrow checking. Zig is not going to add a borrow checker though, so as a user, it's sort of a moot point.
dnautics 1 days ago [-]
no. let me be clearer; it is possible to intercept zigs ir from the compiler NOW (well, 15.2 proven) and have a third party package do borrow checking from the data that flow through, without changing zig (think "how miri works without changing rust"). this is not currently directly possible without changing the compiler (~ 50 loc), however the core team has indicated that exporting ir, the only change needed, will be a supported feature once the language stabilizes.
steveklabnik 1 days ago [-]
How do you get the information to check properly without lifetimes in the signature?
dnautics 1 days ago [-]
lifetimes are not the only way to check safety. you just need to detect conflicts in the data dependency graph, lifetimes are in some way an overspecification (for safety, there are autoaliasing advantages). i suspect agnostic conflict detection is probably more expensive, too, but 1) borrow checking was not the slow step in rust compilation and 2) maybe you dont have to check for memory safety on every compile. on commit or on ci is probably fine
steveklabnik 1 days ago [-]
Gotcha! Well this sounds like a cool project, I look forward to seeing how it turns out.
I’m incredibly excited for this and have been looking forward to it for a while - I think new moves in IR will solve many issues with have with dynamic analysis of memory safety. The decompilation into other representations like Binary Ninjas IR have been a godsend to actually seeing what the hell Rust and Objective C do on the backend and understand actual cost the compiler makes to create memory fences.
pron 1 days ago [-]
So that could be a clear definition, but for it to be "table stakes" it needs to have some universal value and it doesn't (in fact, that very same definition could also classify even C as "memory safe"): https://news.ycombinator.com/item?id=49087458
steveklabnik 1 days ago [-]
I can say that something is "table stakes" for me without demanding that everyone else adhere to my values.
awesome_dude 1 days ago [-]
There's little point in me telling the world how I like my dinnercooked, unless the world both understands exactly what I mean, and cares
IshKebab 1 days ago [-]
I'm not exactly sure what you mean by "universal value", but I would say to be "table stakes" (i.e. not optional), it has to have overwhelming value. I think outside some fairly niche areas (e.g. programs that don't process untrusted data at all), it very very clearly has overwhelming value.
Now you might argue that the other features of Zig, like `defer`, are so good that they reduce the chance of memory errors and therefore memory safety has less value for Zig. But that seems highly dubious to me, especially for use-after-free. I guess we'll find out when Zig has more widespread use.
imtringued 11 hours ago [-]
His point is literally that writing a JVM (or any other language runtime) in Rust is unpleasant.
He can't write a JVM without using unsafe. So his disagreement on the "table stakes" is that his "table stakes" require using unsafe Rust everywhere and from that perspective Rust is not that different from any other language.
Hence the complaint about the lack of universality, which I personally consider weird. His point is that you cannot write the most interesting low level programs using just the safe subset of Rust.
If you have to write unsafe Rust (emphasis on have, your mileage may wary lot on that), then you have to litter unsafe everywhere in your code base so how does Rust help him? That's his point, but he doesn't want to say it out loud.
pron 8 hours ago [-]
What makes a JVM a good fit for a low-level language is the need/utility for precise control over the hardware, but that's always the reason to want a low-level language (why would you use a low-level language if you don't want precise control over the hardware?). When Rust gives you safety it takes away much of that control, and when it gives you control it takes away the safety. Now, there could well be cases where this mix is fine - I've never worked on a browser, and Rust may well be the best language for writing browsers.
saghm 1 days ago [-]
> I'm not sure what that means. Java lets you do many things in a memory-safe way but not everything. Rust lets you do fewer things than Java in a memory-safe way, but more things than Zig.
I don't think I agree with this framing. The question to me isn't "what can you do while being memory safe", it's "can you accidentally do something memory unsafe without noticing?" Rust and Java are the same here; you need to explicitly opt into using the language's mechanism for relaxing restrictions (Rust's `unsafe` blocks, Java's `Unsafe` class APIs), whereas from what I understand, neither Zig or C offers anything strict in that way.
pron 1 days ago [-]
That framing may seem intellectually satisfying, but it's not useful in practice. Consider the extreme edge case of C: We can clearly mechanically delineate between the empty program and a non-empty one, we call the empty program safe and any program that isn't empty unsafe (i.e. C is memory-safe if you want to do nothing and not if you want to do anything). And so, we also have this property that in C you can't do anything unsafe without noticing.
Now, that's ridiculous, but something not too different happens to me with Rust. I reach for a low-level language when I want to do low-level things in a more convenient way than in Java, but the very things that would make me reach for a low-level language in the first place are unsafe in Rust. So in ~100% of the programs I want to write in a low-level language, Rust and Zig offer the same level of memory safety (but I need to pay a higher price for Rust). That Rust reminds me that what I want to do is unsafe doesn't help me.
Of course, other people may want to reach for a low-level language in other situations and their perspective could be different, but if I pay the price and get little in return I can't see how that would be "table stakes". Table stakes imply some universality that is obviously not here.
CJefferson 1 days ago [-]
Do you find most of your rust code is unsafe? Because I also find I do some unsafe stuff, because of the algorithms I work on I often end up with some unchecked array accesses and a couple of raw pointers into those arrays I pass around. But 98% of the code is safe and I find this makes it easier to reason about.
imtringued 11 hours ago [-]
Yes, it's pron. Pron works on a JVM so he looks at everything from a language runtime lens. To him, the entire code base might as well be unsafe.
The argument that you can contain unsafe in safe abstractions does not interest him, because of the nature of the projects he works on.
pron 9 hours ago [-]
> To him, the entire code base might as well be unsafe.
Not at all. I would very much like the code to be safe, it's just that I reach for a low level language to get the thing that low-level languages are designed to offer me, which is control, and no language offers control and safety at the same time. So when the interesting parts of the code could be written in safe Rust, I have to give up control, and in that case I'd rather use a more convenient language that doesn't give me full control. And when the interesting parts of the code need full control, Rust doesn't give me safety anyway, and I still pay for its complexity. As to encapsulating unsafe, that doesn't help at all if the most tricky parts of the code are inside. What happens there is that Rust makes the hard parts harder and the easy parts easier, and for me that's a net negative.
pron 7 hours ago [-]
P.S.
And that brings me back to the universality point, that you're actually repeating, so let me explain it again. All programming languages bring a certain aesthetic that appeals to some and repels others within their intended domain. For example, I prefer Java to C# and Kotlin because I place a value on language simplicity, but I'm well aware that some other people like a lot of convenience features and they have the opposite preferences. I don't expect everyone to like Java even among those who work in the domains in which it is intended. For the same reason, I prefer Haskell over Scala and Clojure to Common Lisp, yet I fully recognise there are those who have the opposite preference.
Yet when people say they don't like Rust, the reaction among those who do is often one of: you don't want it to work, you don't care about correctness, your situation is special, or you just don't get it. I.e. they expect Rust to be universally liked unless there's a some good exceptional reason not to. But this is not the case for any language, let alone a language as complicated and as rich as Rust, which is certain to evoke a strong aesthetic reaction either for or against. So I have a long, long record of working with low level languages, I have a long record of working with formal methods and studying software correctness in general, I've worked extensively on safety-critical and mission critical systems, and I find Rust unappealing. There is nothing that should be surprising about that, there is nothing special about Rust or any other language that should lead anyone to expect it to be universally liked among those in the domains for which it is intended, and indeed, it is not the case for Rust as it is not the case for any other language. This amazement that quite a few people, even those with the relevant expertise, don't like your favourite language is exactly this sense of universality that I find annoying.
That some people get it yet don't want to choose a language is the case for Java, it's the case for Haskell, it's the case for Zig, and it's the case for Rust. Some people who really care about writing correct low-level code will have good reasons to choose Rust over other languages, while others who really care about writing correct low-level code will have equally good reasons to choose other languages over Rust. It's not because our programs are special, but it's because we think we can do it better with other languages. That's just the reality of all programming languages ever made, and that's what I mean by no universality. There is no external, objective requirement for any program that makes any (general purpose) language objectively best for that program. Choices always involve some kind of personal aesthetic preference.
That your favourite language isn't "objectively best" and isn't universally preferred even among those who care about the things you care about isn't something that should bother you, and pointing out this simple fact isn't trolling. It's something you should expect, because it's always true.
CJefferson 6 hours ago [-]
P.P.S.
Assuming you are replying to me, you seem to be projecting very powerfully. Rust isn't my favorite language, and I just asked about how much of your code was safe vs unsafe.
pron 5 hours ago [-]
No, I was replying to imtringued.
But to answer your question, I reach for a low-level language when I need the one and only thing low-level languages are designed to do best, which is give me full and precise control over the hardware. In those situations, Rust offers little safety in the interesting/subtle/critical code, and for me it even makes things worse. It helps with the simple uninteresting code. So it's not a matter of quantity but of quality.
Of course, if the tricky parts of the program don't require precise control over the hardware, I don't use a low-level language in the first place. For example, if I'm going to let the language manage memory for me, why suffer the high overhead of heap memory management in the Rust/C runtime when the Java runtime offers me lower overhead?
saghm 1 days ago [-]
> That framing may seem intellectually satisfying, but it's not useful in practice.
Honestly, that's exactly how I feel in reverse. The framing you gave is more intellectually interesting, but it doesn't help explain the actual real-world outcomes where in practice, Rust and Java both don't have much problem with unsafety, whereas C does, and at least from what I've heard, Zig does as well.
> I reach for a low-level language when I want to do low-level things in a more convenient way than in Java, but the very things that would make me reach for a low-level language in the first place are unsafe in Rust. So in ~100% of the programs I want to write in a low-level language, Rust and Zig offer the same level of memory safety (but I need to pay a higher price for Rust). That Rust reminds me that what I want to do is unsafe doesn't help me.
I mean, sure, if you want to do things that are fundamentally not possible to validate because you think you're smart enough not to screw up, that's going to make Rust a tough sell. My issue with it is that history has shown that the best C and C++ programmers in the world still write code where memory safety rears its head, so I'm distrustful of the claim that being smart and diligent is enough to prevent the sort of bugs that we're still dealing with after half a century of us learning how not to write C. You need to have an excess of either talent or hubris to consider that a reasonably safe path, and given that the amount of talent needed is a lot higher than the amount of hubris, it seems way more likely that it's the latter.
The alternative is just learning how to write code that doesn't require expressing things in a way that can't be validated. While there are some things that fundamentally are not possible to, I'm dubious that it's anywhere close to as high as you seem to expect if your experience is that you literally can't reduce the amount of unsafe code you need in Rust below "literally my entire program is unsafe".
pron 1 days ago [-]
> Rust and Java both don't have much problem with unsafety, whereas C does, and at least from what I've heard, Zig does as well.
I'm not interested in the definition so much as I am in calling it "table stakes", and so the fact that these languages satisfy their promises is uninteresting in isolation. What matters is the value of their promises. The majority of Rust programs I see, I wouldn't have written in a low-level language, so the fact that it offers memory safety for the things I don't need it to do does nothing for me.
Now, clearly, Rust's originators didn't consider what Java offers (or at least what it offered 20 years ago when Rust was first conceived) to be table stakes or they wouldn't have wanted Rust. Java exacted some price in exchange for its memory safety that was unacceptable to Rust's originators and trumped its memory safety. But the same thing happens with Rust vs Zig. Rust exacts a heavy price for its memory safety, that - just as in Rust's case vs Java - is sometimes unacceptable. So I can't see how any of these could be "table stakes".
> I mean, sure, if you want to do things that are fundamentally not possible to validate because you think you're smart enough not to screw up, that's going to make Rust a tough sell.
What Rust can validate and what can fundamentally be validated are two very, very different things. Compared to what ATS can validate, what Rust can validate is almost indistinguishable from C. In Rust you have to do lots and lots of things that require you to be "smart enough not to screw up" that you could prove in ATS, and still no one (including Rust programmers) would say that what ATS offers is "table stakes" because, obviously, it comes at a high price that the people who choose Rust don't want to pay.
So clearly different languages offer different capabilities and charge a price for them. Sometimes the price is worth it and sometimes it isn't.
> so I'm distrustful of the claim that being smart and diligent is enough to prevent the sort of bugs that we're still dealing with after half a century of us learning how not to write C
But Java or Rust programs still suffer from a lot of bugs that ATS could eliminate, if you're willing to pay the price, and you're clearly unwilling. ATS programmers could say about Rust programmers what you say about C++ programmers. Clearly there's no universal table stakes here.
saghm 1 days ago [-]
> I'm not interested in the definition so much as I am in calling it "table stakes", and so the fact that these languages satisfy their promises is uninteresting in isolation. What matters is the value of their promises. The majority of Rust programs I see, I wouldn't have written in a low-level language, so the fact that it offers memory safety for the things I don't need it to do does nothing for me.
I mean, if you're already going to say "I don't want a low level language for anything other than what I can use unsafe for", then of course Rust will seem like overkill. I'd argue that the value of Rust is that it makes low-level viable for a lot of stuff that would otherwise require a lack of memory safety; a lot of it is stuff that might be written in a higher level language, but that's just because relatively few programs are impossible to write in higher level languages. That doesn't mean that the ones that need to be lower level can't be written in Rust though.
> Now, clearly, Rust's originators didn't consider what Java offers (or at least what it offered 20 years ago when Rust was first conceived) to be table stakes or they wouldn't have wanted Rust. Java exacted some price in exchange for its memory safety that was unacceptable to Rust's originators and trumped its memory safety. But the same thing happens with Rust vs Zig. Rust exacts a heavy price for its memory safety, that - just as in Rust's case vs Java - is sometimes unacceptable. So I can't see how any of these could be "table stakes".
Yes, "table stakes" is a value judgment, and one some people will disagree with. The cost for memory safety in Java is performance overhead though, and the cost for memory safety in Rust is not being able to express certain valid things that can't be validated; those are both objectively different from not offering memory safety at all, and my point is that the cases where what you want to express is literally impossible in Rust to do safely while actually being memory safe are pretty rare. There are some cases where what you're trying to do are fundamentally unsafe, in which case you need to use an unsafe block, but that's not anywhere close to the same as removing validation from the entire program. I'm fairly skeptical that you're basing your view that there are so many cases where you want to do something that's guaranteed to be safe but impossible to write in safe Rust on objective criteria, and extremely skeptical that the programs you write are anywhere close to entirely comprised of logic that can't be expressed safely.
> What Rust can validate and what can fundamentally be validated are two very, very different things. Compared to what ATS can validate, what Rust can validate is almost indistinguishable from C. In Rust you have to do lots and lots of things that require you to be "smart enough not to screw up" that you could prove in ATS, and still no one (including Rust programmers) would say that what ATS offers is "table stakes" because, obviously, it comes at a high price that the people who choose Rust don't want to pay.
> So clearly different languages offer different capabilities and charge a price for them. Sometimes the price is worth it and sometimes it isn't.
Sure, no one is disputing that. But that doesn't change the fact that some languages objectively require you to opt into which parts are memory unsafe, and others don't. It's obvious we won't see eye to eye on whether that's table stakes or not, but that's a difference of opinion, and having a different opinion than you isn't literally illogical; I find your take on it to be as hard to understand as mine is to you.
> But Java or Rust programs still suffer from a lot of bugs that ATS could eliminate, if you're willing to pay the price, and you're clearly unwilling. ATS programmers could say about Rust programmers what you say about C++ programmers. Clearly there's no universal table stakes here.
You're again taking an empirical argument as an abstract one and ignoring the real world outcomes that languages produce. You mentioned finding the fact that they actually produce real world software that in practice do not suffer from the class of bugs that C/C++ suffers from uninteresting, and that's fine, but it's meaningful for people who care about software actually getting used in the real world for real things. You seem to be arguing that unless you can eliminate literally the most bugs of any language in existence, then eliminating any bugs by picking a language that eliminates some of them is a useless endeavor. To me, the reasonable thing would be to choose a place to draw the line and say "anything beyond this is too risky, but I'll tolerate anything that's at least this safe", and memory safety is in practice the place I think it makes sense to do. I don't agree at all that not drawing any line at all is the only logical choice in a scenario when there are multiple places to draw it.
pron 1 days ago [-]
> I'd argue that the value of Rust is that it makes low-level viable for a lot of stuff that would otherwise require a lack of memory safety; a lot of it is stuff that might be written in a higher level language, but that's just because relatively few programs are impossible to write in higher level languages.
Maybe, but I don't see making a low language viable for something it's not needed as offering much value. Low-level languages are primarily designed to give you direct, low-level control over interaction with the hardware, they sacrifice other things for that goal (including performance [1]), and so if I don't need that control I don't use a low-level language. When I do need that control, I find that Rust requires reaching for unsafe too frequently while still paying the full price for the safety of things I don't use (even Rust's memory management of strings doesn't give me the control I want; I have to work pretty hard for it).
> The cost for memory safety in Java is performance overhead though,
It's not performance (you often gain performance, especially in large programs). It's warmup and footprint.
> But that doesn't change the fact that some languages objectively require you to opt into which parts are memory unsafe, and others don't.
Like I said, C also fits in the category, so it's not a meaningful distinction. The difference is in what you can do in the safe subset. Zig lets you do more things in a safe way than C (where the safe subset is effectively empty), Rust lets you do more safe things than Zig, and Java lets you do more safe things than Rust.
> You mentioned finding the fact that they actually produce real world software that in practice do not suffer from the class of bugs that C/C++ suffers from uninteresting
I didn't say that that's uninteresting; in fact Zig also eliminates spatial unsafety as well as Rust, and I think that's good. I said that merely looking at broad statistics is uninteresting if you don't consider the kinds of programs being written. I.e. Rust gives me safety mostly when I write code with the same level of low-level control as I have in Java, then that's the part I find interesting.
> You seem to be arguing that unless you can eliminate literally the most bugs of any language in existence, then eliminating any bugs by picking a language that eliminates some of them is a useless endeavor.
That's the very thing I'm arguing against. I'm saying that different languages eliminate different bugs at a cost (again, Zig eliminates many memory safety bugs you'd find in C or even C++, arguably the most dangerous ones). What I'm saying is that what you get and whether the price is worth it depends both on the program you're writing and on your personal preferences. Just to be clear, "preferences" doesn't mean I care more or less about correctness, but which approaches to correctness I find more or less effective, something on which there is no consensus.
> To me, the reasonable thing would be to choose a place to draw the line and say "anything beyond this is too risky, but I'll tolerate anything that's at least this safe", and memory safety is in practice the place I think it makes sense to do.
I think it also depends on the kinds of programs you write, because for many programs I write (and for which I pick Java) Rust's level of memory safety is too low, and for the programs I pick a low-level language I wish I could have some cheap memory safety, but it's not offered to me. So in those cases I would prefer Zig's spatial memory safety, as it's no worse than Rust, and not pay the high price for Rust's while getting little in return. Anyway, I'm saying that it's both a matter of which approach you believe leads to better correctness and the kinds of programs you write in the language.
[1]: For example, the fact that in Java, references are not required to be stable machine pointers opens the door to some powerful optimisations that are not available to languages where pointers are required to be machine pointers (or something close enough to them). Or the fact that low-level languages require that the machine instructions executed are those present in the compiled image (or close enough), or, if you want, caring about worst-case performance at the expense of average case performance (although both C++ and Rust specifically don't always make that easy) precludes some other very powerful optimisations. People like me who've spent years on huge C++ programs know that the low-level control offered by low-level languages (regardless of the question of safety) sometimes helps performance and sometimes harms it.
uecker 1 days ago [-]
> C (where the safe subset is effectively empty),
Well, Fil-C and also Cheri show that C is a language that can be implemented with perfect memory safety for 99.9% of the language. This is not true for every language but is also not an accident in C. But also with the typical implementations of C such as clang and gcc you can essentially get spatial memory easily by using safe abstractions.
dwattttt 24 hours ago [-]
Very explicitly making a silly point; a rock is 100% memory safe.
The serious point: I care about whether the program I write aborts regularly, whether due to a Rust panic or a capability violation.
uecker 16 hours ago [-]
Regarding the silly point: Fil-C is less of a rock than Safe Rust as everything in C just works.
I am not sure about what you mean by "aborts regularly". After a memory safety issue, I think one usually wants to abort quickly and not do anything else in the program as the program state is confused, so running specific sanitizers in trapping mode together with coding abstractions that avoid unchecked raw pointer access you can write spatially memory safe code in C without a problem. But yes, sometimes you may want to continue running, but this is much harder and needs a careful design of the system anyway, also in other languages.
dwattttt 13 hours ago [-]
> Regarding the silly point: Fil-C is less of a rock than Safe Rust as everything in C just works.
I'm going to challenge this. Is this from your experience? InvisiCaps, which Fil-C is based on, turns out of bounds accesses into panics. You're saying that turning any "benign" out of bounds into "abort the program" in all the C you? anyone? everyone? uses Just Works?
I'd rather the majority of those failure modes be caught by linters/compiler passes, not runtime safe aborts.
saghm 1 days ago [-]
> It's not performance (you often gain performance, especially in large programs). It's warmup and footprint.
To me, those are also performance characteristics. Maybe my view on what constitutes "performance" is broader than average here.
> When I do need that control, I find that Rust requires reaching for unsafe too frequently while still paying the full price for the safety of things I don't use (even Rust's memory management of strings doesn't give me the control I want; I have to work pretty hard for it).
Fair enough, I can't tell you that you don't have that experience when writing Rust. It's pretty different from mine though, and the experience of the large number of former C/C++ devs I've worked with after they learned Rust; the only people I've talked to with that experience didn't really try to learn Rust and went in hoping that it wouldn't work for them, which informs my perception here, but I recognize that individual experiences won't always fit into larger trends.
> Like I said, C also fits in the category, so it's not a meaningful distinction. The difference is in what you can do in the safe subset. Zig lets you do more things in a safe way than C (where the safe subset is effectively empty), Rust lets you do more safe things than Zig, and Java lets you do more safe things than Rust.
I don't think I understand what you're saying here. I don't know of a way to turn off undefined behavior by default in C and only opt into it in discrete segements of the code, but maybe I'm missing something.
> That's the very thing I'm arguing against. I'm saying that different languages eliminate different bugs at a cost (again, Zig eliminates many memory safety bugs you'd find in C or even C++, arguably the most dangerous ones). What I'm saying is that what you get and whether the price is worth it depends both on the program you're writing and on your personal preferences. Just to be clear, "preferences" doesn't mean I care more or less about correctness, but which approaches to correctness I find more or less effective, something on which there is no consensus.
It seems like you're arguing against the idea of memory safety as a category at all then. To me, "I can't write code that's memory unsafe without explicitly opting into it" seems like an objective statement, and it's objectively different than "I can't write certain types of memory safety bugs in a given language". I don't really understand what's useful about being able to write memory unsafe code without having to opt in when in practice the number of bugs from mistaken memory safety are overwhelmingly more common than the cases when you're forced to opt into unsafe because Rust forced you to work around the constraints, and even in low-level programs, the actual number of truly unsafe operations you need to do tend to be fairly low in my experience. I guess I can't say for certain that you don't truly need to do things that you're forced to write unsafe for too often, but to me, it seems like you're refusing to pay a pretty small price for mostly ideological purity rather than pragmatism.
> I think it also depends on the kinds of programs you write, because for many programs I write (and for which I pick Java) Rust's level of memory safety is too low
> [1]: For example, the fact that in Java, references are not required to be stable machine pointers opens the door to some powerful optimisations that are not available to languages where pointers are required to be machine pointers (or something close enough to them). Or the fact that low-level languages require that the machine instructions executed are those present in the compiled image (or close enough), or, if you want, caring about worst-case performance at the expense of average case performance (although both C++ and Rust specifically don't always make that easy) precludes some other very powerful optimisations.
I'm struggling to imagine what the circumstances are where these are genuine concerns rather than theoretical or premature optimizations. What are some examples of programs where you'd get better characteristics running them if they were written in Java rather than Rust due to the lack of enough "memory safety" in Rust?
pron 1 days ago [-]
> To me, those are also performance characteristics. Maybe my view on what constitutes "performance" is broader than average here.
Yes, but they come with speed gains, so you can't say that you pay "performance overheads" when Java removes some of the performance overheads that programs in low-level languages and replaces them with others. You could similarly say that you pay performance overheads when going in the other direction.
> and the experience of the large number of former C/C++ devs I've worked with after they learned Rust
And it's not my experience or a large number of C/C++ devs I work with.
> the only people I've talked to with that experience didn't really try to learn Rust and went in hoping that it wouldn't work for them
Then your exposure isn't wide enough.
> I don't think I understand what you're saying here.
What I'm saying is that we can't say that the value is merely in the existence of a clear syntactic distinction between safe and unsafe code, because that distinction exists in C, only in C, the clearly delineated line between safe and unsafe code is that between `int main(void) {}` and anything that isn't that; i.e. any program other than that explicitly opts into unsafety. So any meaningful discussion about memory safe languages must include what you can do in the safe subset. In C's "safe subset" (the empty program), you can do nothing, and that's what makes it not valuable. But for my needs, what you can do in Rust's safe subset (compared to both Java and Zig) is also far too little (to justify the cost).
> To me, "I can't write code that's memory unsafe without explicitly opting into it" seems like an objective statement
It is, but what I'm trying to say is that it alone doesn't have much value. In C you also "can't write code that's memory unsafe without explicitly opting into it" by writing anything other than the empty program, but obviously you wouldn't consider C's memory-safe subset suitable because you can't use it to do what you want to do in C. Rust's value is not, therefore, in that it has a memory-safe subset, but that it has a useful memory-safe subset. It's just that the utility of that subset depends on the kinds of programs you'd want to use a low-level language in the first place.
> it seems like you're refusing to pay a pretty small price for mostly ideological purity rather than pragmatism.
Quite the opposite. The price of Rust's complexity, implicitness, and compilation time is too high for what little safety I get in return, that I don't want to pay it for pragmatic reasons.
> I'm struggling to imagine what the circumstances are where these are genuine concerns rather than theoretical or premature optimizations. What are some examples of programs where you'd get better characteristics running them if they were written in Java rather than Rust due to the lack of enough "memory safety" in Rust?
It's nothing to do with memory safety. Low-level programs sacrifice optimisation opportunities available to Java because above all else they need to offer low-level control. That low-level control can translate to good performance sometimes (especially in smaller programs), and sometimes it translates to worse performance (especially in large programs). The huge C++ programs I worked on migrated to Java not (just) for safety but also for better performance than C++ (again, it's easy to get excellent performance in low-level languages when the programs are small or specialised; it gets harder and harder as they grow). So we got better performance than C++ while also getting better safety than Rust, a much simpler language than Rust (or C++), and faster build cycles than Rust (or C++). But the topic of how Java reduces the overheads that C/C++/Rust/Zig programs often have when they grow large (although Zig makes it easier than the other them to reduce them) is a whole complicated topic. I might give a talk about it at the upcoming Devoxx.
saghm 24 hours ago [-]
> > and the experience of the large number of former C/C++ devs I've worked with after they learned Rust
> And it's not my experience or a large number of C/C++ devs I work with.
>> the only people I've talked to with that experience didn't really try to learn Rust and went in hoping that it wouldn't work for them
> Then your exposure isn't wide enough.
Or maybe your exposure is only to people who didn't give it a fair chance? I don't know how either of us can be confident that we know 100% for sure that our sample is more definitive.
> What I'm saying is that we can't say that the value is merely in the existence of a clear syntactic distinction between safe and unsafe code, because that distinction exists in C, only in C, the clearly delineated line between safe and unsafe code is that between `int main(void) {}` and anything that isn't that; i.e. any program other than that explicitly opts into unsafety. So any meaningful discussion about memory safe languages must include what you can do in the safe subset. In C's "safe subset" (the empty program), you can do nothing, and that's what makes it not valuable. But for my needs, what you can do in Rust's safe subset (compared to both Java and Zig) is also far too little (to justify the cost).
> It is, but what I'm trying to say is that it alone doesn't have much value. In C you also "can't write code that's memory unsafe without explicitly opting into it" by writing anything other than the empty program, but obviously you wouldn't consider C's memory-safe subset suitable because you can't use it to do what you want to do in C. Rust's value is not, therefore, in that it has a memory-safe subset, but that it has a useful memory-safe subset. It's just that the utility of that subset depends on the kinds of programs you'd want to use a low-level language in the first place.
That seems like an absurd false dichotomy in the form I was talking about before. I don't seriously believe that you can't easily identify when looking at Rust code whether unsafe is explicitly being allowed in it or not, or that you are writing programs that are doing things that would require unsafe literally everywhere.
I've genuinely been trying to understand where you're coming from, but the more I try, the more it seems like you just genuinely seem to think that you're too smart to accidentally write memory safety bugs, or that the memory safety bugs don't matter much. Maybe you're right, but I don't think there's anything left for me to learn from your point of view.
pron 23 hours ago [-]
> or that you are writing programs that are doing things that would require unsafe literally everywhere
You don't need unsafe "literally everywhere" to run into issues. First, what matters most are the areas that are most subtle/tricky in your program. If in those areas Rust doesn't add much safety and makes things worse due to language complexity, that's a problem. Second, when you want low-level control, you might well want it in quite large swaths of the code. For example, one thing that low-level languages currently, in principle, do better than Java is arenas. But the whole point of arenas is that you want _all_ allocations in some large and elaborate call chain to go in the arena (and you'd like to enjoy both the standard library and 3rd party libraries). Rust doesn't make that easy (and neither does C++, for that matter).
> the more it seems like you just genuinely seem to think that you're too smart to accidentally write memory safety bugs, or that the memory safety bugs don't matter much
I don't see how you've reached that conclusion. I told you that for most programs I choose a language that is more memory-safe than Rust, and when I choose a language that's less memory-safe than Rust it's when Rust doesn't offer much safety, either.
See, this is exactly the thing I find so annoying in the Rust discourse. There's no doubt Rust significantly helps avoid memory safety issues (i.e. Rust => more men-safety) but that doesn't mean that caring about memory safety issues means preferring Rust (more mem-safety => Rust). One simply doesn't follow from the other because the logical implication is reversed.
slopinthebag 1 days ago [-]
I just wanna give my perspective since I came from high level languages and pretty much exclusively use Rust now, so perhaps I can articulate why I find value in the language. And my apologies if my input is not wanted, no need to respond if so.
First of all I respect your point of view - I'm not a Rust absolutist, I think that garbage collected languages are a massive advantage for a lot of things and would never criticise someone choosing a higher level language. Likewise I wouldn't criticise someone choosing Zig or Oden or Jai or even C for tasks where you really need that low level control.
For me, I like to have a single language that I can use for pretty much everything. Afaik there is no other language that is a) popular b) has a modern toolchain with integrated build, formatting & linting etc, and c) can be used both in the kernel and for developing websites. Rust might not be the best choice for most of the spectrum of software, but it's good enough for everything. I can write a low level service + a web server and UI in the same language, where with other choices I would need to use two separate languages. This matters to me because I don't have the time to maintain mastery of multiple languages, I find a lot of value in focusing deeply on one language and learning it completely.
Now I also don't write a lot of low level rust, I've never written a block of unsafe before and I probably write "unidiomatic" rust with too much copying, too many Arc<Mutex>>'s etc. But I like knowing that I can if I need to.
Rust has a lot of other things going for it. A good type system with plenty of nice language constructs that are missing in a lot of higher level languages. It has Cargo and a healthy ecosystem (although I do worry about the number of dependencies used sometimes). And a large community of very smart people. I'm not saying this is exclusive to Rust, but as a whole Rust is a unique language with no alternatives if you value the things I do.
So I would say that it's approach to memory safety threads the needle where it can be used (although not the very best choice) for when you'd use a higher level language, but also gives enough control that you can do plenty of low level stuff in it safely, and with clearly delineated unsafe sections where you really can do anything.
pron 23 hours ago [-]
I get that perspective and I agree it has value, but for me, Rust is a jack of all trades but master of none, all while being one of the most complicated languages ever made and requires very long build times. So I agree it continues C++'s dream of being "one language for everything", but I think that dream is misguided, and that Rust suffers from most of the same problems as C++.
For low-level programs, I already said that Rust doesn't offer much safety for the things I reach low-level languages for (or, conversely, its safe subset doesn't offer the very control I'm after in such a language). Furthermore, the complexity and implicitness of the language make it harder for me to carefully understand the kind of subtle code I write in such programs. The long build times could mean I write fewer tests.
For high-level programs, Rust's safe subset is technically sufficient, but the problems are even worse (and exactly match C++'s): High level Rust code looks quite good and is easy to write, same as in C++, but the problems start with the maintenance and evolution. Small local changes - to a returned object's lifetime or thread-share ability, or between static and dynamic dispatch - require non-local changes. That's because low-level languages have low abstraction, i.e. the same contract covers fewer possible implementations. True, unlike C++, Rust tells you what things you need to change, but you still need to change them. That was the main problem we had with C++: the code looks great and it's very easy to write at first, but the maintenance and evolution costs - especially when the program is large and long-lived - get high and remain high forever. Furthermore, once a program grows large, it starts suffering from similar performance ovhearheads large C++ programs suffer from: you find yourself needing more dynamic dispatch, which is slow in Rust and C++; you find yourself needing more shared objects with different lifetimes, which are also slow in those languages, so the program isn't even particularly fast or scalable (sure it's faster than a JS or a Go program, but that doesn't say much). Java (or C#) which is aimed at optimising the performance of large programs, removes many of these overheads. Lastly, deep always-on observability/profiling isn't quite poor (it's better now with eBPF, but still a long ways away from what you get with Java or C#).
So yes, Rust and C++ are intended as "one language for everything", and Rust is probably somewhat better than C++, but your high level programs pay for the low level feature (i.e. suffer from the maintenance and performance costs of low-level languages), while your low-level programs pay for the high-level features (the complexity needed for implicitness and safety). So yes, you can do everything, but rarely as well as could be done, and while I see the value in getting expertise only in one language, 1. it's a language that requires a lot of expertise as its "multi-functionality" makes it very complicated, so much so that you could probably become an expert at two more specialised languages for not much more effort, and 2. I think that if you really need to write low-level code, e.g. you're writing a kernel or a hardware driver or a controller or a GC, then expertise in the domain dwarfs expertise in the language anyway (i.e. we're talking years of required experience until you're really good at it).
BUT I acknowledge that the weight I assign to these things is subjective, and I'm certain others reach the opposite conclusion through arguments that are no less reasonable than mine.
slopinthebag 23 hours ago [-]
I think it's also a matter of experience - someone used to writing code in unsafe low level languages has a different approach to solving problems and may find Rust gets in the way. I actually started with C++ and after writing a reasonable amount of it I found myself wondering why I had to keep track of lifetimes, nullability etc in my head when it was so easy to mess those up. I kind of discovered "why Rust" from first principles and from then on I was hooked.
I'm not sure I understand your point about dynamic dispatch being slow in Rust/C++ or shared objects? If you're targeting native (which I find important) neither Java or C# are going to be faster surely. Maybe if you're willing to run Java/C# JIT you might find some wins (skeptical it's faster across the board) but you also don't need dynamic dispatch in performance-critical areas. I rarely reach for a Box<dyn Something> even in my high-level code.
I haven't worked on large Rust projects (> 500k loc) so I can't speak to the maintenance costs of that, but for me it doesn't matter (at least yet).
But we see even experienced professionals making mistakes with low level languages and I think it's worth considering if it's worth some of the cons you bring up to avoid those. Kind of reminds me of Carmack talking about static code analysis years ago:
> The more I push code through static analysis, the more I’m amazed that computers boot at all.
pron 22 hours ago [-]
> I kind of discovered "why Rust" from first principles and from then on I was hooked.
I get it. The language certainly does appeal to some people, and I can understand why, just as I understand why it does not appeal to others.
> I found myself wondering why I had to keep track of lifetimes, nullability etc in my head when it was so easy to mess those up.
And I agree with that, but my conclusion (after decades of experience with low-level programming) is somewhat different: Don't reach for a low-level language unless precise low-level control over the hardware is the exact thing you're after. And when that is the case, I find that safe Rust doesn't offer the control I need, and unsafe Rust (and/or a lot of custom code) is not what I want to use.
> Maybe if you're willing to run Java/C# JIT you might find some wins
Of course I use the JIT. That's exactly what it's for. Now, I don't care if the buffer from which the CPU reads instructions is memmapped from a file or generated by a JIT, but I do know that some people like the "single native file experience". To that end, we're working with Google to add a small feature to the JDK that would allow it to link the JVM, other native libraries, and Java classes into a single native executable (it's still going to JIT the Java code, but you'd be launching a "native binary").
> (skeptical it's faster across the board)
I wouldn't say it's faster across the board. You sometimes can write large programs in C++ (or Rust) that match and even exceed Java's performance, but it gets harder and harder the larger the program is. On average, I find that the "effort per performance" is, on average, significantly lower in Java in large programs. And it's not just the JIT. Another weak point of low level languages is that their pointers can't move, which means they can't use moving collectors, which also offer superb efficiency, again, mostly in large programs when you have lots of objects of varying sizes and lifetimes (especially now when we no longer have GC pauses).
> but you also don't need dynamic dispatch in performance-critical areas
You certainly don't start out needing it. Over time, however (and important codebases last at least 15-25 years), it either creeps in or it affects sufficiently many less critical paths to make an impact. You can try and re-architect things, but it takes a lot of effort (and it's this evolution effort that was a major reason for C++'s decline).
> But we see even experienced professionals making mistakes with low level languages
Absolutely, but my prescription would be to avoid low level languages altogether, and that has indeed been the industry's trajectory, and it's continuing. And when you absolutely do need to kind of control that low level languages offer, language complexity can also cause (or help hide) mistakes in code that is often very subtle, and the added safety, which is partial at best in those situations, isn't enough to offset that. Again, this isn't universal, but there are reasons to avoid Rust in low level code that are just as good as the reasons to pick it, and so different people will choose differently.
BTW, I've never worked on a browser, and it may well be the Rust is the best language for that, but I would be very curious to try Java. First, modern browsers run a lot of JS so you have a JIT and a GC, anyway, and so it might be both easier and more efficient to have everything use the same GC, and while process isolation would have required Java to re-JIT the rendering pipeline, Java is about to allow sharing JITted code (and even caching it from one run to the next) so that there would be no need to warm up the same code over and over.
slopinthebag 19 hours ago [-]
I think the only argument I’d make about high level programming languages is that software continues to outpace hardware development in sucking up as much performance gains as possible. One program written in a slower, garbage collected, high level language is ok, when they are all it’s bad. I think eventually we’ll get to a point where we won’t have to think about memory management anymore but we aren't really there yet. Heck, software written in C++ like browsers are dog slow, imagine if they were written in Java…
pron 19 hours ago [-]
I originally came to Java because of the better performance it offered compared to C++ in large programs. The JVM is specifically designed to remove some of the fundamental performance overheads that low-level languages suffer from, and manifest especially when programs grow large (and a browser is quite large). So when someone talks to me about "GC languages" being slow and low-level languages being fast, I know they've not had much experience with either Java or low level languages, nor do they understand modern compilers and memory management. Java offers strictly more optimisation opportunities than low-level languages, in compilation as well as in memory management. What it gives up in exchange is low-level control (including worst-case performance), but it is low-level languages that sacrifice performance (especially average-case performance) in exchange for the control they need. Not being able to move pointers freely and not being able to deoptimise and recompile at runtime are serious impediments to modern optimisation, but low-level languages gladly give that up because they're not optimised for performance but for precise control.
In particular, modern moving collectors were designed for the purpose of removing the high overheads of malloc/free allocators that make heap memory management so expensive in low-level languages (and in any language that uses non-moving memory management strategies). The reason code in low-level languages tries to avoid things like heap allocation and virtual dispatch on the hot path is not because these things can't be super-fast (most virtual calls in Java are faster than many static calls in C), but because they are slow in low level languages because of their constraints.
slopinthebag 17 hours ago [-]
I don't think it's true that Java offers strictly more optimisation opportunities than low-level languages, but rather different optimization opportunities. C++ and Rust have other opportunities that Java does not generally have:
- Explicit object lifetime and deterministic destruction.
- Stack allocation by default.
- Value types and direct embedding of values in data structures.
- Precise control over data layout, alignment, padding, and SIMD-friendly representations.
- Explicit allocation strategies: arenas, pools, slab allocators, region allocators, custom allocators, memory mapping, etc.
- No mandatory tracing, write barriers, object headers, or garbage-collector scheduling.
- Native interoperation without an FFI boundary.
- More predictable latency behavior.
- Compile-time specialization through templates in C++ and monomorphized generics in Rust.
Rust’s ownership and borrowing model can also provide strong non-aliasing and mutability guarantees in safe code which are useful for optimization.
Saying that "Low-level languages sacrifice performance for control" is also not true imo, since they can avoid allocation entirely, store data contiguously rather than as individually allocated objects, avoid all gc work, control cache behavior and eliminate pointer chasing, and importantly, guarantee hard or soft latency bounds.
Saying that "Most virtual calls in Java are faster than many static calls in C" is not a meaningful claim either. I think? Cuz "it depends" :)
And remember that GC is not free. Objects must eventually be traced. Reference writes may require write barriers. Objects have headers and alignment overhead. Heap size must often be larger to maintain throughput. Concurrent collectors consume CPU and memory bandwidth. Stop-the-world phases or other latency issues remain even with modern collectors. Object movement can complicate native interoperability and pinning. Malloc can cause performance issues but Java is not beating an arena allocator in C++ or Rust.
Keep in mind speed and throughput are not the only performance metrics. So is startup time and memory footprint, where Java loses badly here.
I wish we had real published research to go off of here but real world examples are all we really have. I'm not seeing AAA game studios building their engines in Java. I don't see any OS's building their kernel (or anything really) in Java. If Java is faster than low level languages, why is that?
And look, I don't dislike Java or the JVM, and I'm actually a really big fan of C#. I just like Rust more.
pron 8 hours ago [-]
> Stack allocation by default, Value types and direct embedding of values in data structures.
This used to be the big one, but not anymore: https://openjdk.org/jeps/401 (so Valhalla is integrating in JDK 28; it's not complete and this JEP is only the first step, but Java will have everything it needs on that front very soon.
> Precise control over data layout, alignment, padding, and SIMD-friendly representations.
Since the JVM controls layout, this is a point for Java.
> No mandatory tracing, write barriers, object headers, or garbage-collector scheduling.
Java doesn't require these things. JVM implementations choose to have them as an optimisation.
> Explicit object lifetime and deterministic destruction.
Hypothetically, this could have been an optimisaton opportunity. In practice, this is not a problem for Java but it is a problem for low-level languages, and especially Rust. The problem isn't knowing when an object's life is over, but needing to do something about it then and there. The whole idea of moving collectors (and arenas) is that it is more efficient to do nothing when an object becomes unreachable, and knowing when that happens doesn't help.
> And remember that GC is not free
Of course it isn't, but moving collectors are cheaper than free-list-based approaches, which is why we use them (most objects are never traced; those that are, are traced rarely etc.). On the whole, moving GCs are a speed improvement, reducing the overheads in C's runtime, and what they take in exchange is footprint (i.e. they use RAM chips as hardware accelerators). Now, that footprint cost could be expensive in smart watches and smaller devices, but in larger ones, the tradeoff is almost always worth it. I gave a talk about exactly that at Java One that should be up on YouTube eventually.
> Saying that "Low-level languages sacrifice performance for control" is also not true imo, since they can avoid allocation entirely, store data contiguously rather than as individually allocated objects, avoid all gc work, control cache behavior and eliminate pointer chasing, and importantly, guarantee hard or soft latency bounds.
I don't agree with that, and that's the very crux of my point. What you're really saying is that fine-grained control over the hardware lets a user who
works hard enough to optimise their program to any level they choose. This does work well in small programs, but it fails in larger ones, and the JVM is designed to solve the performance issues that we C++ programmers experience in large programs. Over time, as programs grow and become more elaborate, it gets harder and harder to do those manual optimisations, which are very intrusive. E.g. you could perhaps use arenas in Rust more-or-less safely, and maybe Rust will make it easier in the future (right now the only language that makes that easy is Zig), but changing from no-arena to arena or vice versa, or finding out that you need special cases to some objects, requires a huge change to a low-level program. Similarly, trying to keep virtual calls to a minimum is very easy in the beginning, but becomes harder over time. So the idea that with enough control you can do anything is true in principle, but in practice it's very hard as programs evolve. The idea of modern runtimes is that you write the code naively, and the runtime performs global optimisations that help the average-case performance.
> I'm not seeing AAA game studios building their engines in Java. I don't see any OS's building their kernel (or anything really) in Java. If Java is faster than low level languages, why is that?
Much more performance-sensitive software is written in Java than in C++ these days, and your question assumes that the main thing that's important in these particular is speed, but that is not the case. What is the main thing kernels need to do? Directly control hardware. And what is the one thing that low-level languages are optimised for? Direct control over the hardware. Low-level languages fit the domain of OS kernels like SQL fits data queries; that's what they're for.
As for games, first, performance isn't the issue here. The most performance-critical parts of a game are not written in C++ but in CUDA, and the main important part for the CPU is a good algorithm for scheduling the data to the GPU, and that can be done in any language. What is very important for games is hardware support, and the JVM simply doesn't target most consoles. Second, games do care about latency, at least up to the length of a frame, and until very recently Java had GC pauses, and those could sometimes exceed the latency needed by games. GC pauses were removed in HotSpot only 3 years ago. So these are the reasons game engines are normally written in C++ (except, of course, for the most successful game in history, which is written in Java).
duped 1 days ago [-]
People find it immensely useful in practice, though.
vlovich123 1 days ago [-]
Java and Rust have actually very similar memory safety profiles. Rust let's you within the language escape the memory safety requirements whereas Java does not but both are considered memory safe languages. Rust also enforces thread safety as well which Java does not, but the slower JVM memory model doesn't let race conditions become memory safety issues whereas Rust is lower-level like Zig/C/C++ and thus thread safety could be a memory safety issue.
Zig has an identical memory safety profile to C. It has facilities to make it easier to stay memory safe, but those facilities are basically equivalent to what you have in C++ and that's equivalent memory safety profile as C.
> So among these four languages we already have four levels of memory safety, none of them is 100%
No, you've pretended like there's four when really it's Java / Rust which are safe by default and Zig/C/C++ which are unsafe by default.
One effective metric to evaluate is memory safety per LoC. Rust is ~0.2 vulnerabilities per MLoC. Java is effectively 0. C and C++ both seem to be about 1,000 vulnerabilities per MLoC. Zig is too new and hasn't had any analysis done on it, but generously it's likely at least 10-100.
So the table stakes could be defined as 1 memory safety vulnerability per MLoC.
xedrac 1 days ago [-]
Java may be memory safe, but no memory is safe from the JVM. :)
pron 1 days ago [-]
> Java and Rust have actually very similar memory safety profiles
They really don't. Look at how many basic data structures (in the standard library or outside it) require unsafe features in Java vs Rust.
> Zig has an identical memory safety profile to C
It really doesn't. Zig gives you the same spatial memory safety as Rust and very much not like C (and violations of spatial memory safety are a bigger cause of vulnerabilities than violations of temporal memory safety).
kibwen 1 days ago [-]
> Look at how many basic data structures (in the standard library or outside it) require unsafe features in Java vs Rust.
This is a fundamental misunderstanding of how "unsafe" code relates to a platform's trusted computing base. Rust could move all of those unsafe data structures out of the standard library and into the compiler itself, thereby reducing the amount of occurrences of the string "unsafe" in the source, code, but this would do nothing to reduce the size of the trusted computing base that Rust presents. In fact, it would decrease our confidence in that code, because Rust libraries have a robust ecosystem of tools for validating their correctness, unlike whatever bespoke IR the Rust compiler itself is emitting. Java's own data structures are implemented with the support of an extensive runtime written in C++, which forms their own trusted computing base that every user of Java relies upon, and demands just as much careful auditing as any data structure in the Rust standard library.
pron 2 hours ago [-]
> This is a fundamental misunderstanding of how "unsafe" code relates to a platform's trusted computing base. Rust could move all of those unsafe data structures out of the standard library and into the compiler itself, thereby reducing the amount of occurrences of the string "unsafe" in the source, code, but this would do nothing to reduce the size of the trusted computing base that Rust presents.
No, you're missing the point, which is that you cannot write some common data structures in safe Rust (whether they're implemented in the standard library or in the compiler), but you can in Java (inside or outside the standard library). In other words, the point is that safe Rust is quite restricted.
> Java's own data structures are implemented with the support of an extensive runtime written in C++, which forms their own trusted computing base that every user of Java relies upon, and demands just as much careful auditing as any data structure in the Rust standard library.
No, because these data structures are not part of the trusted computing base, which is fixed and closed. In Rust, that base has to be extended to any third-party code that uses unsafe, which is needed in many more situations. In fact, the need for unsafe code in Java has been so reduced that we're currently in the process of removing Unsafe altogether, making it inaccessible to third party code, which would still be able to write the data structures that would be unsafe in Rust in safe Java (the only unsafe thing remaining would be the FFM API, used for FFI, and the legacy JNI, used for that same purpose).
It isn't controversial that the amount of stuff you can do in safe Java is significantly higher than the amount of stuff you can do in safe Rust. That's just obvious.
bluecalm 1 days ago [-]
The point is not that those specific implementations use unsafe Rust but to illustrate that to write even basic data structures you need unsafe Rust.
afdbcreid 23 hours ago [-]
That's just false. You can use `Arc` or even one of the safe GC crates available, and get semantics like Java with no `unsafe`.
pron 2 hours ago [-]
You can't get the same semantics (e.g. no leaks) and you certainly can't get the same performance.
bluecalm 8 hours ago [-]
Yeah but that doesn't work for any kind of performant code which is the reason people who write those data structures use unsafe.
This is one very annoying thing about Rust community. The language sucks for coding self-referencing data structures with unpredictable free patterns. This is a fact and the reason number of people on this very forum posted long articles about moving away from Rust for those purposes.
Your "actually you can" post is just misleading and will result in more people who will get burnt but the design of the language.
afdbcreid 3 hours ago [-]
No, you really can. You can use GC crates and the performance will be like Java, or you can use Rc and the performance will be like Swift. The only reason Rust people use unsafe for data structures (and they do not always do) is that for them, Java/Swift-level performance is just not enough.
pron 1 hours ago [-]
> No, you really can. You can use GC crates and the performance will be like Java
It won't be anywhere near Java's. Those GCs are mark-and-sweep collectors. Java uses moving collectors. Moving collectors are used to avoid the high overheads of malloc/free in the C runtime (or of any free-list-based mechanism). They're a performance optimisation. Heap allocations in Java behave more like arenas than like heap allocations in languages with non-moving memory management, whether it's C, Rust, Python, or Go. Other runtimes that use moving collectors are Google's V8 and Microsoft's .NET, except that Java's ZGC has no GC pauses.
applfanboysbgon 1 days ago [-]
> Rust is ~0.2 vulnerabilities per MLoC. Java is effectively 0. C and C++ both seem to be about 1,000 vulnerabilities per MLoC. Zig is too new and hasn't had any analysis done on it, but generously it's likely at least 10-100.
You have just described six orders of magnitude in your attempt to rebut pron pointing out the four languages have four levels of memory safety.
joshuamorton 1 days ago [-]
"two things are within an order of magnitude, and two other things are within an order of magnitude, and those two groups are three orders of magnitude apart" does sound like two groups to me.
applfanboysbgon 1 days ago [-]
> those two groups are three orders of magnitude apart
They aren't necessarily, though. Supposing that Zig were "10 issues per MLoC" (with just as much handwaving as the original poster), it would be equidistant from Rust and C. Java may also be more than one order of magnitude away from Rust; we say ~0 but is it 0.01, 0.001, 0.0001...? And why is "1 issue per MLoC" the acceptable metric that delineates what constitutes table-stakes memory-safe language? Because it's a nice, round-sounding number? I think 0 is a nicer, rounder number than 1, so let's call only Java table stakes and condemn all other languages to the garbage bin, tradeoffs be damned. Or would you say your arbitrary delineation point is worth more than mine?
vlovich123 16 hours ago [-]
You’re making an assumption based on zero information in a very favorable to zig way. When you have no information it’s best to rely on good priors grounded in what you do know.
zig as a language not distinguishing between safe and unsafe. Zig does not forbid unsafe. Zig’s safety model is conceptually not too different from running with ASAN for C/C++ during development. Given that’s already a followed “best practice” for the data that was used to come up with the vulnerability estimate per MLoC for C and C++ it’s reasonable to assume zig is closer to that side (at scale) than to Rust. You could argue defer as a keyword is worth a 10x improvement but even then im not sure how considering c and c++ are close and c++ does essentially a similar thing
joshuamorton 1 days ago [-]
> Or would you say your arbitrary delineation point is worth more than mine?
Yes for the reasons I already gave. I think that at the point that you're having to stretch the numbers from their post to the breaking point to remove the pretty clear order of magnitude differences it's not really a constructive way to engage.
I think you have two groups with one at ~.1 and one on ~100. You seen to either disagree with that, or think it doesn't matter, I'm not sure which. But taking that assumption as true it is self evident that the 3-order-of magnitude demarcation is not arbitrary.
applfanboysbgon 1 days ago [-]
I think it is absolutely arbitrary. First because I believe every order of magnitude is significant. You handwave away that one order of magnitude difference is fine but three is bad. I think this is hypocritcal, and that if you want to be a memory safety purist who ignores all tradeoffs and declares a language fundamentally unusable on safety grounds, even a single order of magnitude of additional issues should clearly be unacceptable. And simultaneously you group them arbitrarily -- what is your actual basis for suggesting that Zig code is more likely to be 100 than 10, or that 10 somehow bares grouping at 100 rather than grouping .1 and 10 together at 1? You also fail to address that there may be more than one order of magnitude between Rust and Java. It is entirely plausible that Rust is closer to Zig than to Java.
---
Rate-limit edit replying to below response:
> a variety of statements I haven't said
We are in a conversation thread specifically about statements of this nature, which I was contesting. The original poster of this thread called their arbitrary definition of memory safety "table stakes" and explicitly said that they rule out Zig as a language completely on this basis alone. If you don't agree with them, I'm not sure what we're discussing.
> you take issue with the assumptions I'm making ... but you could just state that instead of saying that I'm being hypocritical
The only assumption I disagreed with is assigning Zig to exactly 100 when a poster I was replying to originally asserted a range of "10 or 100"; and regardless of whether we agree on assigning a concrete lower/upper bound to that assumption, everything else is not an assumption but a value judgment given the condition "assuming the premise holds". Yet your position is taking those value judgments - which orders of magnitudes to accept, which to group together as being the same degree of memory safety - and asserting them as objectively correct boundaries with minimal rationalisation beyond "because I feel it is so".
joshuamorton 1 days ago [-]
Yes, it's clear you take issue with the assumptions I'm making. That's okay. If you don't accept my premise that's fine, but you could just state that instead of saying that I'm being hypocritical or strutting out a variety of statements I haven't said ("you want to be a memory safety purist who ignores all tradeoffs and declares a language fundamentally unusable on safety grounds").
If you want to have a conversation with me about the things I'm talking about, I welcome it, but I don't see that happening.
joshuamorton 1 days ago [-]
> Yet your position is taking those value judgments - which orders of magnitudes to accept, which to group together as being the same degree of memory safety - and asserting them as objectively correct boundaries with minimal rationalisation beyond "because I feel it is so".
Yes, like I keep saying: two clusters each within an order of magnitude, separated by three orders of magnitude feel to me like two distinct things. That does not at all feel arbitrary. I think that claim is pretty self-explanatory. You appear to think it reduces to "because I feel it so" and in some sense it does. I am applying my own judgement and values in constructing those clusters. Someone who felt that any amount of memory safety was unacceptable would structure them differently. Someone who cared naught about memory safety would similarly group them differently too.
throwaway2037 6 hours ago [-]
> Java lets you do many things programs may want to do in a memory-safe way but not everything.
Can you give an example of Java lets you do that is not memory-safe? Honestly, I don't really count the crazy off-heap tricks that some people use. It's almost like writing a Python library in pure C, intead of Python, then complaining that Python allows you to do non-memory-safe things.
nextaccountic 23 hours ago [-]
safe Rust is actually more memory safe than Java, since it guards against data races (in Java data races are not UB, but they are still one of the worst kinds of bugs because it leads to logically impossible program states)
Also note that Java has unsafe, but doesn't have the culture of plainly stating safety invariants like Rust. The unsafe features of Java are less widely used, but when they are you rarely know if a Java library has unsafe internals for performance, and if they do, it may be hard to audit
imtringued 12 hours ago [-]
Pointer aliasing by default was a mistake. I won't budge on this one. The borrow checker is a good idea independent of memory safety. In fact, I would say that if you think that the borrow checker is a tool to ensure memory safety and it is getting in the way of flexibility, you probably don't understand the underlying problem.
The memory safety given by Rust is obviously not airtight, because the aliasing case requires either unsafe blocks or reference counting, but you fundamentally give something up by allowing pointer aliasing that you can never get back once the genie is out of the bottle. The painfully constricting flexibility so reminiscent of C that everyone else clings to is not where I want to go back to.
nh2 1 days ago [-]
10 years ago, I commented on the Rust issue for "Incremental recompilation", where it was suggested that Rust could at least adopt Haskell GHC's model of incrementality, which is currently file-level:
I recommend anybody who's interested in incremental recompilation to read what GHC does, because the effort to achieve that is relatively low.
Of course there's always desire for more:
GHC currently needs to parse+typecheck+codegen a file before it can process other files that import it. Codegen is slow. Thus, there's currently demand split compilation into "stages", so that the next file can be typechecked after its imports have been just typechecked (not codegenned).
I would also enjoy if recompilation avoidance were to happen at the function level, not the file level.
Macro systems are a key language feature that can destroy incremental recompilation. In theory, Haskell is well set up for that, as its macro system (TemplateHaskell) is fully AST based and _theoretically_ could distinguish "fully pure" macros from side-effectful macros (such as splicing the current git commit in as a string literal). But the recompilation avoidance system does not currently exploit such differences.
steveklabnik 1 days ago [-]
Just to be clear about it, Rust today does do some amount of incremental compilation, and there is more work being done to continue to make it moreso. It's just very difficult to re-architect such a large and heavily used codebase. People are putting in heroic amounts of effort to improve things.
> Haskell GHC's model of incrementality, which is currently file-level
From what I see in Haskell files are the unit of compilation, and that's what allows incremental compilation to be file based (because it's really unit-of-compilation based)
I can see you can have circular dependencies between files with the `{-# SOURCE #-}` pragma, but I don't see documentation about how that affects incremental compilation.
A couple of issues I see with doing this in Rust are:
- in Rust the unit of compilaion is a crate, which can contain many fils/modules with circular imports, which is much more coarser than what can be done in Haskell.
- in Rust downstream crates can depend on function bodies upstream for running compile time functions; as such the crate/module interface is not enough to gate recompilation, but at the same time including all function bodies will also not give the wanted benefits. This is solvable but likely requires more work than what was done in Haskell.
In general you cannot take a language approach and blanket applying it to another one without considering their different quirks, which is likely why your proposal didn't get much attention. Or am I missing something that would make it easier to apply Haskell approach here?
panstromek 15 hours ago [-]
> GHC currently needs to parse+typecheck+codegen a file before it can process other files that import it. Codegen is slow. Thus, there's currently demand split compilation into "stages", so that the next file can be typechecked after its imports have been just typechecked (not codegenned).
> I would also enjoy if recompilation avoidance were to happen at the function level, not the file level.
This sounds like Rust is already doing a lot more incremental then GHC then. Rustc only needs to parse, expand macros and do name resolution. Everything else is incremental after that, on a very granular level.
amelius 1 days ago [-]
Can't we have a system where we trade some performance for quick incremental compilation?
We can always compile with full optimization just before shipping?
steveklabnik 1 days ago [-]
The article gestures at (and the author has made a comment in this thread about) how this is the case for Zig. You are right that there is tension here, and so that's exactly what you do: accept less performance for the gains in incremental, and then don't do incremental for final builds. It's a fine way to go about it, assuming that the lack of performance doesn't make the program unusuable. (Some people add some basic optimizations to their Rust debug builds, for example, because no optimizations is too painful to actually use.)
pjmlp 1 days ago [-]
Of course we can, C++ even REPL and hot reloading tools.
The main issue is that so far such tools haven't been a priority for Rust.
hoppp 1 days ago [-]
The thing with rust is that you get safety with slow compilation, it's a tradeoff.
Zig doesn't have the same safety guarantees, it's on the dev to use safe coding patterns, so the tradeoff for safety is discipline or experience.
steveklabnik 1 days ago [-]
Rust's safety checks have basically nothing to do with its slow compile times. This is something that sounds intuitive but is just completely incorrect.
In particular, Rust made several good design decisions around this stuff that keeps those checks fast, like keeping checks local rather than being global.
jamiejquinn 1 days ago [-]
That's interesting. Coming from C++ and Zig, the massive time "wasters" are metaprogramming features, i.e. Templates and comptime. Are Rust's macros the compile-time culprits?
norman784 1 days ago [-]
The issue with Rust proc macros is that they are impure, so even with incremental compilation, you need to expand them every time.
afdbcreid 15 hours ago [-]
That's only part of the issue. First, declarative macros are of course pure. Most procedural macros are pure too, although there are exceptions (an infamous case is sqlx). And while rustc does expand them every time (although a config for declaring "my macro is pure" was considered multiple times and the teams generally view it favorably), rust-analyzer just declares "we don't support impure macros" and caches them anyway.
steveklabnik 1 days ago [-]
Macros can be, but in part because they can produce new items (top level declarations, to sort of make the same handwave as the article does) and so that means you have to do macro expansion and stuff before you can even start to check some things, and similar issues. See the link I posted above for some details on a related issue.
There's also stuff around name resolution.
Proc macros are just an inherently very slow way to do what they do.
Because Rust commits to the traditional compilation model and pipleine (which I think is overall a good thing, or at least, a good thing to support), it does a lot of work that will eventually be thrown away. Consider this example: I have a library with a function foo that returns a simple 42. I have a binary which calls foo from that library and prints the result. Now imagine the library is a hundred thousand lines of unrelated code to what the binary needs, but is useful for other people. Because compilation works in the "produce libraries, produce binary, link them all together" style model, you have to compile the entire library with all of that code, when all you need is really one function. That intermediate work is useful, and I'm picking an example that's deliberately extreme, of course.
There's a bunch of stuff like this, and I do not have time to really say more than that right now. But yeah, monomorphized generics also produce a lot of compile time pressure too, in various ways.
Anyway I just also want to reiterate a few things: first of all, all of these decisions were made for good reasons, and there are pros to what Rust does and why. It's just that compile times suffer because of it. What I wish was that we had taken compile times into more consideration when deciding what to do and why in a more serious way. The same decisions might have been made, but at least it would have been known, rather than the situation now, where there's just a tremendous amount of work to try to optimize what exists, rather than having the freedom to maybe tweak some things to make that job way easier.
xenadu02 22 hours ago [-]
The traditional model of compilation is the biggest issue holding back fast incremental compilation IMHO. Swift suffers from this as well.
Even a simple change to one file results in re-parsing the whole library because definitions come from anywhere and we have to obey the 1970s single file compilation model. The result is a driver spawns 8 threads and each one wastes time re-parsing every file in the library looking for definitions. AFAIK Rust doesn't really track dependencies at the file or function level either so it doesn't really know what changed.
To me compilers should be content-addressed databases. Each declaration and its associated content generate hashes that roll up to its containing type or namespace, then to the file, then to the library as a whole, along with hashes of the dependencies. Changing the type signature of a single function should result in the compiler being able to cheaply determine whether that has any visibility and if so to what other files in the same library or if it affects the public interface.
A file that hasn't changed and whos inputs hasn't changed should re-use the IR from the prior compilation. Even for an individual type that should be the case so changing the internals of a function in a struct only regnerates that one function and nothing else. The compiler knows deterministically that change can't have affected anything else.
That has major benefits for code completion and editing as prior compilations can feed into generating errors or suggested corrections.
Then you can take things a step further and JIT a changed function, injecting the new machine code on the fly so long as the shapes of the types don't change. Very useful for debugging.
Compilers are mostly held back because the people who write compilers are stuck on certain ideas about how compilers should be written.
panstromek 21 hours ago [-]
> To me compilers should be content-addressed databases.
There's a language called Unison that does that - and the "content" is the AST, so all functions that have the same shape are the same function. It's pretty interesting.
SkiFire13 15 hours ago [-]
> To me compilers should be content-addressed databases
Rustc already does something like this. The issues are:
- when you have to rehash everything to check that they indeed didn't change from the previous compilation. For big projects this takes _a lot_ of time.
- when small changes do indeed change the hash of a lot of seemingly unrelated code, which is more common than you might think.
tancop 11 hours ago [-]
You dont need to rehash everything if you do multi level hashing (file -> mod/impl block -> function). The expensive part is doing lots of hash passes over small parts of a file, so if you break as early as possible the moment you can guarantee that part is unchanged you save a lot of time. And if you assume (and document) that libraries need to be rebuilt manually you can completely skip checking them.
OP handles the small change problem by hashing IR instead of source text. If the new function compiles to the same IR as the old one its guaranteed to give the same machine code. You should repeat this after each lowering or optimization pass so functions with different HIR but same MIR are also marked as unchanged and dont cause items up the tree to rebuild.
mlugg 10 hours ago [-]
> OP handles the small change problem by hashing IR instead of source text
OP here---I think you got confused somewhere, because this isn't true! The hashes we take are of source code, they're just stored inside of the first IR. Source code is just pretty convenient to hash. If the change does turn out to be something trivial, we'll only invalidate the first-order dependencies of the source hash, which is never usually a big deal. In a particularly bad case, maybe we re-analyze, re-codegen, and re-link many different instances of a generic function, but even that wouldn't usually take long at all.
Also, it's actually important that we do notice these kinds of "trivial" changes in some parts of the pipeline---see Andrew's comment [0] on the Lobsters discussion for this post.
A really dumb 1 AM question. If there is a lot of work thrown away because stuff is compiled even if not needed, would making every function generic and delaying compilation until instantiation help here?
Note that it's not a serious suggestion, but I wonder what effect it would have on build times.
SkiFire13 15 hours ago [-]
The downside of this is that if two crates use the same function each of them will have to codegen it, duplicating the work needed for those functions.
As always this could be avoided with some extra complexity, but that require lot of work and testing that hasn't been done.
So for now this is useful only in crates that have a lot of unused functions, so even if one function is codegenned multiple times you still save time overall.
panstromek 21 hours ago [-]
Yes, and there is some compiler flag to do this, even though it's not 100% intended for this use (I think it's something like mir-inline-trheshold=0).
There's also -Zhint-mostly-unused flag.
The tradeoff is that these functions then have to be encoded in metadata for downstream crates, so it's not necessarily faster.
steveklabnik 21 hours ago [-]
I mean, the core thing is like, you have to have a compiler codebase (and language semantics) that's designed around being able to delay in the first place to be able to even try this, and once you've gotten that in place, well, it's not really about this specific idea anymore.
tancop 11 hours ago [-]
Would this work better in a language where generics are vtable based by default and only monomorphized as a form of LTO? If you have a Zig style machine IR where function calls are a "fake instruction" you can decide between direct and virtual call at final codegen time.
That would mean generics in shared libraries are possible without hacks but you can always choose to statically link and monomorphize for speed. Libraries could even ship both the vtable based catch all code and specializations for common types as a fast path (but that would probably need a custom dynamic linker).
steveklabnik 5 hours ago [-]
It really depends on a lot of things. This design is possible, yes, but it also means that you can't have some of the same features as Rust has. Rust's traits can be "non-dyn safe" (which used to be called "non-object safe"), because sometimes you cannot use them via a vtable, and must use them via monomorphization. So you can have languages that do things this way, but they make other tradeoffs (either performance ones or power ones, depending).
saghm 1 days ago [-]
> Because Rust commits to the traditional compilation model and pipleine (which I think is overall a good thing, or at least, a good thing to support), it does a lot of work that will eventually be thrown away. Consider this example: I have a library with a function foo that returns a simple 42. I have a binary which calls foo from that library and prints the result. Now imagine the library is a hundred thousand lines of unrelated code to what the binary needs, but is useful for other people. Because compilation works in the "produce libraries, produce binary, link them all together" style model, you have to compile the entire library with all of that code, when all you need is really one function. That intermediate work is useful, and I'm picking an example that's deliberately extreme, of course.
I feel extremely validated reading this! I've had a pet theory for a while that compile times would be drastically reduced if every single item in a crate were implicitly under a cargo feature flag and then they only got enabled if they were imported (and used in non-dead code). I know that making something like that work isn't anywhere close to as simple as I'm describing it, and there are probably a million edge cases, but I've long felt that the ergonomics of Cargo features basically making it too annoying to expose everything conditionally (and then transitively expose all of the features from all of the direct dependencies as well so that things depending on your library could also only conditionally enable them) is secretly the reason that people think Rust compile times are slow, and seeing someone who has way more direct knowledge than me of how all of it works under the hood give a similar take makes me more confident that I might have been on to something all along.
steveklabnik 1 days ago [-]
Yes, this idea is sort of similar, just like, more complicated in a sense than the clean design, which is what Zig does.
The sort of key here is understanding that "produce a library" means that every public item is "used" in the sense of "do we need to compile it." The real trick is to do demand-driven compilation starting from the actual final program's needs, and this is inherently at odds with the idea of producing standalone libraries and combining them into the final artifact.
saghm 1 days ago [-]
Makes sense! I think what's most surprising to me about the library compilation model is that for the most part, it doesn't seem like it's actually that useful to how Rust does things by default. Without something like sccache, the intermediate libraries aren't going to be reused across all of my projects, so unless I'm compiling multiple binaries in the same project, I'm not really getting much out of having the entire library sitting there in my target directory rather than just the subset that I'm using. I'm guessing this is related to what you mean by potentially being able to do something differently if there were more time before 1.0?
steveklabnik 1 days ago [-]
I think that there is a general desire to work with the system, and since that is how other systems languages have generally done it, doing it the same way feels good. You don't necessarily want to innovate on everything all at once.
It is true that for various reasons, Rust can't take as much advantage as say, C can.
> I'm guessing this is related to what you mean by potentially being able to do something differently if there were more time before 1.0?
I mean more traditional language design things, but sure, this too.
saghm 1 days ago [-]
Interesting. My perception for why other systems languages generally compile things to libraries is that they tend to use dynamic linking from a single instance of libraries on a system. I would have expected that when static linking is the default, the argument for having standalone intermediate libraries is much weaker, since the deviation from the other systems languages has already been decided. Maybe I'm missing something about how choosing static linking by default but still supporting dynamic linking requires still having a library as the output always.
steveklabnik 1 days ago [-]
> My perception for why other systems languages generally compile things to libraries is that they tend to use dynamic linking from a single instance of libraries on a system.
This is both true and not the whole story. In C, the file (okay if you want to get REALLY technical it's not the file but I'm talking about 99.9% of computers and not some old mainframe platforms) is the compilation unit. You pass a .c in, you get a .o (or whatever for your platform) out. Producing a final binary is where the static vs dynamic choice really comes into play, but you end up with these intermediate artifacts because that is how the language is defined.
> I would have expected that when static linking is the default, the argument for having standalone intermediate libraries is much weaker,
It is weaker, sure. But that doesn't mean there aren't other advantages. For example, you can more easily parallelize a large workload by breaking it up into multiple intermediate libraries, and compiling those simultaneously, whereas doing it all in one compilation/translation unit requires compiler support for said parallelization. Especially if you're already writing a batch compiler, this is a much easier win than rearchitecting the whole thing.
skavi 1 days ago [-]
> The real trick is to do demand-driven compilation starting from the actual final program's needs
this is not so far off from hint-mostly-unused, no?
NobodyNada 1 days ago [-]
hint-mostly-unused defers codegen of a crate's functions until compiling a dependent crate where those functions are actually called. Therefore, unused functions will not need to be codegen'd.
The downside is that functions which are called from multiple dependent crates will need to be codegen'd in each of their dependents, so this can increase compile times if the crate is not "mostly unused."
So it's not quite as powerful as full demand-driven compilation, because of how Rust separates the compilation process into separate crates.
IshKebab 23 hours ago [-]
Can't you just cache them?
steveklabnik 1 days ago [-]
In my understanding, it's similar, yeah. I don't know enough about how hint-mostly-unused works internally to really speak to the specific differences here.
pjmlp 1 days ago [-]
Yet that doesn't prevent C++ to have REPL and hot reloading tools, use binary libraries instead of compiling the world from scratch, all of which allow for a much better experience.
stock_toaster 1 days ago [-]
(tongue in cheek) It seems recent history has shown that zig can get you to working software faster, then you can port it to rust once you are acquired or find market fit?
Maybe at some point in the future zig could add a rust compilation target ( like with `-ofmt=c` )...
chaz72 1 days ago [-]
I know this is mostly a joke but I have seriously wondered if “no hidden behavior” made it easier to port from Zig. Like, regardless of how easy or successful the port was in general, I think Zig’s explicitness may have worked in its favor.
dnautics 1 days ago [-]
just staple a borrow checker to zig. it seems pretty doable as per my experiments
afdbcreid 23 hours ago [-]
This post is really interesting. As a member of the rust-analyzer team, I cannot avoid comparing it to the situation in Rust land. Rust famously has not less (or even more) sophisticated system for incremental compilation, yet its compilation is way slower. I attribute that to two main things:
- Language design. Zig was designed for fast and incremental compilation, Rust is just not. For instance, the post states that Zig has four properties (layout, type, value, body) that the compiler has to track for changes. Rust has much more, to the point that tracking them statically is just impossible, so the compiler uses a query system that tracks them dynamically, which adds overhead.
- Compiler implementation. Rust is much more complicated to compile than Zig, and rustc is both older and bigger (10x-20x LOC) than the Zig compiler, making changing it way harder.
lsuresh 20 hours ago [-]
Isn't most of Rust's compilation overhead from the llvm backend?
afdbcreid 18 hours ago [-]
That is common wisdom but reality is more complicated. It's true in some projects, but not all.
simonask 14 hours ago [-]
Every analysis of the problem I've seen has concluded that the main problem is that rustc generates a lot of input to LLVM. Efforts to reduce compilation times are currently focused on doing more to the IR before converting it to LLVM IR.
Rust as a language is even more reliant on monomorphization and inlining than C++, due to core language traits such as Deref, AsRef, From/Into, and so on.
That said, in practice my personal experience has been that Rust compares pretty favorably on compilation speed, even to some high-level languages like C#. On many developer machines, the actual slow part is linking.
This analysis is great, and I don't mean to knock it, but one of the things that it doesn't capture is, what choices does the frontend make that impact the amount of work that the backend does. That is, some of the work attributed to the backend could be improved without touching the backend.
afdbcreid 3 hours ago [-]
That is of course true, but if anything that just proves more what I said. Also that too is part of the "common wisdom" (that as I said, is not always true).
upboatsy 16 hours ago [-]
[flagged]
thefaux 1 days ago [-]
There is something that I don't fully understand about this design: why are they insisting on building a giant binary for debug builds that contains all of the code? From my perspective, a simpler approach is to generate many smaller shared libraries (perhaps at the file level) and link them in to the final binary. With this approach, the program binary would have a tiny text section and a (potentially long) list of shared libraries to load. But even with thousands of shared libraries to load, the resulting program binary would not be all that long and there would be no need for binary patching.
I understand that for a release mode a single giant binary may be desirable, but I am struggling to understand this design for debug builds. Moreover, while reading this article, I found myself wondering what happens if the main binary becomes corrupted. Maybe the user cancels compilation with ctrl+c while it is patching the binary. Even if they have a story for avoiding and/or detecting corruption, it is simpler to not patch in the first place and always generate a new main binary. Again, this is reasonable because the new binary is mainly just a list of shared libraries to link which will not take up much space and can be written to disk quickly. Moreover, this process can be done recursively, e.g. at the subdirectory level, so that during incremental linking a few quite small shared libraries may be produced rather than patching in the new code and writing cascading relocations.
dzaima 1 days ago [-]
A quick test (C, clang) gives me that a binary depending on 1000 shared libraries, each containing a single function returning an integer, with a main function summing up the results of all those functions, takes ~270ms to run from the dynamic linker overhead. So you'd definitely want a good chunk below thousands.
(a process with 100 shared libraries takes 6ms to run, which is a lot better (0.9ms for 1 library, for reference), but, especially with in-place patching skipping work on unchanged values, static linking still has a good shot at beating dynamic linking, especially if you run the binary multiple times)
Incremental compilation generally already depends on its stored intermediate data not getting corrupted, and the final binary need not be any differently handled in that aspect.
mlugg 12 hours ago [-]
Oh, I forgot to respond to the "corrupted binary" thing in my other reply, sorry.
Right now, yep, corrupting the binary that the compiler reads would crash the compiler. In future, we want to detect the corruption and force a clean build. Note however that the Zig compiler is writing the binary to its internal cache directory (typically `.zig-cache/`), and the build system then copies the final artifact to your output ("prefix") directory (`zig-out/` by default), so it doesn't matter if the user messes with the final binary in `zig-out/bin/my_program`, because that's just a copy. Lastly, this is not implemented yet, but we will definitely make sure that Ctrl+C leave the cache in a clean state. I'm pretty sure our incremental compilation system has a nice property that you can just cancel an update partway through and continue it later without too much effort. See also Zig's IO interface [0] for information about cancelation. The compiler should already support graceful-ish cancelation internally (I won't claim to have verified this, because we never actually do cancel compilation right now, but I'm not aware of any glaring issues!). I don't think it'd be a crazy amount of work to improve that so that it also leaves incremental compilation in a valid state.
IME dynamic libraries ever only add more problems on the pile but never solve anything (they're basically only good for plugin systems, or as userland operating system interface).
Shared libraries work slightly differently on each operating system, and at least on Windows and macOS, having to load thousands of tiny shared libraries will almost definitely cause your startup time to explode. I once wrote an 'OOP system for C' where each class lives in its own DLL, and let's just say that this was probably the most stupid idea I ever came up with, and I came up with a lot of stupid ideas in my life ;)
mlugg 12 hours ago [-]
Zig uses a "single compilation unit" compilation model, so even if we did what you're suggesting, pretty much everything in the post would still be relevant, especially the whole-file stuff and the semantic analysis stuff. Note that there's not really anything special about putting things in separate source files in Zig---files are just a unit of code organization, like namespaces. They don't help the compiler at all (aside from in that very first part of compilation, but that's fast regardless).
The only thing I discussed that we could potentially avoid using your suggested strategy is incremental linking, but AFAICT that would only be easily avoidable if we made a separate shared object for each individual function. I guess we could group them arbitrarily and then re-run codegen for everything in one shared library when the other parts change, but that frankly doesn't sound much less awkward than incremental linking!
But putting all that aside: the only thing this approach would really achieve is offloading the linking work from the static linker to the dynamic linker (aka runtime linker). So if it did make compilation faster, I'd expect all of the saved time to just become runtime execution time---which is kind of worse, because the average number of times you run a compiled program is probably >1!
Of course, there's also the obvious point that dynamic linking only works in cases where you can run dynamic executables. For instance, that approach wouldn't work when doing operating system development, while our approach should work completely fine for that use case.
chaz72 1 days ago [-]
If you choose to build static or dynamic libraries, I expect you’d still get to do that, and I expect as long as those don’t change and only the main binary needs patching it’d work the same. (Though I’m waiting for the next tagged release to really try it out myself, so that’s not like a guarantee or anything.)
anitil 21 hours ago [-]
I'm becoming a big fan of Zig every since learning about `zig cc` as a way of dipping my toes in it. I was already impressed by the build caching so I'm keen to play with this
patrec 24 hours ago [-]
> Dependencies on the body of a runtime function are impossible (at least in the simplified view I’m presenting here)
How does this work given that e.g. a constant can be computed by a comptime function?
mlugg 23 hours ago [-]
It works through the fact that I specified "runtime" function ;)
A bit after that quote I have a note about `inline` functions in Zig, where I mention that they perform semantic inlining, which means dependencies triggered by the function actually get associated with the call site. Well, `comptime` function calls work just the same way---in fact, to the compiler, `comptime` calls are almost exactly identical to `inline` calls. So when we encounter a comptime function call, we start analyzing the ZIR for that function's body, but we don't switch our analysis unit, so comptime stuff doesn't really complicate the dependency graph at all (aside from the fact that it means you can depend on any number of source code hashes, instead of everything depending on exactly one).
With all that being said, there actually is a (completely unrelated) way in Zig to can depend on the body of a runtime function (hence why the quote includes "at least in the simplified view I'm presenting here"). It's to do with "inferred error sets" (IESes for short). If a function's return type is written `!T`, that means it can return an error, but we're asking the compiler to figure out exactly which errors are possible. So if at some point we need to know that set of errors (e.g. because the user has done some reflection to try and access the list of errors), that's where we get a dependency on a runtime function body, because we need to analyze the function body to learn about all the places it might return an error.
sigbottle 24 hours ago [-]
I've always thought this was fascinating, but the only incremental compilation I knew was obscure programming languages and Rust. Oh yeah, I guess Roslyn?
Really fun and fascinating problem to work on.
tester756 23 hours ago [-]
Wasnt Roslyn 1st at implementing this on such scale?
sigbottle 22 hours ago [-]
Yeah I remember being in sophomore year of college, watching Anders Hejlsberg's video on "the new way to build compilers" or something and having my mind blown. But I only ever looked at the source code for Rust when it came to something actually implementing this, so that came to mind first.
Roslyn also has an extra constraint of integrating with live editing on the fly; I think you can get simpler and/or have different constraints if your requirement is only incremental compilation.
hoppp 1 days ago [-]
The zig compiler can compile C so will it work with C also?
bpavuk 1 days ago [-]
not quite! if your project is mixed C/Zig, then editing Zig would work but editing C would not. the Zig compiler caches Zig AIR but no information from C. plus, the C compilation can only really be done with LLVM and writing a self-hosted backend would be PITA for C
dundarious 1 days ago [-]
Not quite true, there is already a capable C compiler written in Zig (Aro/arocc), and a plan to transition to it for C compilation: https://codeberg.org/ziglang/translate-c
Using this native (written in Zig) C compiler to translate C source into Zig source as a part of the build, would presumably lend itself trivially to all the incremental logic in TFA, as updating C would update the generated Zig, and the incremental logic would detect differences just like it detects differences made by a human in an editor. Maybe there are aspects of the generated Zig that would complicate that somewhat, but I don't know -- just a warning about my ignorance.
This is part of plans to remove the hard LLVM dependency. AFAIK, the LLVM dependency will still be a variant many will use for the convenience of Zig as a much better clang, but removing the hard dependency is part of enabling all these great features like incremental compilation.
Thanks for the correction, I knew I was about 2 decisions out of date when I had the impulse to write my post, but my research to fill in my blanks was a bit faulty. I added the 2nd paragraph and "plan to" ending of 1st as an edit because I thought I had learned enough good info when writing the rest, but I was wrong.
Does this work for release builds or just debug builds now?
mlugg 1 days ago [-]
It only works with our self-hosted code generation backends (the main one being for x86_64), which right now don't have any optimisation passes. It's planned that they will in future, but that's a long-term goal. Also, any optimisations which propagate information between functions (the most obvious and important one is inlining) are more-or-less incompatible with incremental compilation (I touch on this in the post IIRC), so once this does work it'll probably still be limited to a subset of optimisations.
TL;DR: only debug builds for now, could extend to release builds once we have our own optimisation passes one day, but some optimisations will still be inapplicable.
minraws 1 days ago [-]
I don't think it will work with llvm/release yet but it might some day maybe.
Getting incremental linker to work with llvm is kinda hard atm. Maybe the zig team has a plan dunno.
dlcarrier 1 days ago [-]
I just looked up a Hello World program from the Zig Wikipedia article:
That's a lot to follow, just to output a plan-text message, especially after this line: "The primary goal of Zig is to be a better solution to the sorts of tasks that are currently solved with C. A primary concern in that respect is readability…"
jdlshore 1 days ago [-]
I don’t think you can judge a programming language based on its “Hello World.” AppleSoft BASIC has this:
10 PRINT “Hello World”
Beautifully simple and readable. But it’s not a good language by modern standards.
In your example, I see a lot of complexity being surfaced: output streams, locals instead of globals, error handling. I don’t know Zig but all of those are things that are important to address, and I like that the example doesn’t sweep them under the rug in pursuit of a false readability.
gracefulliberty 23 hours ago [-]
Everything it's doing it clear and readable. It's just not as easy to write. It streams the bytes to stdout using the default IO interface, and it can fail.
Alternatively, here's a simpler version (prints to stderr).
In practice, you normally don't want to print messages to stdout. So the increased friction here actually pushes you in a better direction.
peesem 19 hours ago [-]
the given complicated version's complexity actually just comes from the fact that it's a "more correct" way to write a hello world program, as it manually acquires the stdout File object and acknowledges that printing to stdout can fail. the complexity has nothing to do with stdout vs stderr, you could just use `.stderr()` instead of `.stdout()` (same "friction", it's even the same number of characters). `std.debug.print` is only meant for debugging/development as it gets stderr for you and discards the errors that could happen when writing to stderr.
gracefulliberty 15 hours ago [-]
I meant that the convenient interface only works for stderr. So you aren't accidentally sending debug messages to stdout and if you want to send bytes to stdout you have to do so intentionally and use the right interface.
defen 1 days ago [-]
The issue is that most "hello world" programs are not correct.
dwattttt 24 hours ago [-]
While most hello worlds do not check that the message was printed (which I assume writeStreamingAll does for you), dismissing the rest of the differences as "the others aren't correct" isn't really accurate.
Explicitly passing IO in is a fine design choice, but it's not a correctness issue to say others are wrong to not do so.
dlcarrier 3 hours ago [-]
Also, it's easy to make a C program return the number of characters printed and doesn't hurt readability:
#include <stdio.h>
int main(void)
{
return printf("hello, world\n");
}
tester756 24 hours ago [-]
>While most hello worlds do not check that the message was printed
Should they?
23 hours ago [-]
tester756 24 hours ago [-]
Whats the point of evaluating technology from hello world programs?
flohofwoe 12 hours ago [-]
Tbf, it's a useful indicator whether a language follows the "simple things should be simple, complex things should be possible" principle. The vanilla 'Hello World' should always be an example of the "simple things should be simple" part.
boomlinde 9 hours ago [-]
The typical Hello World implementation tends to reveal very little about the language, because their print/println/printf/whatever implementations have failure modes that are either impossible to handle or easily ignored (e.g. panicking, throwing exceptions or returning error codes which you can implicitly ignore without compilation error) which they frequently use to effectively hide the complexity inherent to the problem. Some examples of this:
The C Programming Language includes a Hello World example that calls printf without checking the return value and returns a success code from the main function regardless.
The first example I find when googling "java hello world" simply calls System.out.println and neglects to call System.out.checkError to see if it was successful before exiting with a success code. Some Java developers won't even know what I'm talking about here because it has never occurred to them that printing may fail in a way that can only be discovered through this weird checking mechanism.
Go's example from their getting started guide simply calls fmt.Println while ignoring the return values which include any error that may have occurred, and the program exits with a success code regardless.
The example from Rust by Example is at least correct and thorough in that it will predictably panic upon error when invoking the println! macro, which is documented, but will through that mechanism not give you the option to actually handle the error except by using a different mechanism which front-loads more of the complexity (e.g. writeln!(io::stdout(), "Hello World")? for something equivalent to the Zig example).
Of course for something as basic as Hello World it might be easy to tell whether it was successful through a quick glance at the output, but consider some of these limitations in a larger program.
So maybe there is more inherent complexity to this problem than a typical Hello World implementation will reveal. Add to that the complexity of Zig's new swappable I/O models and their Hello World isn't so absurd.
flohofwoe 9 hours ago [-]
I mean sure, from a purely 'is this program correct' pov you're correct, but then a hello-world is mainly about "how do I get some frigging text to show up on the terminal", and how likely is that to fail anyway (at least I never had the canonical C hello-world fail on me).
boomlinde 8 hours ago [-]
What's the point of showing an example if it's incorrect? If someone asks "how do I get some frigging text to show up on the terminal" and the answer is incorrect, it's bad advice as far as I'm concerned.
> and how likely is that to fail anyway (at least I never had the canonical C hello-world fail on me).
I don't expect to know how likely writing to stdout is to fail and I don't think any answer to that question other than 0 really warrants ignoring the potential error if the correct result of invoking your program depends on it. For what it's worth, at least in Unix-likes, stdout could be pretty much anything. Writing to stdout could fail because a switch at the user's ISP is rebooting.
flohofwoe 12 hours ago [-]
That's the 'official' hello world which is indeed a bit verbose (it's "correct" in the way that it generally shows how to stream formatted text to stdout though).
Arguably this is the more beginner friendly version, this prints to stderr though:
It disappoints me how unseriously the industry has taken compilation speed for so long. I'm glad to see Zig doing incredibly valuable, high-impact work here.
deepsun 1 days ago [-]
Well, I remember back in the day Java was winning a lot of love from C++ devs over its much-much faster compilation than C++. It was important, even 15+ years ago. People praised faster iteration time while coding.
pjmlp 1 days ago [-]
Yes, we get people amazed to rediscover Modula-2, Object Pascal compiler execution speed.
logicchains 1 days ago [-]
Golang's reason for existence is pretty much compilation speed.
myshapeprotocol 13 hours ago [-]
[flagged]
khanhnguyen8386 1 days ago [-]
[flagged]
justrust 14 hours ago [-]
[flagged]
rustynailer 16 hours ago [-]
[flagged]
rusttrash 15 hours ago [-]
[flagged]
14 hours ago [-]
ycombiburger 16 hours ago [-]
[flagged]
muth02446 23 hours ago [-]
The incremental linking part sounds pretty hackish to me and I wonder what the price is in increased code complexity and maintenance effort.
It also does not mention how it deals with the patching of debug information.
Rendered at 21:02:25 GMT+0000 (Coordinated Universal Time) with Vercel.
> Semantic analysis is the most difficult part of the compiler to handle incrementally. Perhaps unsurprisingly then, this is where language design starts to matter a lot: while I am pretty confident that most modern languages could support incremental compilation similar to how we do, certain design decisions can make that much more difficult. Zig has had its design tweaked over the years (sometimes controversially) specifically so that it is easier to support fast incremental compilation.
This is something I wish that we had done with Rust. It is impossible to do all of the things at once, though, and we already had a tremendous amount of things to do. This is also part of the "when do you ship 1.0" tradeoff; for our goals with the language, 2015 was the right moment to launch, but if had a few more years to bake things, maybe we could have made compile times way faster. Software engineering is hard.
I'm not sure what that means. Java lets you do many things programs may want to do in a memory-safe way but not everything. Rust lets you do fewer things than Java in a memory-safe way, but more things than Zig. Zig lets you do fewer things in a memory-safe way than Rust, but more than C. So among these four languages we already have four levels of memory safety, none of them is 100%, all of them give up something in exchange for what they offer, and different programmers have different preferences for the compromise they prefer, and even that preference is context-dependent. Which of those less-than-100% memory safety compromises is the table stakes? And given that all of these compromises require something that could be quite substantial, depending on the circumstance, in exchange, and consequently programmers with the highest level of knowledge and expertise choose every one of those four in different situations, to me it seems pretty obvious that none of these is "table stakes".
In a language like C (or Zig), you need to manually manage memory. This makes programming a lot more complex, and it's really easy to accidentally mess up. Especially in large projects which have a lot of separate modules.
The biggest advantage of using a garbage collector is that you don't have to think about freeing memory. The GC automatically frees objects when they're no longer referenced. This makes programming much much easier. The downside of using a garbage collector is that it hurts performance at runtime. GC languages are slower and use more RAM.
Fil-C is the worst of all worlds here. Like C, it forces you to manually manage your own memory. But you still pay the performance cost of having a runtime garbage collector. And that cost is (apparently) really high. The only performance numbers I've seen showed ~2x worse CPU performance and ~4x worse memory performance. There's no way Fil-C can compete with C, Zig and Rust for performance.
So with Fil-C, you have a language that's much slower than C, and much more difficult to program in than C#, Java, Go or Typescript.
Fil-C still has some wonderful uses. Fil-C could be a fabulous debugging tool for C programs. It could be a wonderful teaching tool if it had nice visualisations on top of the GC's view of the world. And it could be a great way to run legacy C code.
But it's not a "rust killer". Fil-C programs run too slowly to be able to compete head to head with rust. And Fil-C doesn't offer the language benefits of a GC that you get in C#, Go, and friends. It seems like a really bad deal.
This is not necessarily true. It depends on a language, e.g. Go is slow, Nim[0] is extremely fast with conventional GC and slightly faster with ARC/ORC[1].
GC programs can be faster than manually managed ones in some cases. It's just manual memory management gives you more control of where and when free is called. And a good type system is a privelege that gives Nim more control with destructors.
Another scarecrow of safe languages is GC pauses, which is also not a thing in Nim, see table in [2].
[0] - https://nim-lang.org/
[1] - https://nim-lang.org/blog/2020/10/15/introduction-to-arc-orc...
[2] - https://nim-lang.github.io/Nim/mm.html
[0] - https://programming-language-benchmarks.vercel.app/nim-vs-go
> GC programs can be faster than manually managed ones in some cases.
I've seen poorly written programs in C/C++/Rust which are slow because they allocate millions of tiny objects. Its true that generational GCs can be faster in this case. But you usually get much better performance again by using arenas and such. The reality is that I know more about the lifecycle of my data than my compiler. If you know what you're doing, you can take advantage of this information to write better programs.
If you don't want to think about memory management, then I agree - you're usually better off using a language with a GC. Personally I do a lot of my prototyping in typescript because I can iterate faster when I don't have to think about lifetimes.
Maybe some day Fil-C will run general purpose C code at native speeds, without a high memory overhead. But we're not there yet. I'm not holding my breath.
user created an hour ago
Can you expand on this?
Do you have any actual benchmarks?
What I do not like, primarily comes down to how the project is talked about and marketed. First, because it promotes an "us vs them" mindset, instead of a "we're all trying to improve memory safety" mindset, and second, because in doing so, it also overstates its case.
These things are sort of intertwined. Let's talk about the overstatement first. Fil-c has its own definition of memory safety that is slightly different than others. For example, I saw this recently:
This, when invoked with "012345678" passed in, will print "I am root!". In my understanding, this is deliberately allowed.But beyond corners like this, fil-c's author will go on about "Rust has unsafe as a hatch, fil-c does not" while if you control-f for "zunsafe_" on https://fil-c.org/stdfil you get ... escape hatches.
The author regularly erases the difference between "traps at runtime" and "is prevented at compile time", which are legitimate tradeoffs where one or the other may be better depending on what you're doing. But they're presented as either equivalent, or one is superior, and I find this muddles the discourse. The performance issues also tie into this, "add a GC" is absolutely a valid way to handle these sorts of issues, but it is not the same thing as what Rust does. And that's okay! But presenting it as purely superior means that it's just hard to talk about.
Speaking of muddling the discourse, the author regularly trolls on X, providing tons of bad faith arguments and generally trying to rile up a "fil-c vs Rust" war that I think reduces our ability to talk about these differences in a calm, engineering focused context.
Finally, due to its design, fil-c is effectively Linux only. That's great for Linux, but many people also use other systems, and so it is not a meaningful option for them.
Anyway, after saying all that: I still think that it is a good project, and that it should exist and continue to be worked on. I just wish that the heat was turned down, and people could talk about the various approaches and their tradeoffs without turning it into a culture war.
On the muddling the discourse, I'm not on twitter and don't engage there, so I don't have an opinion on that, but I did come across https://news.ycombinator.com/item?id=49044561 recently, and I just don't see how the author can make such bold claims while examples like the one Steve provided above are still in the language. Corrupting memory in Fil-C is still easy, type confusion is still easy, intra-object overflows are still easy. Fil-C prevents a range of classes of bugs from being exploitable, but it doesn't stop the bugs from happening.
> Those languages rely on a much larger pile of YOLO C/C++ code for their runtimes and standard libraries than Fil-C does. So Fil-C is safer than those
Given the relative immaturity of Fil-C, this seems wildly wrong to me. I’m not sure how to take his claims about his runtime seriously.
[ https://news.ycombinator.com/item?id=49042736 ]
Sure, this is a simple fact.
> So Fil-C is safer than those
This is not a simple fact that follows, and a good example of why you seem to be catching so much criticism for overly bold claims. One could state that a smaller runtime is easier to audit, and so the investment needed to reach similar levels of safety is lower. One might even argue that after similar levels of investment, that the probability that it's safer is higher. But jumping all the way from lower number of lines => safer is a simple fact is a huge leap.
By contrast Fil-C has a small number of rules and largely obviates the need for “native” code.
I make bold claims because they hold water.
- You can at worst corrupt only the capability you’re pointing to.
- intra object overflows are almost never useful for memory corruption exploits unless they let you corrupt a pointer, and Fil-C prevents that from being useful because you cannot corrupt the capability.
- the zunsafe api is basically unused. One library uses it (OpenSSL) for good reasons. This is in contrast to widespread use of the unsafe keyword in Rust, beyond just one library for a narrow purpose.
Thanks for reporting bugs. Worth noting that they require doing things that extant C code never does. It’s good to fix those, but the true threat model of any memory safe language is not to sandbox a malicious programmer, but to protect the program of a normal programmer against a malicious user
But you did the same thing when, on the spectrum that ranges from C to ATS, with Zig, Rust, and Java somewhere in the middle (though all closer to C than to ATS), you declared the exact compromise that Rust makes "table stakes"! [1]
Zig improves on C's memory safety when it comes to spatial safety, possibly the more impactful kind, so it, too, could be part of the "we're all trying to improve memory safety", yet you exclude it.
You're trying to draw some hard line that passes exactly between Rust and Zig on the C to ATS spectrum, and I'm trying to say that that line isn't there (your attempt at a definition of delineating safe and unsafe code also applies to C). Obviously, C, Zig, Rust, Java, and ATS all make very different tradeoffs, all of which may be more or less attractive to different people and in different circumstances, but there is no sharp line, at least not one that is meaningful enough to be "table stakes". Your personal inclinations place a premium on the things Rust offers and Zig doesn't while mine are the opposite, but I make no claim to universality.
I'm happy to accept that not everyone shares my aesthetics and can understand why some people prefer Rust, but those claims to or hints at universality annoy me (as they did when they were made by Haskellers, and I actually find Haskell's aesthetics quite pleasing), as they are simply unsupported. I've spent a lot of time studying formal methods and software correctness in general (https://pron.github.io) and if there's one thing we know in that field is that things are never that simple (and, bringing this back to this posts topic, even something like incremental compilation can contribute to program correctness).
(Now, you may argue that you're only talking about "memory safety" and not correctness in general, but what gives memory safety value is that violations are causes of many dangerous vulnerabilities; but once, say, Java eliminates all of them, 100% of bugs/vulnerability - which are still numerous - will be caused by other problems, all potentially avoidable with ATS, so why isn't ATS table stakes? Of course, the answer is cost, but all the languages on the spectrum differ in their costs.)
[1]: I assume that you meant Rust's compromise, because you implied that Zig doesn't pass that bar but Rust does.
I do not go around posting "omg Rust is SO MUCH BETTER than zig or fil-c, which are TRASH." I talk about engineering tradeoffs, and what matters to me personally. I do not say "if you use Zig, you are a bad person." I am not saying that any comparison is bad. I am saying that the way that the comparison is presented is bad. That is different.
> you declared the exact compromise that Rust makes "table stakes"
Table stakes for me.
> Zig improves on C's memory safety when it comes to spatial safety,
I agree that it's an improvement on C!
> yet you excluded it.
I said that it is not pursuing a design that I personally find compelling enough to use to write software. That doesn't mean that I think it's worthless. This whole thing started off with me talking about how much I respect the Zig project! Yet you're trying to turn this into something where I'm talking shit. I presented a specific technical tradeoff that is important to me. That is very different.
> You're trying to draw some hard line on a spectrum that passes exactly between Rust and Zig, and I'm trying to say that that line isn't there (your attempt at a definition of delineating safe and unsafe code also applies to C)
I don't believe you've shown that. And my "attempt" does apply to C: it fails the bar, because it does not delineate between a safe subset and an unsafe superset.
> you may argue that you're only talking about "memory safety" and not correctness in general,
I am in fact talking about "memory safety" and have been this whole time, yes.
> what gives memory safety value is that violations are causes of many dangerous vulnerabilities; but once, say, Java eliminates all of them, 100% of bugs/vulnerability - which are still numerous - will be caused by other problems, all potentially avoidable with ATS, so why isn't ATS table stakes?
This is just an entirely different question. Yes, there are other forms of safety that are important too. That's just not what we're talking about here.
> While I still don't plan to write software in it, given that I believe memory safety is table stakes
What exactly am I missing? You know you don't have to hijack Zig threads (repeatedly) with your thoughts on memory safety, right?
My point in bringing this up is to strengthen my compliment. Even though I disagree with aspects of Zig's design, the stuff talked about in this post is excellent.
Oh, who is saying things like that? Do you mean to imply that this kind of vitriol is characteristic of the Fil-C project?
Ok, so if you meant "table stakes" as an expression of a personal preference and suitability to the programs you write without making an unsupported universal claim such as "this leads to better correctness" or "the price is almost always worth it" then we're good :)
> Yes, there are other forms of safety that are important too.
The thing is that they can be at least equally important, and some affordances for memory safety could potentially _harm_ them. To me, Rust offers little safety in the programs I want to write in a low-level language, but the price it charges in language complexity and implicitness ends up in a negative balance (I can't prove it, of course; as I said, software correctness is very complicated, and some of the greatest researchers in the field were proven wrong on how to best achieve it).
I never said Fil-C is “so much better” (let alone with all caps) than anything.
I never called Rust “trash”.
I think you’re taking this all too personally
It is absolutely possible that one language might actually have an objectively better approach to memory safety than another, and in such cases it is usually possible to argue for this using sound technical or empirical arguments. But the way the author of Fil-C presents their arguments it often comes across in a kind of antagonistic manner, like he has a chip on his shoulder.
As a long time PL researcher, I can, should, and will point out interesting corner cases of languages. Including in Fil-C or Rust
You may well be right. I've yet to learn about it, but I'm planning to.
So your defense is a tu quoque fallacy? Note that "the same thing" is an admission.
I was always intrigued by the maturity about how the Rust team approached this sort of thing. IIRC years ago you and I had a back and forth on me thinking it would be helpful to have a "Why Rust is better than C++" type page.
Seeing an alternative approach from Andrew Kelley in recent weeks has really hammered home the value of the approach the Rust team took in terms of community building.
The Rust community is lobbying to make programming in C illegal
I'll do you a favor though and cite a few things related to "lobbying"
The White House put out an RFI regarding open-source security: https://bidenwhitehouse.archives.gov/wp-content/uploads/2024...
Rust foundation's response: https://www.regulations.gov/comment/ONCD-2023-0002-0045
Neither have anything to do with making "programming in C illegal".
Fil-C now has support for inline assembly, so it's not needed anymore, and I believe indeed the intention is to remove it, since Fil-C is not supposed to have any unsafe hatches.
Fil-C is not Linux only by design, that's completely false.
By the way, if you don't want a culture war, you gotta stop warring.
I find it hard to understand how could steveklabnik's comment be seen as warring.
See, this is nuance! Nuance is good! But you can't go around saying "fil-c has no escape hatches" when it has one, even if that one is planned on being removed.
> Fil-C is not Linux only by design, that's completely false.
Can you explain to me how it would work on other platforms? It currently does not, and I don't see how it can. Or at least, not without more "escape hatches."
This is one reason why Rust has unsafe: you have to interact with inherently unsafe APIs in order to do anything meaningful with the operating system. fil-c needs an equivalent of some kind, and so isn't better or worse here, it's just the reality of how these systems work.
Just to add to this, on Windows for example you're really only supposed to invoke syscalls via ntdll as the syscall table is not stable so their numbering changes over time. You cannot guarantee forward or backward compat if you do not use the library.
If you look at some of the syscalls in https://github.com/j00ru/windows-syscalls, you can see they clearly do change over time too.
Kinda rich of you to knock another language for its "marketing" when here you are once again on a Zig thread marketing Rust as a memory safe language. And speaking of "us vs them" mindsets, guess which language that reminds me of?
I don't even work on Rust anymore, and in fact started this thread with a criticism of Rust. There are lots of good criticisms of Rust. There is a difference between "this criticism isn't good" and "every criticism is an offense."
I just post facts. It is interesting that Rust is marketed as memory safe and yet has some large holes and that use of `unsafe` is quite widespread. That’s worth having a grown up discussion about.
Maybe you don’t like that this was pointed out so you felt trolled. But then that’s on you.
Rust folks, this whole thing is a thread about Zig’s new feature - not even a memory safety-related feature! - and we cannot spend the whole damn time talking about Rust.
Steve, even you - I don’t believe I have ever seen you say an unkind word. But have you considered that it may be unkind to have written more than half of the words on a thread about a Zig performance feature?
This comment spawned two subthreads. One of them was focused on the differences between Rust and Zig's compilation model, which is directly relevant to the article and illuminating regarding the engineering tradeoffs.
In the other subthread, pron posted paragraphs and paragraphs arguing about what memory safety really means and whether or not Steve is right to have his opinion that Rust is "safe". This tangent had essentially nothing to do with the content of Steve's comment; it (and not Steve's initial comment) was the point where the thread was derailed from the topic of Zig's incremental compilation model. Steve responded politely in this thread to comments and questions directed at him, but did not fan the flames or take the thread further into off-topicness. If the moderators collapsed pron's comment or detached it and pinned it to the bottom of the page, this comment thread would be much better and much more respectful to the Zig project.
I think the RESF trope is just about dead now; it's given way to the Rust Detractor Strike Force showing up to turn unrelated threads into tangential arguments about why Rust is bad.
Inherently? No! I commented specifically because I was really glad to see this post. This work that Zig is doing is very good, and I wanted to call that out, in part specifically because I am on "the other side" in whatever sense that is. Why would it be unkind for kind words to be coming from me?
What I mean is a bit different though, it’s that these arguments you get drawn into end up drowning out any real discussion of Zig’s progress. I don’t think that’s your intent but it is frustrating. I should be clear, I don’t think it’s wrong for you to defend yourself from accusations etc., I just wish it didn’t look like this.
I wonder how much better HN would be if they took a page from other forum systems that said “you know what, this whole branch of stuff should be moved over here and renamed so the original topic can move on”.
Sorry, all this may be unhelpful, I don’t know where the line should be, I’m just thinking out loud about the problem.
You're accusing steve of what pron is doing... I mean look at how much text pron wrote, it is much more than steve wrote, and how pron is constantly skirting the edges of the HN guidelines.
The fact is that every thread about Zig is filled with this kind of battle, and I’m tired of it, and it doesn’t represent any community, it’s so much worse on HN than anywhere else, and it’s all just defensiveness and crap.
I think this is a case of people who can dish it out but can't take it. As far as I'm concerned if you troll someone you should expect to get trolled back.
It’s funny, I’ve heard people claim this about rust developers for years. But I’ve seen very little evidence of it. Where are all these toxic comments? Look at Klabnik’s comments in this thread. He’s lovely.
—-
A son comes home to his poverty stricken family with a spring in his step. “Mum! Dad! All that time at community college paid off! I got a job!”. Dad immediately snaps - “so what, now you have a job, you think you’re better than us?”
What happened? Dad is unconsciously projecting a belief onto his son. Something like “unemployed people are shameful”. Then dad feels judged by the projected belief and he attacks the son for it. But it wasn’t the son’s belief in the first place. He just wanted his parents to be proud.
How does the son respond? It’s a tricky one. If the son defends himself by talking up how great it is to have a job, he reinforces the projection and dad will get more angry. If he says “there’s nothing to be proud of for having a job” then he’s lying about his values. It’s a trap.
When I’m feeling uncharitable, I project this same dynamic onto rust and C/C++ devs. “Mom! Dad! I figured out a way to get memory safety without sacrificing native execution and performance!” C: “So you think your language is better than ours? Why are you so toxic about it?”
I’m not really sure how to respond to comments like yours. I think you’re mad at ghosts.
The OP is about Zig and now there are 40+ mentions of Fil-C initiated mostly by Rust folks and those comments are largely criticizing me personally.
That’s toxic AF!
Troll gets criticized. Cry me a river.
And the second one (about swatting a linux dev) says "Others think someone from the Rust (programming language, not video game) development community was responsible due to how critical René has been of that project, but those claims are entirely unsubstantiated."
--
Also wild to complain about botting when I've seen half a dozen accounts that didn't exist an hour ago all pop up to criticize Klabnik.
But hey, nerd holy wars have existed since the internet began. I use vim btw...oh you use emacs? You're an idiot. Etc etc.
When he brought up the universality claim, he came up with a niche counter example that he kept inside his head and he makes it out to be the general rule by being extremely vague about literally everything.
https://github.com/ityonemo/clr
Now you might argue that the other features of Zig, like `defer`, are so good that they reduce the chance of memory errors and therefore memory safety has less value for Zig. But that seems highly dubious to me, especially for use-after-free. I guess we'll find out when Zig has more widespread use.
He can't write a JVM without using unsafe. So his disagreement on the "table stakes" is that his "table stakes" require using unsafe Rust everywhere and from that perspective Rust is not that different from any other language.
Hence the complaint about the lack of universality, which I personally consider weird. His point is that you cannot write the most interesting low level programs using just the safe subset of Rust.
If you have to write unsafe Rust (emphasis on have, your mileage may wary lot on that), then you have to litter unsafe everywhere in your code base so how does Rust help him? That's his point, but he doesn't want to say it out loud.
I don't think I agree with this framing. The question to me isn't "what can you do while being memory safe", it's "can you accidentally do something memory unsafe without noticing?" Rust and Java are the same here; you need to explicitly opt into using the language's mechanism for relaxing restrictions (Rust's `unsafe` blocks, Java's `Unsafe` class APIs), whereas from what I understand, neither Zig or C offers anything strict in that way.
Now, that's ridiculous, but something not too different happens to me with Rust. I reach for a low-level language when I want to do low-level things in a more convenient way than in Java, but the very things that would make me reach for a low-level language in the first place are unsafe in Rust. So in ~100% of the programs I want to write in a low-level language, Rust and Zig offer the same level of memory safety (but I need to pay a higher price for Rust). That Rust reminds me that what I want to do is unsafe doesn't help me.
Of course, other people may want to reach for a low-level language in other situations and their perspective could be different, but if I pay the price and get little in return I can't see how that would be "table stakes". Table stakes imply some universality that is obviously not here.
The argument that you can contain unsafe in safe abstractions does not interest him, because of the nature of the projects he works on.
Not at all. I would very much like the code to be safe, it's just that I reach for a low level language to get the thing that low-level languages are designed to offer me, which is control, and no language offers control and safety at the same time. So when the interesting parts of the code could be written in safe Rust, I have to give up control, and in that case I'd rather use a more convenient language that doesn't give me full control. And when the interesting parts of the code need full control, Rust doesn't give me safety anyway, and I still pay for its complexity. As to encapsulating unsafe, that doesn't help at all if the most tricky parts of the code are inside. What happens there is that Rust makes the hard parts harder and the easy parts easier, and for me that's a net negative.
And that brings me back to the universality point, that you're actually repeating, so let me explain it again. All programming languages bring a certain aesthetic that appeals to some and repels others within their intended domain. For example, I prefer Java to C# and Kotlin because I place a value on language simplicity, but I'm well aware that some other people like a lot of convenience features and they have the opposite preferences. I don't expect everyone to like Java even among those who work in the domains in which it is intended. For the same reason, I prefer Haskell over Scala and Clojure to Common Lisp, yet I fully recognise there are those who have the opposite preference.
Yet when people say they don't like Rust, the reaction among those who do is often one of: you don't want it to work, you don't care about correctness, your situation is special, or you just don't get it. I.e. they expect Rust to be universally liked unless there's a some good exceptional reason not to. But this is not the case for any language, let alone a language as complicated and as rich as Rust, which is certain to evoke a strong aesthetic reaction either for or against. So I have a long, long record of working with low level languages, I have a long record of working with formal methods and studying software correctness in general, I've worked extensively on safety-critical and mission critical systems, and I find Rust unappealing. There is nothing that should be surprising about that, there is nothing special about Rust or any other language that should lead anyone to expect it to be universally liked among those in the domains for which it is intended, and indeed, it is not the case for Rust as it is not the case for any other language. This amazement that quite a few people, even those with the relevant expertise, don't like your favourite language is exactly this sense of universality that I find annoying.
That some people get it yet don't want to choose a language is the case for Java, it's the case for Haskell, it's the case for Zig, and it's the case for Rust. Some people who really care about writing correct low-level code will have good reasons to choose Rust over other languages, while others who really care about writing correct low-level code will have equally good reasons to choose other languages over Rust. It's not because our programs are special, but it's because we think we can do it better with other languages. That's just the reality of all programming languages ever made, and that's what I mean by no universality. There is no external, objective requirement for any program that makes any (general purpose) language objectively best for that program. Choices always involve some kind of personal aesthetic preference.
That your favourite language isn't "objectively best" and isn't universally preferred even among those who care about the things you care about isn't something that should bother you, and pointing out this simple fact isn't trolling. It's something you should expect, because it's always true.
Assuming you are replying to me, you seem to be projecting very powerfully. Rust isn't my favorite language, and I just asked about how much of your code was safe vs unsafe.
But to answer your question, I reach for a low-level language when I need the one and only thing low-level languages are designed to do best, which is give me full and precise control over the hardware. In those situations, Rust offers little safety in the interesting/subtle/critical code, and for me it even makes things worse. It helps with the simple uninteresting code. So it's not a matter of quantity but of quality.
Of course, if the tricky parts of the program don't require precise control over the hardware, I don't use a low-level language in the first place. For example, if I'm going to let the language manage memory for me, why suffer the high overhead of heap memory management in the Rust/C runtime when the Java runtime offers me lower overhead?
Honestly, that's exactly how I feel in reverse. The framing you gave is more intellectually interesting, but it doesn't help explain the actual real-world outcomes where in practice, Rust and Java both don't have much problem with unsafety, whereas C does, and at least from what I've heard, Zig does as well.
> I reach for a low-level language when I want to do low-level things in a more convenient way than in Java, but the very things that would make me reach for a low-level language in the first place are unsafe in Rust. So in ~100% of the programs I want to write in a low-level language, Rust and Zig offer the same level of memory safety (but I need to pay a higher price for Rust). That Rust reminds me that what I want to do is unsafe doesn't help me.
I mean, sure, if you want to do things that are fundamentally not possible to validate because you think you're smart enough not to screw up, that's going to make Rust a tough sell. My issue with it is that history has shown that the best C and C++ programmers in the world still write code where memory safety rears its head, so I'm distrustful of the claim that being smart and diligent is enough to prevent the sort of bugs that we're still dealing with after half a century of us learning how not to write C. You need to have an excess of either talent or hubris to consider that a reasonably safe path, and given that the amount of talent needed is a lot higher than the amount of hubris, it seems way more likely that it's the latter.
The alternative is just learning how to write code that doesn't require expressing things in a way that can't be validated. While there are some things that fundamentally are not possible to, I'm dubious that it's anywhere close to as high as you seem to expect if your experience is that you literally can't reduce the amount of unsafe code you need in Rust below "literally my entire program is unsafe".
I'm not interested in the definition so much as I am in calling it "table stakes", and so the fact that these languages satisfy their promises is uninteresting in isolation. What matters is the value of their promises. The majority of Rust programs I see, I wouldn't have written in a low-level language, so the fact that it offers memory safety for the things I don't need it to do does nothing for me.
Now, clearly, Rust's originators didn't consider what Java offers (or at least what it offered 20 years ago when Rust was first conceived) to be table stakes or they wouldn't have wanted Rust. Java exacted some price in exchange for its memory safety that was unacceptable to Rust's originators and trumped its memory safety. But the same thing happens with Rust vs Zig. Rust exacts a heavy price for its memory safety, that - just as in Rust's case vs Java - is sometimes unacceptable. So I can't see how any of these could be "table stakes".
> I mean, sure, if you want to do things that are fundamentally not possible to validate because you think you're smart enough not to screw up, that's going to make Rust a tough sell.
What Rust can validate and what can fundamentally be validated are two very, very different things. Compared to what ATS can validate, what Rust can validate is almost indistinguishable from C. In Rust you have to do lots and lots of things that require you to be "smart enough not to screw up" that you could prove in ATS, and still no one (including Rust programmers) would say that what ATS offers is "table stakes" because, obviously, it comes at a high price that the people who choose Rust don't want to pay.
So clearly different languages offer different capabilities and charge a price for them. Sometimes the price is worth it and sometimes it isn't.
> so I'm distrustful of the claim that being smart and diligent is enough to prevent the sort of bugs that we're still dealing with after half a century of us learning how not to write C
But Java or Rust programs still suffer from a lot of bugs that ATS could eliminate, if you're willing to pay the price, and you're clearly unwilling. ATS programmers could say about Rust programmers what you say about C++ programmers. Clearly there's no universal table stakes here.
I mean, if you're already going to say "I don't want a low level language for anything other than what I can use unsafe for", then of course Rust will seem like overkill. I'd argue that the value of Rust is that it makes low-level viable for a lot of stuff that would otherwise require a lack of memory safety; a lot of it is stuff that might be written in a higher level language, but that's just because relatively few programs are impossible to write in higher level languages. That doesn't mean that the ones that need to be lower level can't be written in Rust though.
> Now, clearly, Rust's originators didn't consider what Java offers (or at least what it offered 20 years ago when Rust was first conceived) to be table stakes or they wouldn't have wanted Rust. Java exacted some price in exchange for its memory safety that was unacceptable to Rust's originators and trumped its memory safety. But the same thing happens with Rust vs Zig. Rust exacts a heavy price for its memory safety, that - just as in Rust's case vs Java - is sometimes unacceptable. So I can't see how any of these could be "table stakes".
Yes, "table stakes" is a value judgment, and one some people will disagree with. The cost for memory safety in Java is performance overhead though, and the cost for memory safety in Rust is not being able to express certain valid things that can't be validated; those are both objectively different from not offering memory safety at all, and my point is that the cases where what you want to express is literally impossible in Rust to do safely while actually being memory safe are pretty rare. There are some cases where what you're trying to do are fundamentally unsafe, in which case you need to use an unsafe block, but that's not anywhere close to the same as removing validation from the entire program. I'm fairly skeptical that you're basing your view that there are so many cases where you want to do something that's guaranteed to be safe but impossible to write in safe Rust on objective criteria, and extremely skeptical that the programs you write are anywhere close to entirely comprised of logic that can't be expressed safely.
> What Rust can validate and what can fundamentally be validated are two very, very different things. Compared to what ATS can validate, what Rust can validate is almost indistinguishable from C. In Rust you have to do lots and lots of things that require you to be "smart enough not to screw up" that you could prove in ATS, and still no one (including Rust programmers) would say that what ATS offers is "table stakes" because, obviously, it comes at a high price that the people who choose Rust don't want to pay.
> So clearly different languages offer different capabilities and charge a price for them. Sometimes the price is worth it and sometimes it isn't.
Sure, no one is disputing that. But that doesn't change the fact that some languages objectively require you to opt into which parts are memory unsafe, and others don't. It's obvious we won't see eye to eye on whether that's table stakes or not, but that's a difference of opinion, and having a different opinion than you isn't literally illogical; I find your take on it to be as hard to understand as mine is to you.
> But Java or Rust programs still suffer from a lot of bugs that ATS could eliminate, if you're willing to pay the price, and you're clearly unwilling. ATS programmers could say about Rust programmers what you say about C++ programmers. Clearly there's no universal table stakes here.
You're again taking an empirical argument as an abstract one and ignoring the real world outcomes that languages produce. You mentioned finding the fact that they actually produce real world software that in practice do not suffer from the class of bugs that C/C++ suffers from uninteresting, and that's fine, but it's meaningful for people who care about software actually getting used in the real world for real things. You seem to be arguing that unless you can eliminate literally the most bugs of any language in existence, then eliminating any bugs by picking a language that eliminates some of them is a useless endeavor. To me, the reasonable thing would be to choose a place to draw the line and say "anything beyond this is too risky, but I'll tolerate anything that's at least this safe", and memory safety is in practice the place I think it makes sense to do. I don't agree at all that not drawing any line at all is the only logical choice in a scenario when there are multiple places to draw it.
Maybe, but I don't see making a low language viable for something it's not needed as offering much value. Low-level languages are primarily designed to give you direct, low-level control over interaction with the hardware, they sacrifice other things for that goal (including performance [1]), and so if I don't need that control I don't use a low-level language. When I do need that control, I find that Rust requires reaching for unsafe too frequently while still paying the full price for the safety of things I don't use (even Rust's memory management of strings doesn't give me the control I want; I have to work pretty hard for it).
> The cost for memory safety in Java is performance overhead though,
It's not performance (you often gain performance, especially in large programs). It's warmup and footprint.
> But that doesn't change the fact that some languages objectively require you to opt into which parts are memory unsafe, and others don't.
Like I said, C also fits in the category, so it's not a meaningful distinction. The difference is in what you can do in the safe subset. Zig lets you do more things in a safe way than C (where the safe subset is effectively empty), Rust lets you do more safe things than Zig, and Java lets you do more safe things than Rust.
> You mentioned finding the fact that they actually produce real world software that in practice do not suffer from the class of bugs that C/C++ suffers from uninteresting
I didn't say that that's uninteresting; in fact Zig also eliminates spatial unsafety as well as Rust, and I think that's good. I said that merely looking at broad statistics is uninteresting if you don't consider the kinds of programs being written. I.e. Rust gives me safety mostly when I write code with the same level of low-level control as I have in Java, then that's the part I find interesting.
> You seem to be arguing that unless you can eliminate literally the most bugs of any language in existence, then eliminating any bugs by picking a language that eliminates some of them is a useless endeavor.
That's the very thing I'm arguing against. I'm saying that different languages eliminate different bugs at a cost (again, Zig eliminates many memory safety bugs you'd find in C or even C++, arguably the most dangerous ones). What I'm saying is that what you get and whether the price is worth it depends both on the program you're writing and on your personal preferences. Just to be clear, "preferences" doesn't mean I care more or less about correctness, but which approaches to correctness I find more or less effective, something on which there is no consensus.
> To me, the reasonable thing would be to choose a place to draw the line and say "anything beyond this is too risky, but I'll tolerate anything that's at least this safe", and memory safety is in practice the place I think it makes sense to do.
I think it also depends on the kinds of programs you write, because for many programs I write (and for which I pick Java) Rust's level of memory safety is too low, and for the programs I pick a low-level language I wish I could have some cheap memory safety, but it's not offered to me. So in those cases I would prefer Zig's spatial memory safety, as it's no worse than Rust, and not pay the high price for Rust's while getting little in return. Anyway, I'm saying that it's both a matter of which approach you believe leads to better correctness and the kinds of programs you write in the language.
[1]: For example, the fact that in Java, references are not required to be stable machine pointers opens the door to some powerful optimisations that are not available to languages where pointers are required to be machine pointers (or something close enough to them). Or the fact that low-level languages require that the machine instructions executed are those present in the compiled image (or close enough), or, if you want, caring about worst-case performance at the expense of average case performance (although both C++ and Rust specifically don't always make that easy) precludes some other very powerful optimisations. People like me who've spent years on huge C++ programs know that the low-level control offered by low-level languages (regardless of the question of safety) sometimes helps performance and sometimes harms it.
Well, Fil-C and also Cheri show that C is a language that can be implemented with perfect memory safety for 99.9% of the language. This is not true for every language but is also not an accident in C. But also with the typical implementations of C such as clang and gcc you can essentially get spatial memory easily by using safe abstractions.
The serious point: I care about whether the program I write aborts regularly, whether due to a Rust panic or a capability violation.
I am not sure about what you mean by "aborts regularly". After a memory safety issue, I think one usually wants to abort quickly and not do anything else in the program as the program state is confused, so running specific sanitizers in trapping mode together with coding abstractions that avoid unchecked raw pointer access you can write spatially memory safe code in C without a problem. But yes, sometimes you may want to continue running, but this is much harder and needs a careful design of the system anyway, also in other languages.
I'm going to challenge this. Is this from your experience? InvisiCaps, which Fil-C is based on, turns out of bounds accesses into panics. You're saying that turning any "benign" out of bounds into "abort the program" in all the C you? anyone? everyone? uses Just Works?
I'd rather the majority of those failure modes be caught by linters/compiler passes, not runtime safe aborts.
To me, those are also performance characteristics. Maybe my view on what constitutes "performance" is broader than average here.
> When I do need that control, I find that Rust requires reaching for unsafe too frequently while still paying the full price for the safety of things I don't use (even Rust's memory management of strings doesn't give me the control I want; I have to work pretty hard for it).
Fair enough, I can't tell you that you don't have that experience when writing Rust. It's pretty different from mine though, and the experience of the large number of former C/C++ devs I've worked with after they learned Rust; the only people I've talked to with that experience didn't really try to learn Rust and went in hoping that it wouldn't work for them, which informs my perception here, but I recognize that individual experiences won't always fit into larger trends.
> Like I said, C also fits in the category, so it's not a meaningful distinction. The difference is in what you can do in the safe subset. Zig lets you do more things in a safe way than C (where the safe subset is effectively empty), Rust lets you do more safe things than Zig, and Java lets you do more safe things than Rust.
I don't think I understand what you're saying here. I don't know of a way to turn off undefined behavior by default in C and only opt into it in discrete segements of the code, but maybe I'm missing something.
> That's the very thing I'm arguing against. I'm saying that different languages eliminate different bugs at a cost (again, Zig eliminates many memory safety bugs you'd find in C or even C++, arguably the most dangerous ones). What I'm saying is that what you get and whether the price is worth it depends both on the program you're writing and on your personal preferences. Just to be clear, "preferences" doesn't mean I care more or less about correctness, but which approaches to correctness I find more or less effective, something on which there is no consensus.
It seems like you're arguing against the idea of memory safety as a category at all then. To me, "I can't write code that's memory unsafe without explicitly opting into it" seems like an objective statement, and it's objectively different than "I can't write certain types of memory safety bugs in a given language". I don't really understand what's useful about being able to write memory unsafe code without having to opt in when in practice the number of bugs from mistaken memory safety are overwhelmingly more common than the cases when you're forced to opt into unsafe because Rust forced you to work around the constraints, and even in low-level programs, the actual number of truly unsafe operations you need to do tend to be fairly low in my experience. I guess I can't say for certain that you don't truly need to do things that you're forced to write unsafe for too often, but to me, it seems like you're refusing to pay a pretty small price for mostly ideological purity rather than pragmatism.
> I think it also depends on the kinds of programs you write, because for many programs I write (and for which I pick Java) Rust's level of memory safety is too low
> [1]: For example, the fact that in Java, references are not required to be stable machine pointers opens the door to some powerful optimisations that are not available to languages where pointers are required to be machine pointers (or something close enough to them). Or the fact that low-level languages require that the machine instructions executed are those present in the compiled image (or close enough), or, if you want, caring about worst-case performance at the expense of average case performance (although both C++ and Rust specifically don't always make that easy) precludes some other very powerful optimisations.
I'm struggling to imagine what the circumstances are where these are genuine concerns rather than theoretical or premature optimizations. What are some examples of programs where you'd get better characteristics running them if they were written in Java rather than Rust due to the lack of enough "memory safety" in Rust?
Yes, but they come with speed gains, so you can't say that you pay "performance overheads" when Java removes some of the performance overheads that programs in low-level languages and replaces them with others. You could similarly say that you pay performance overheads when going in the other direction.
> and the experience of the large number of former C/C++ devs I've worked with after they learned Rust
And it's not my experience or a large number of C/C++ devs I work with.
> the only people I've talked to with that experience didn't really try to learn Rust and went in hoping that it wouldn't work for them
Then your exposure isn't wide enough.
> I don't think I understand what you're saying here.
What I'm saying is that we can't say that the value is merely in the existence of a clear syntactic distinction between safe and unsafe code, because that distinction exists in C, only in C, the clearly delineated line between safe and unsafe code is that between `int main(void) {}` and anything that isn't that; i.e. any program other than that explicitly opts into unsafety. So any meaningful discussion about memory safe languages must include what you can do in the safe subset. In C's "safe subset" (the empty program), you can do nothing, and that's what makes it not valuable. But for my needs, what you can do in Rust's safe subset (compared to both Java and Zig) is also far too little (to justify the cost).
> To me, "I can't write code that's memory unsafe without explicitly opting into it" seems like an objective statement
It is, but what I'm trying to say is that it alone doesn't have much value. In C you also "can't write code that's memory unsafe without explicitly opting into it" by writing anything other than the empty program, but obviously you wouldn't consider C's memory-safe subset suitable because you can't use it to do what you want to do in C. Rust's value is not, therefore, in that it has a memory-safe subset, but that it has a useful memory-safe subset. It's just that the utility of that subset depends on the kinds of programs you'd want to use a low-level language in the first place.
> it seems like you're refusing to pay a pretty small price for mostly ideological purity rather than pragmatism.
Quite the opposite. The price of Rust's complexity, implicitness, and compilation time is too high for what little safety I get in return, that I don't want to pay it for pragmatic reasons.
> I'm struggling to imagine what the circumstances are where these are genuine concerns rather than theoretical or premature optimizations. What are some examples of programs where you'd get better characteristics running them if they were written in Java rather than Rust due to the lack of enough "memory safety" in Rust?
It's nothing to do with memory safety. Low-level programs sacrifice optimisation opportunities available to Java because above all else they need to offer low-level control. That low-level control can translate to good performance sometimes (especially in smaller programs), and sometimes it translates to worse performance (especially in large programs). The huge C++ programs I worked on migrated to Java not (just) for safety but also for better performance than C++ (again, it's easy to get excellent performance in low-level languages when the programs are small or specialised; it gets harder and harder as they grow). So we got better performance than C++ while also getting better safety than Rust, a much simpler language than Rust (or C++), and faster build cycles than Rust (or C++). But the topic of how Java reduces the overheads that C/C++/Rust/Zig programs often have when they grow large (although Zig makes it easier than the other them to reduce them) is a whole complicated topic. I might give a talk about it at the upcoming Devoxx.
> And it's not my experience or a large number of C/C++ devs I work with.
>> the only people I've talked to with that experience didn't really try to learn Rust and went in hoping that it wouldn't work for them
> Then your exposure isn't wide enough.
Or maybe your exposure is only to people who didn't give it a fair chance? I don't know how either of us can be confident that we know 100% for sure that our sample is more definitive.
> What I'm saying is that we can't say that the value is merely in the existence of a clear syntactic distinction between safe and unsafe code, because that distinction exists in C, only in C, the clearly delineated line between safe and unsafe code is that between `int main(void) {}` and anything that isn't that; i.e. any program other than that explicitly opts into unsafety. So any meaningful discussion about memory safe languages must include what you can do in the safe subset. In C's "safe subset" (the empty program), you can do nothing, and that's what makes it not valuable. But for my needs, what you can do in Rust's safe subset (compared to both Java and Zig) is also far too little (to justify the cost).
> It is, but what I'm trying to say is that it alone doesn't have much value. In C you also "can't write code that's memory unsafe without explicitly opting into it" by writing anything other than the empty program, but obviously you wouldn't consider C's memory-safe subset suitable because you can't use it to do what you want to do in C. Rust's value is not, therefore, in that it has a memory-safe subset, but that it has a useful memory-safe subset. It's just that the utility of that subset depends on the kinds of programs you'd want to use a low-level language in the first place.
That seems like an absurd false dichotomy in the form I was talking about before. I don't seriously believe that you can't easily identify when looking at Rust code whether unsafe is explicitly being allowed in it or not, or that you are writing programs that are doing things that would require unsafe literally everywhere.
I've genuinely been trying to understand where you're coming from, but the more I try, the more it seems like you just genuinely seem to think that you're too smart to accidentally write memory safety bugs, or that the memory safety bugs don't matter much. Maybe you're right, but I don't think there's anything left for me to learn from your point of view.
You don't need unsafe "literally everywhere" to run into issues. First, what matters most are the areas that are most subtle/tricky in your program. If in those areas Rust doesn't add much safety and makes things worse due to language complexity, that's a problem. Second, when you want low-level control, you might well want it in quite large swaths of the code. For example, one thing that low-level languages currently, in principle, do better than Java is arenas. But the whole point of arenas is that you want _all_ allocations in some large and elaborate call chain to go in the arena (and you'd like to enjoy both the standard library and 3rd party libraries). Rust doesn't make that easy (and neither does C++, for that matter).
> the more it seems like you just genuinely seem to think that you're too smart to accidentally write memory safety bugs, or that the memory safety bugs don't matter much
I don't see how you've reached that conclusion. I told you that for most programs I choose a language that is more memory-safe than Rust, and when I choose a language that's less memory-safe than Rust it's when Rust doesn't offer much safety, either.
See, this is exactly the thing I find so annoying in the Rust discourse. There's no doubt Rust significantly helps avoid memory safety issues (i.e. Rust => more men-safety) but that doesn't mean that caring about memory safety issues means preferring Rust (more mem-safety => Rust). One simply doesn't follow from the other because the logical implication is reversed.
First of all I respect your point of view - I'm not a Rust absolutist, I think that garbage collected languages are a massive advantage for a lot of things and would never criticise someone choosing a higher level language. Likewise I wouldn't criticise someone choosing Zig or Oden or Jai or even C for tasks where you really need that low level control.
For me, I like to have a single language that I can use for pretty much everything. Afaik there is no other language that is a) popular b) has a modern toolchain with integrated build, formatting & linting etc, and c) can be used both in the kernel and for developing websites. Rust might not be the best choice for most of the spectrum of software, but it's good enough for everything. I can write a low level service + a web server and UI in the same language, where with other choices I would need to use two separate languages. This matters to me because I don't have the time to maintain mastery of multiple languages, I find a lot of value in focusing deeply on one language and learning it completely.
Now I also don't write a lot of low level rust, I've never written a block of unsafe before and I probably write "unidiomatic" rust with too much copying, too many Arc<Mutex>>'s etc. But I like knowing that I can if I need to.
Rust has a lot of other things going for it. A good type system with plenty of nice language constructs that are missing in a lot of higher level languages. It has Cargo and a healthy ecosystem (although I do worry about the number of dependencies used sometimes). And a large community of very smart people. I'm not saying this is exclusive to Rust, but as a whole Rust is a unique language with no alternatives if you value the things I do.
So I would say that it's approach to memory safety threads the needle where it can be used (although not the very best choice) for when you'd use a higher level language, but also gives enough control that you can do plenty of low level stuff in it safely, and with clearly delineated unsafe sections where you really can do anything.
For low-level programs, I already said that Rust doesn't offer much safety for the things I reach low-level languages for (or, conversely, its safe subset doesn't offer the very control I'm after in such a language). Furthermore, the complexity and implicitness of the language make it harder for me to carefully understand the kind of subtle code I write in such programs. The long build times could mean I write fewer tests.
For high-level programs, Rust's safe subset is technically sufficient, but the problems are even worse (and exactly match C++'s): High level Rust code looks quite good and is easy to write, same as in C++, but the problems start with the maintenance and evolution. Small local changes - to a returned object's lifetime or thread-share ability, or between static and dynamic dispatch - require non-local changes. That's because low-level languages have low abstraction, i.e. the same contract covers fewer possible implementations. True, unlike C++, Rust tells you what things you need to change, but you still need to change them. That was the main problem we had with C++: the code looks great and it's very easy to write at first, but the maintenance and evolution costs - especially when the program is large and long-lived - get high and remain high forever. Furthermore, once a program grows large, it starts suffering from similar performance ovhearheads large C++ programs suffer from: you find yourself needing more dynamic dispatch, which is slow in Rust and C++; you find yourself needing more shared objects with different lifetimes, which are also slow in those languages, so the program isn't even particularly fast or scalable (sure it's faster than a JS or a Go program, but that doesn't say much). Java (or C#) which is aimed at optimising the performance of large programs, removes many of these overheads. Lastly, deep always-on observability/profiling isn't quite poor (it's better now with eBPF, but still a long ways away from what you get with Java or C#).
So yes, Rust and C++ are intended as "one language for everything", and Rust is probably somewhat better than C++, but your high level programs pay for the low level feature (i.e. suffer from the maintenance and performance costs of low-level languages), while your low-level programs pay for the high-level features (the complexity needed for implicitness and safety). So yes, you can do everything, but rarely as well as could be done, and while I see the value in getting expertise only in one language, 1. it's a language that requires a lot of expertise as its "multi-functionality" makes it very complicated, so much so that you could probably become an expert at two more specialised languages for not much more effort, and 2. I think that if you really need to write low-level code, e.g. you're writing a kernel or a hardware driver or a controller or a GC, then expertise in the domain dwarfs expertise in the language anyway (i.e. we're talking years of required experience until you're really good at it).
BUT I acknowledge that the weight I assign to these things is subjective, and I'm certain others reach the opposite conclusion through arguments that are no less reasonable than mine.
I'm not sure I understand your point about dynamic dispatch being slow in Rust/C++ or shared objects? If you're targeting native (which I find important) neither Java or C# are going to be faster surely. Maybe if you're willing to run Java/C# JIT you might find some wins (skeptical it's faster across the board) but you also don't need dynamic dispatch in performance-critical areas. I rarely reach for a Box<dyn Something> even in my high-level code.
I haven't worked on large Rust projects (> 500k loc) so I can't speak to the maintenance costs of that, but for me it doesn't matter (at least yet).
But we see even experienced professionals making mistakes with low level languages and I think it's worth considering if it's worth some of the cons you bring up to avoid those. Kind of reminds me of Carmack talking about static code analysis years ago:
https://archive.is/qC9a
> The more I push code through static analysis, the more I’m amazed that computers boot at all.
I get it. The language certainly does appeal to some people, and I can understand why, just as I understand why it does not appeal to others.
> I found myself wondering why I had to keep track of lifetimes, nullability etc in my head when it was so easy to mess those up.
And I agree with that, but my conclusion (after decades of experience with low-level programming) is somewhat different: Don't reach for a low-level language unless precise low-level control over the hardware is the exact thing you're after. And when that is the case, I find that safe Rust doesn't offer the control I need, and unsafe Rust (and/or a lot of custom code) is not what I want to use.
> Maybe if you're willing to run Java/C# JIT you might find some wins
Of course I use the JIT. That's exactly what it's for. Now, I don't care if the buffer from which the CPU reads instructions is memmapped from a file or generated by a JIT, but I do know that some people like the "single native file experience". To that end, we're working with Google to add a small feature to the JDK that would allow it to link the JVM, other native libraries, and Java classes into a single native executable (it's still going to JIT the Java code, but you'd be launching a "native binary").
> (skeptical it's faster across the board)
I wouldn't say it's faster across the board. You sometimes can write large programs in C++ (or Rust) that match and even exceed Java's performance, but it gets harder and harder the larger the program is. On average, I find that the "effort per performance" is, on average, significantly lower in Java in large programs. And it's not just the JIT. Another weak point of low level languages is that their pointers can't move, which means they can't use moving collectors, which also offer superb efficiency, again, mostly in large programs when you have lots of objects of varying sizes and lifetimes (especially now when we no longer have GC pauses).
> but you also don't need dynamic dispatch in performance-critical areas
You certainly don't start out needing it. Over time, however (and important codebases last at least 15-25 years), it either creeps in or it affects sufficiently many less critical paths to make an impact. You can try and re-architect things, but it takes a lot of effort (and it's this evolution effort that was a major reason for C++'s decline).
> But we see even experienced professionals making mistakes with low level languages
Absolutely, but my prescription would be to avoid low level languages altogether, and that has indeed been the industry's trajectory, and it's continuing. And when you absolutely do need to kind of control that low level languages offer, language complexity can also cause (or help hide) mistakes in code that is often very subtle, and the added safety, which is partial at best in those situations, isn't enough to offset that. Again, this isn't universal, but there are reasons to avoid Rust in low level code that are just as good as the reasons to pick it, and so different people will choose differently.
BTW, I've never worked on a browser, and it may well be the Rust is the best language for that, but I would be very curious to try Java. First, modern browsers run a lot of JS so you have a JIT and a GC, anyway, and so it might be both easier and more efficient to have everything use the same GC, and while process isolation would have required Java to re-JIT the rendering pipeline, Java is about to allow sharing JITted code (and even caching it from one run to the next) so that there would be no need to warm up the same code over and over.
In particular, modern moving collectors were designed for the purpose of removing the high overheads of malloc/free allocators that make heap memory management so expensive in low-level languages (and in any language that uses non-moving memory management strategies). The reason code in low-level languages tries to avoid things like heap allocation and virtual dispatch on the hot path is not because these things can't be super-fast (most virtual calls in Java are faster than many static calls in C), but because they are slow in low level languages because of their constraints.
- Explicit object lifetime and deterministic destruction.
- Stack allocation by default.
- Value types and direct embedding of values in data structures.
- Precise control over data layout, alignment, padding, and SIMD-friendly representations.
- Explicit allocation strategies: arenas, pools, slab allocators, region allocators, custom allocators, memory mapping, etc.
- No mandatory tracing, write barriers, object headers, or garbage-collector scheduling.
- Native interoperation without an FFI boundary.
- More predictable latency behavior.
- Compile-time specialization through templates in C++ and monomorphized generics in Rust.
Rust’s ownership and borrowing model can also provide strong non-aliasing and mutability guarantees in safe code which are useful for optimization.
Saying that "Low-level languages sacrifice performance for control" is also not true imo, since they can avoid allocation entirely, store data contiguously rather than as individually allocated objects, avoid all gc work, control cache behavior and eliminate pointer chasing, and importantly, guarantee hard or soft latency bounds.
Saying that "Most virtual calls in Java are faster than many static calls in C" is not a meaningful claim either. I think? Cuz "it depends" :)
And remember that GC is not free. Objects must eventually be traced. Reference writes may require write barriers. Objects have headers and alignment overhead. Heap size must often be larger to maintain throughput. Concurrent collectors consume CPU and memory bandwidth. Stop-the-world phases or other latency issues remain even with modern collectors. Object movement can complicate native interoperability and pinning. Malloc can cause performance issues but Java is not beating an arena allocator in C++ or Rust.
Keep in mind speed and throughput are not the only performance metrics. So is startup time and memory footprint, where Java loses badly here.
I wish we had real published research to go off of here but real world examples are all we really have. I'm not seeing AAA game studios building their engines in Java. I don't see any OS's building their kernel (or anything really) in Java. If Java is faster than low level languages, why is that?
And look, I don't dislike Java or the JVM, and I'm actually a really big fan of C#. I just like Rust more.
This used to be the big one, but not anymore: https://openjdk.org/jeps/401 (so Valhalla is integrating in JDK 28; it's not complete and this JEP is only the first step, but Java will have everything it needs on that front very soon.
> Precise control over data layout, alignment, padding, and SIMD-friendly representations.
Since the JVM controls layout, this is a point for Java.
> No mandatory tracing, write barriers, object headers, or garbage-collector scheduling.
Java doesn't require these things. JVM implementations choose to have them as an optimisation.
> Explicit object lifetime and deterministic destruction.
Hypothetically, this could have been an optimisaton opportunity. In practice, this is not a problem for Java but it is a problem for low-level languages, and especially Rust. The problem isn't knowing when an object's life is over, but needing to do something about it then and there. The whole idea of moving collectors (and arenas) is that it is more efficient to do nothing when an object becomes unreachable, and knowing when that happens doesn't help.
> And remember that GC is not free
Of course it isn't, but moving collectors are cheaper than free-list-based approaches, which is why we use them (most objects are never traced; those that are, are traced rarely etc.). On the whole, moving GCs are a speed improvement, reducing the overheads in C's runtime, and what they take in exchange is footprint (i.e. they use RAM chips as hardware accelerators). Now, that footprint cost could be expensive in smart watches and smaller devices, but in larger ones, the tradeoff is almost always worth it. I gave a talk about exactly that at Java One that should be up on YouTube eventually.
> Saying that "Low-level languages sacrifice performance for control" is also not true imo, since they can avoid allocation entirely, store data contiguously rather than as individually allocated objects, avoid all gc work, control cache behavior and eliminate pointer chasing, and importantly, guarantee hard or soft latency bounds.
I don't agree with that, and that's the very crux of my point. What you're really saying is that fine-grained control over the hardware lets a user who works hard enough to optimise their program to any level they choose. This does work well in small programs, but it fails in larger ones, and the JVM is designed to solve the performance issues that we C++ programmers experience in large programs. Over time, as programs grow and become more elaborate, it gets harder and harder to do those manual optimisations, which are very intrusive. E.g. you could perhaps use arenas in Rust more-or-less safely, and maybe Rust will make it easier in the future (right now the only language that makes that easy is Zig), but changing from no-arena to arena or vice versa, or finding out that you need special cases to some objects, requires a huge change to a low-level program. Similarly, trying to keep virtual calls to a minimum is very easy in the beginning, but becomes harder over time. So the idea that with enough control you can do anything is true in principle, but in practice it's very hard as programs evolve. The idea of modern runtimes is that you write the code naively, and the runtime performs global optimisations that help the average-case performance.
> I'm not seeing AAA game studios building their engines in Java. I don't see any OS's building their kernel (or anything really) in Java. If Java is faster than low level languages, why is that?
Much more performance-sensitive software is written in Java than in C++ these days, and your question assumes that the main thing that's important in these particular is speed, but that is not the case. What is the main thing kernels need to do? Directly control hardware. And what is the one thing that low-level languages are optimised for? Direct control over the hardware. Low-level languages fit the domain of OS kernels like SQL fits data queries; that's what they're for.
As for games, first, performance isn't the issue here. The most performance-critical parts of a game are not written in C++ but in CUDA, and the main important part for the CPU is a good algorithm for scheduling the data to the GPU, and that can be done in any language. What is very important for games is hardware support, and the JVM simply doesn't target most consoles. Second, games do care about latency, at least up to the length of a frame, and until very recently Java had GC pauses, and those could sometimes exceed the latency needed by games. GC pauses were removed in HotSpot only 3 years ago. So these are the reasons game engines are normally written in C++ (except, of course, for the most successful game in history, which is written in Java).
Zig has an identical memory safety profile to C. It has facilities to make it easier to stay memory safe, but those facilities are basically equivalent to what you have in C++ and that's equivalent memory safety profile as C.
> So among these four languages we already have four levels of memory safety, none of them is 100%
No, you've pretended like there's four when really it's Java / Rust which are safe by default and Zig/C/C++ which are unsafe by default.
One effective metric to evaluate is memory safety per LoC. Rust is ~0.2 vulnerabilities per MLoC. Java is effectively 0. C and C++ both seem to be about 1,000 vulnerabilities per MLoC. Zig is too new and hasn't had any analysis done on it, but generously it's likely at least 10-100.
So the table stakes could be defined as 1 memory safety vulnerability per MLoC.
They really don't. Look at how many basic data structures (in the standard library or outside it) require unsafe features in Java vs Rust.
> Zig has an identical memory safety profile to C
It really doesn't. Zig gives you the same spatial memory safety as Rust and very much not like C (and violations of spatial memory safety are a bigger cause of vulnerabilities than violations of temporal memory safety).
This is a fundamental misunderstanding of how "unsafe" code relates to a platform's trusted computing base. Rust could move all of those unsafe data structures out of the standard library and into the compiler itself, thereby reducing the amount of occurrences of the string "unsafe" in the source, code, but this would do nothing to reduce the size of the trusted computing base that Rust presents. In fact, it would decrease our confidence in that code, because Rust libraries have a robust ecosystem of tools for validating their correctness, unlike whatever bespoke IR the Rust compiler itself is emitting. Java's own data structures are implemented with the support of an extensive runtime written in C++, which forms their own trusted computing base that every user of Java relies upon, and demands just as much careful auditing as any data structure in the Rust standard library.
No, you're missing the point, which is that you cannot write some common data structures in safe Rust (whether they're implemented in the standard library or in the compiler), but you can in Java (inside or outside the standard library). In other words, the point is that safe Rust is quite restricted.
> Java's own data structures are implemented with the support of an extensive runtime written in C++, which forms their own trusted computing base that every user of Java relies upon, and demands just as much careful auditing as any data structure in the Rust standard library.
No, because these data structures are not part of the trusted computing base, which is fixed and closed. In Rust, that base has to be extended to any third-party code that uses unsafe, which is needed in many more situations. In fact, the need for unsafe code in Java has been so reduced that we're currently in the process of removing Unsafe altogether, making it inaccessible to third party code, which would still be able to write the data structures that would be unsafe in Rust in safe Java (the only unsafe thing remaining would be the FFM API, used for FFI, and the legacy JNI, used for that same purpose).
It isn't controversial that the amount of stuff you can do in safe Java is significantly higher than the amount of stuff you can do in safe Rust. That's just obvious.
Your "actually you can" post is just misleading and will result in more people who will get burnt but the design of the language.
It won't be anywhere near Java's. Those GCs are mark-and-sweep collectors. Java uses moving collectors. Moving collectors are used to avoid the high overheads of malloc/free in the C runtime (or of any free-list-based mechanism). They're a performance optimisation. Heap allocations in Java behave more like arenas than like heap allocations in languages with non-moving memory management, whether it's C, Rust, Python, or Go. Other runtimes that use moving collectors are Google's V8 and Microsoft's .NET, except that Java's ZGC has no GC pauses.
You have just described six orders of magnitude in your attempt to rebut pron pointing out the four languages have four levels of memory safety.
They aren't necessarily, though. Supposing that Zig were "10 issues per MLoC" (with just as much handwaving as the original poster), it would be equidistant from Rust and C. Java may also be more than one order of magnitude away from Rust; we say ~0 but is it 0.01, 0.001, 0.0001...? And why is "1 issue per MLoC" the acceptable metric that delineates what constitutes table-stakes memory-safe language? Because it's a nice, round-sounding number? I think 0 is a nicer, rounder number than 1, so let's call only Java table stakes and condemn all other languages to the garbage bin, tradeoffs be damned. Or would you say your arbitrary delineation point is worth more than mine?
zig as a language not distinguishing between safe and unsafe. Zig does not forbid unsafe. Zig’s safety model is conceptually not too different from running with ASAN for C/C++ during development. Given that’s already a followed “best practice” for the data that was used to come up with the vulnerability estimate per MLoC for C and C++ it’s reasonable to assume zig is closer to that side (at scale) than to Rust. You could argue defer as a keyword is worth a 10x improvement but even then im not sure how considering c and c++ are close and c++ does essentially a similar thing
Yes for the reasons I already gave. I think that at the point that you're having to stretch the numbers from their post to the breaking point to remove the pretty clear order of magnitude differences it's not really a constructive way to engage.
I think you have two groups with one at ~.1 and one on ~100. You seen to either disagree with that, or think it doesn't matter, I'm not sure which. But taking that assumption as true it is self evident that the 3-order-of magnitude demarcation is not arbitrary.
---
Rate-limit edit replying to below response:
> a variety of statements I haven't said
We are in a conversation thread specifically about statements of this nature, which I was contesting. The original poster of this thread called their arbitrary definition of memory safety "table stakes" and explicitly said that they rule out Zig as a language completely on this basis alone. If you don't agree with them, I'm not sure what we're discussing.
> you take issue with the assumptions I'm making ... but you could just state that instead of saying that I'm being hypocritical
The only assumption I disagreed with is assigning Zig to exactly 100 when a poster I was replying to originally asserted a range of "10 or 100"; and regardless of whether we agree on assigning a concrete lower/upper bound to that assumption, everything else is not an assumption but a value judgment given the condition "assuming the premise holds". Yet your position is taking those value judgments - which orders of magnitudes to accept, which to group together as being the same degree of memory safety - and asserting them as objectively correct boundaries with minimal rationalisation beyond "because I feel it is so".
If you want to have a conversation with me about the things I'm talking about, I welcome it, but I don't see that happening.
Yes, like I keep saying: two clusters each within an order of magnitude, separated by three orders of magnitude feel to me like two distinct things. That does not at all feel arbitrary. I think that claim is pretty self-explanatory. You appear to think it reduces to "because I feel it so" and in some sense it does. I am applying my own judgement and values in constructing those clusters. Someone who felt that any amount of memory safety was unacceptable would structure them differently. Someone who cared naught about memory safety would similarly group them differently too.
Also note that Java has unsafe, but doesn't have the culture of plainly stating safety invariants like Rust. The unsafe features of Java are less widely used, but when they are you rarely know if a Java library has unsafe internals for performance, and if they do, it may be hard to audit
The memory safety given by Rust is obviously not airtight, because the aliasing case requires either unsafe blocks or reference counting, but you fundamentally give something up by allowing pointer aliasing that you can never get back once the genie is out of the bottle. The painfully constricting flexibility so reminiscent of C that everyone else clings to is not where I want to go back to.
https://github.com/rust-lang/rust/issues/2369#issuecomment-1...
This would already help a lot.
I recommend anybody who's interested in incremental recompilation to read what GHC does, because the effort to achieve that is relatively low.
Of course there's always desire for more:
GHC currently needs to parse+typecheck+codegen a file before it can process other files that import it. Codegen is slow. Thus, there's currently demand split compilation into "stages", so that the next file can be typechecked after its imports have been just typechecked (not codegenned).
I would also enjoy if recompilation avoidance were to happen at the function level, not the file level.
Macro systems are a key language feature that can destroy incremental recompilation. In theory, Haskell is well set up for that, as its macro system (TemplateHaskell) is fully AST based and _theoretically_ could distinguish "fully pure" macros from side-effectful macros (such as splicing the current git commit in as a string literal). But the recompilation avoidance system does not currently exploit such differences.
An example that's being funded right now: https://rust-lang.github.io/rust-project-goals/2026/expansio...
From what I see in Haskell files are the unit of compilation, and that's what allows incremental compilation to be file based (because it's really unit-of-compilation based)
I can see you can have circular dependencies between files with the `{-# SOURCE #-}` pragma, but I don't see documentation about how that affects incremental compilation.
A couple of issues I see with doing this in Rust are:
- in Rust the unit of compilaion is a crate, which can contain many fils/modules with circular imports, which is much more coarser than what can be done in Haskell.
- in Rust downstream crates can depend on function bodies upstream for running compile time functions; as such the crate/module interface is not enough to gate recompilation, but at the same time including all function bodies will also not give the wanted benefits. This is solvable but likely requires more work than what was done in Haskell.
In general you cannot take a language approach and blanket applying it to another one without considering their different quirks, which is likely why your proposal didn't get much attention. Or am I missing something that would make it easier to apply Haskell approach here?
> I would also enjoy if recompilation avoidance were to happen at the function level, not the file level.
This sounds like Rust is already doing a lot more incremental then GHC then. Rustc only needs to parse, expand macros and do name resolution. Everything else is incremental after that, on a very granular level.
We can always compile with full optimization just before shipping?
The main issue is that so far such tools haven't been a priority for Rust.
Zig doesn't have the same safety guarantees, it's on the dev to use safe coding patterns, so the tradeoff for safety is discipline or experience.
In particular, Rust made several good design decisions around this stuff that keeps those checks fast, like keeping checks local rather than being global.
There's also stuff around name resolution.
Proc macros are just an inherently very slow way to do what they do.
Because Rust commits to the traditional compilation model and pipleine (which I think is overall a good thing, or at least, a good thing to support), it does a lot of work that will eventually be thrown away. Consider this example: I have a library with a function foo that returns a simple 42. I have a binary which calls foo from that library and prints the result. Now imagine the library is a hundred thousand lines of unrelated code to what the binary needs, but is useful for other people. Because compilation works in the "produce libraries, produce binary, link them all together" style model, you have to compile the entire library with all of that code, when all you need is really one function. That intermediate work is useful, and I'm picking an example that's deliberately extreme, of course.
There's a bunch of stuff like this, and I do not have time to really say more than that right now. But yeah, monomorphized generics also produce a lot of compile time pressure too, in various ways.
Anyway I just also want to reiterate a few things: first of all, all of these decisions were made for good reasons, and there are pros to what Rust does and why. It's just that compile times suffer because of it. What I wish was that we had taken compile times into more consideration when deciding what to do and why in a more serious way. The same decisions might have been made, but at least it would have been known, rather than the situation now, where there's just a tremendous amount of work to try to optimize what exists, rather than having the freedom to maybe tweak some things to make that job way easier.
Even a simple change to one file results in re-parsing the whole library because definitions come from anywhere and we have to obey the 1970s single file compilation model. The result is a driver spawns 8 threads and each one wastes time re-parsing every file in the library looking for definitions. AFAIK Rust doesn't really track dependencies at the file or function level either so it doesn't really know what changed.
To me compilers should be content-addressed databases. Each declaration and its associated content generate hashes that roll up to its containing type or namespace, then to the file, then to the library as a whole, along with hashes of the dependencies. Changing the type signature of a single function should result in the compiler being able to cheaply determine whether that has any visibility and if so to what other files in the same library or if it affects the public interface.
A file that hasn't changed and whos inputs hasn't changed should re-use the IR from the prior compilation. Even for an individual type that should be the case so changing the internals of a function in a struct only regnerates that one function and nothing else. The compiler knows deterministically that change can't have affected anything else.
That has major benefits for code completion and editing as prior compilations can feed into generating errors or suggested corrections.
Then you can take things a step further and JIT a changed function, injecting the new machine code on the fly so long as the shapes of the types don't change. Very useful for debugging.
Compilers are mostly held back because the people who write compilers are stuck on certain ideas about how compilers should be written.
There's a language called Unison that does that - and the "content" is the AST, so all functions that have the same shape are the same function. It's pretty interesting.
Rustc already does something like this. The issues are:
- when you have to rehash everything to check that they indeed didn't change from the previous compilation. For big projects this takes _a lot_ of time.
- when small changes do indeed change the hash of a lot of seemingly unrelated code, which is more common than you might think.
OP handles the small change problem by hashing IR instead of source text. If the new function compiles to the same IR as the old one its guaranteed to give the same machine code. You should repeat this after each lowering or optimization pass so functions with different HIR but same MIR are also marked as unchanged and dont cause items up the tree to rebuild.
OP here---I think you got confused somewhere, because this isn't true! The hashes we take are of source code, they're just stored inside of the first IR. Source code is just pretty convenient to hash. If the change does turn out to be something trivial, we'll only invalidate the first-order dependencies of the source hash, which is never usually a big deal. In a particularly bad case, maybe we re-analyze, re-codegen, and re-link many different instances of a generic function, but even that wouldn't usually take long at all.
Also, it's actually important that we do notice these kinds of "trivial" changes in some parts of the pipeline---see Andrew's comment [0] on the Lobsters discussion for this post.
[0]: https://lobste.rs/s/rmzzdb/inside_zig_s_incremental_compilat...
Note that it's not a serious suggestion, but I wonder what effect it would have on build times.
As always this could be avoided with some extra complexity, but that require lot of work and testing that hasn't been done.
So for now this is useful only in crates that have a lot of unused functions, so even if one function is codegenned multiple times you still save time overall.
There's also -Zhint-mostly-unused flag.
The tradeoff is that these functions then have to be encoded in metadata for downstream crates, so it's not necessarily faster.
That would mean generics in shared libraries are possible without hacks but you can always choose to statically link and monomorphize for speed. Libraries could even ship both the vtable based catch all code and specializations for common types as a fast path (but that would probably need a custom dynamic linker).
I feel extremely validated reading this! I've had a pet theory for a while that compile times would be drastically reduced if every single item in a crate were implicitly under a cargo feature flag and then they only got enabled if they were imported (and used in non-dead code). I know that making something like that work isn't anywhere close to as simple as I'm describing it, and there are probably a million edge cases, but I've long felt that the ergonomics of Cargo features basically making it too annoying to expose everything conditionally (and then transitively expose all of the features from all of the direct dependencies as well so that things depending on your library could also only conditionally enable them) is secretly the reason that people think Rust compile times are slow, and seeing someone who has way more direct knowledge than me of how all of it works under the hood give a similar take makes me more confident that I might have been on to something all along.
Incidentally, you might want to look at gc-sections, which Rust already does. As well as https://rust-lang.github.io/rust-project-goals/2025h2/relink... which is kinda related.
The sort of key here is understanding that "produce a library" means that every public item is "used" in the sense of "do we need to compile it." The real trick is to do demand-driven compilation starting from the actual final program's needs, and this is inherently at odds with the idea of producing standalone libraries and combining them into the final artifact.
It is true that for various reasons, Rust can't take as much advantage as say, C can.
> I'm guessing this is related to what you mean by potentially being able to do something differently if there were more time before 1.0?
I mean more traditional language design things, but sure, this too.
This is both true and not the whole story. In C, the file (okay if you want to get REALLY technical it's not the file but I'm talking about 99.9% of computers and not some old mainframe platforms) is the compilation unit. You pass a .c in, you get a .o (or whatever for your platform) out. Producing a final binary is where the static vs dynamic choice really comes into play, but you end up with these intermediate artifacts because that is how the language is defined.
> I would have expected that when static linking is the default, the argument for having standalone intermediate libraries is much weaker,
It is weaker, sure. But that doesn't mean there aren't other advantages. For example, you can more easily parallelize a large workload by breaking it up into multiple intermediate libraries, and compiling those simultaneously, whereas doing it all in one compilation/translation unit requires compiler support for said parallelization. Especially if you're already writing a batch compiler, this is a much easier win than rearchitecting the whole thing.
this is not so far off from hint-mostly-unused, no?
The downside is that functions which are called from multiple dependent crates will need to be codegen'd in each of their dependents, so this can increase compile times if the crate is not "mostly unused."
So it's not quite as powerful as full demand-driven compilation, because of how Rust separates the compilation process into separate crates.
Maybe at some point in the future zig could add a rust compilation target ( like with `-ofmt=c` )...
- Language design. Zig was designed for fast and incremental compilation, Rust is just not. For instance, the post states that Zig has four properties (layout, type, value, body) that the compiler has to track for changes. Rust has much more, to the point that tracking them statically is just impossible, so the compiler uses a query system that tracks them dynamically, which adds overhead.
- Compiler implementation. Rust is much more complicated to compile than Zig, and rustc is both older and bigger (10x-20x LOC) than the Zig compiler, making changing it way harder.
Rust as a language is even more reliant on monomorphization and inlining than C++, due to core language traits such as Deref, AsRef, From/Into, and so on.
That said, in practice my personal experience has been that Rust compares pretty favorably on compilation speed, even to some high-level languages like C#. On many developer machines, the actual slow part is linking.
I understand that for a release mode a single giant binary may be desirable, but I am struggling to understand this design for debug builds. Moreover, while reading this article, I found myself wondering what happens if the main binary becomes corrupted. Maybe the user cancels compilation with ctrl+c while it is patching the binary. Even if they have a story for avoiding and/or detecting corruption, it is simpler to not patch in the first place and always generate a new main binary. Again, this is reasonable because the new binary is mainly just a list of shared libraries to link which will not take up much space and can be written to disk quickly. Moreover, this process can be done recursively, e.g. at the subdirectory level, so that during incremental linking a few quite small shared libraries may be produced rather than patching in the new code and writing cascading relocations.
(a process with 100 shared libraries takes 6ms to run, which is a lot better (0.9ms for 1 library, for reference), but, especially with in-place patching skipping work on unchanged values, static linking still has a good shot at beating dynamic linking, especially if you run the binary multiple times)
Incremental compilation generally already depends on its stored intermediate data not getting corrupted, and the final binary need not be any differently handled in that aspect.
Right now, yep, corrupting the binary that the compiler reads would crash the compiler. In future, we want to detect the corruption and force a clean build. Note however that the Zig compiler is writing the binary to its internal cache directory (typically `.zig-cache/`), and the build system then copies the final artifact to your output ("prefix") directory (`zig-out/` by default), so it doesn't matter if the user messes with the final binary in `zig-out/bin/my_program`, because that's just a copy. Lastly, this is not implemented yet, but we will definitely make sure that Ctrl+C leave the cache in a clean state. I'm pretty sure our incremental compilation system has a nice property that you can just cancel an update partway through and continue it later without too much effort. See also Zig's IO interface [0] for information about cancelation. The compiler should already support graceful-ish cancelation internally (I won't claim to have verified this, because we never actually do cancel compilation right now, but I'm not aware of any glaring issues!). I don't think it'd be a crazy amount of work to improve that so that it also leaves incremental compilation in a valid state.
[0]: https://kristoff.it/blog/zig-new-async-io/
Shared libraries work slightly differently on each operating system, and at least on Windows and macOS, having to load thousands of tiny shared libraries will almost definitely cause your startup time to explode. I once wrote an 'OOP system for C' where each class lives in its own DLL, and let's just say that this was probably the most stupid idea I ever came up with, and I came up with a lot of stupid ideas in my life ;)
The only thing I discussed that we could potentially avoid using your suggested strategy is incremental linking, but AFAICT that would only be easily avoidable if we made a separate shared object for each individual function. I guess we could group them arbitrarily and then re-run codegen for everything in one shared library when the other parts change, but that frankly doesn't sound much less awkward than incremental linking!
But putting all that aside: the only thing this approach would really achieve is offloading the linking work from the static linker to the dynamic linker (aka runtime linker). So if it did make compilation faster, I'd expect all of the saved time to just become runtime execution time---which is kind of worse, because the average number of times you run a compiled program is probably >1!
Of course, there's also the obvious point that dynamic linking only works in cases where you can run dynamic executables. For instance, that approach wouldn't work when doing operating system development, while our approach should work completely fine for that use case.
How does this work given that e.g. a constant can be computed by a comptime function?
A bit after that quote I have a note about `inline` functions in Zig, where I mention that they perform semantic inlining, which means dependencies triggered by the function actually get associated with the call site. Well, `comptime` function calls work just the same way---in fact, to the compiler, `comptime` calls are almost exactly identical to `inline` calls. So when we encounter a comptime function call, we start analyzing the ZIR for that function's body, but we don't switch our analysis unit, so comptime stuff doesn't really complicate the dependency graph at all (aside from the fact that it means you can depend on any number of source code hashes, instead of everything depending on exactly one).
With all that being said, there actually is a (completely unrelated) way in Zig to can depend on the body of a runtime function (hence why the quote includes "at least in the simplified view I'm presenting here"). It's to do with "inferred error sets" (IESes for short). If a function's return type is written `!T`, that means it can return an error, but we're asking the compiler to figure out exactly which errors are possible. So if at some point we need to know that set of errors (e.g. because the user has done some reflection to try and access the list of errors), that's where we get a dependency on a runtime function body, because we need to analyze the function body to learn about all the places it might return an error.
Really fun and fascinating problem to work on.
Roslyn also has an extra constraint of integrating with live editing on the fly; I think you can get simpler and/or have different constraints if your requirement is only incremental compilation.
Using this native (written in Zig) C compiler to translate C source into Zig source as a part of the build, would presumably lend itself trivially to all the incremental logic in TFA, as updating C would update the generated Zig, and the incremental logic would detect differences just like it detects differences made by a human in an editor. Maybe there are aspects of the generated Zig that would complicate that somewhat, but I don't know -- just a warning about my ignorance.
This is part of plans to remove the hard LLVM dependency. AFAIK, the LLVM dependency will still be a variant many will use for the convenience of Zig as a much better clang, but removing the hard dependency is part of enabling all these great features like incremental compilation.
That's not planned AFAIK (see https://github.com/ziglang/zig/issues/16269). `translate-c` is really only intended for header translation, not C source code.
See https://github.com/ziglang/zig/issues/20875 for the (not fully fleshed out yet) plans around C compilation.
(To be clear, squeek502 is a part of the Zig core team [0], so he knows what he's talking about :D)
[0]: https://ziglang.org/news/welcoming-new-team-members/
TL;DR: only debug builds for now, could extend to release builds once we have our own optimisation passes one day, but some optimisations will still be inapplicable.
Getting incremental linker to work with llvm is kinda hard atm. Maybe the zig team has a plan dunno.
10 PRINT “Hello World”
Beautifully simple and readable. But it’s not a good language by modern standards.
In your example, I see a lot of complexity being surfaced: output streams, locals instead of globals, error handling. I don’t know Zig but all of those are things that are important to address, and I like that the example doesn’t sweep them under the rug in pursuit of a false readability.
Alternatively, here's a simpler version (prints to stderr).
In practice, you normally don't want to print messages to stdout. So the increased friction here actually pushes you in a better direction.Explicitly passing IO in is a fine design choice, but it's not a correctness issue to say others are wrong to not do so.
Should they?
The C Programming Language includes a Hello World example that calls printf without checking the return value and returns a success code from the main function regardless.
The first example I find when googling "java hello world" simply calls System.out.println and neglects to call System.out.checkError to see if it was successful before exiting with a success code. Some Java developers won't even know what I'm talking about here because it has never occurred to them that printing may fail in a way that can only be discovered through this weird checking mechanism.
Go's example from their getting started guide simply calls fmt.Println while ignoring the return values which include any error that may have occurred, and the program exits with a success code regardless.
The example from Rust by Example is at least correct and thorough in that it will predictably panic upon error when invoking the println! macro, which is documented, but will through that mechanism not give you the option to actually handle the error except by using a different mechanism which front-loads more of the complexity (e.g. writeln!(io::stdout(), "Hello World")? for something equivalent to the Zig example).
Of course for something as basic as Hello World it might be easy to tell whether it was successful through a quick glance at the output, but consider some of these limitations in a larger program.
So maybe there is more inherent complexity to this problem than a typical Hello World implementation will reveal. Add to that the complexity of Zig's new swappable I/O models and their Hello World isn't so absurd.
> and how likely is that to fail anyway (at least I never had the canonical C hello-world fail on me).
I don't expect to know how likely writing to stdout is to fail and I don't think any answer to that question other than 0 really warrants ignoring the potential error if the correct result of invoking your program depends on it. For what it's worth, at least in Unix-likes, stdout could be pretty much anything. Writing to stdout could fail because a switch at the user's ISP is rebooting.
Arguably this is the more beginner friendly version, this prints to stderr though:
hello.zig:
...and then