Hacker News new | ask | show | jobs
by 90s_dev 401 days ago
> But it does not nearly approach the level of systematic prevention of memory unsafety that rust achieves.

Unless I gravely misunderstood Zig when I learned it, the Zig approach to memory safety is to just write a ton of tests fully exercising your functions and let the test allocators find and log all your bugs for you. Not my favorite approach, but your article doesn't seem to take into account this entirely different mechanism.

3 comments

Yes, testing is Zig's answer. But that quote is right. Testing doesn't achieve the same kind of systematic prevention of memory bugs that rust does. (Or GC based languages like Go, Java, JS, etc.).

You can write tests to find bugs in any language. C + Valgrind will do most of the same thing for C that the debug allocator will do for zig. But that doesn't stop the avalanche of memory safety bugs in production C code.

I used to write a lot of javascript. At the time I swore by testing. "You need testing anyway - why not use it to find other kinds of bugs too?". Eventually I started writing more and more typescript, and now I can't go back. One day I ported a library I wrote for JSON based operational transform from javascript to typescript. That library has an insane 3:1 test:code ratio or something, and deterministic constraint testing. Despite all of that testing, the typescript type checker still found a previously unknown bug in my codebase.

As the saying goes, tests can only prove the presence of bugs. They cannot prove your code does not have bugs. For that, you need other approaches - like rust's borrow checker or a runtime GC.

static code analysis tools can also do it. there's no reason why the borrow checker must be in the compiler proper.
There's also no reason to have a separate borrow checker if it could just be integrated in the compiler.

When a compiler has a borrow checker that means the language was already designed to enable borrow checking in the first place. And if a language can let you do borrow checking why would you use a separate tool?

because it gets it out of the fast path compile cycle. do you need a borrow checker for `ls`? Probably not. don't use it. do you need it every time you work through intermediate ideas in a refactor? probably not. just turn it on in CI.
> do you need a borrow checker for `ls`? Probably not.

Does ls use references and objects with lifetimes? I bet it does. And if so, the answer is yes. You do need the borrow checker in rust to make sure it uses memory and lifetimes correctly.

If your program somehow doesn’t use references or owned objects, then the borrow checker doesn’t have any work to do. So there’s no harm done in leaving it on.

The borrow checker is not the slow part of the Rust compiler and lets me avoid bugs, why would I not always want to use it?

And if you put the borrow checker in the CI you massively increased the latency between writing the code and getting all relevant feedback from the compiler/tooling. This would do the opposite of what you intended.

You don't have to query all checks at all times. You only have to borrow check when you pr to main. What is the sum latency of the coder dealing with conceptual problem of satisfying the borrow checker at every stage during an arduous refactor where they don't fully have the destination model in mind (and might want to try things out only to discard them). Versus, just ignoring it, seeing how the chips fall, and fixing ownership errors in one or two passes at the end once you've settled om the code structure? Of course you don't have to take the full round trip-to-someone-else's-computer latency either. If you can run it in CI, I presume you could run it locally too, and borrow checking is not particularly slow for the computer.

You're also not (because one can't) quantifying the problem of a developer getting exasperated and saying, fuck it, it passes the borrow checker, good enough, instead of actually taking the time to make their code legible first and then making sure it is memory safe. This absolutely happens.

The borrow checker is very fast.
computationally. But it slows down the programmer. It is not a zero-cost human operation. If it were, we wouldn't need computers to do it.
Also it’s a great way to make sure every library in the ecosystem passes the borrow checker.
why do you expect compile time static analysis to fail at this? unless youre loading a precompiled asset?
I don't. I think compile time static analysis is great. Upthread you said this:

> there's no reason why the borrow checker must be in the compiler proper.

On a technical front, I completely agree. But there's an ecosystem benefit to having the borrow checker as part of the compiler. If the borrow checker wasn't in the compiler proper, lots of people would "accidentally forget" to run it. As a result, lots of libraries would end up on crates.io which fail the borrow checker's checks. And that would be a knock on disaster for the ecosystem.

But yes, there's nothing stopping you writing a rust compiler without a borrow checker. It wouldn't change the resulting binaries at all.

Because you're always going to write some code that the tools can't reason about.
thats true for rust too (hence "unsafe")
"But seatbelts would also work if everybody was just choosing to use them rather than us mandating their fitment and use, so I don't understand why facts are true"

Amusingly this is even true for the linter, nobody ran the C linter, more or less everybody runs the Rust linter, the resulting improvement in code quality is everything you'd hope. All humans love to believe they're above average, most are not and average is by definition a mediocre aspiration. Do better.

what the hell are you talking about. if you are writing security conscious software you should turn on a static checker and proudly show a badge that says "this code is memory safe". if youre writing a custom data pipeline to be used in a niche scientific field where the consumers are you and anyone that wants to repro your pipeline, and everything is in arenas, who the fuck cares. don't bother with static analysis.
If everything is in arenas, lifetimes get much easier.

But, the borrow checker doesn't just check lifetimes. It also checks ownership, and that variables either have a single mutable reference or immutable references. The optimizer assumes those invariants are maintained in the code. Many of its optimizations wouldn't be sound otherwise.

So, if you could compile code which fails the borrow checker, there's all sorts of weird and wonderful sources of UB eagerly waiting to give you a really bad day - from aliasing issues to thread safety problems to use-after-free bugs. The borrow checker has been around forever in rust. So I don't think anyone has any idea what the implications would be of compiling "bad" code.

Point being, there are many many individual programs where none of those things you talk about exist. So why not have a programming system where you can actually turn those things off for development velocity.

I'm rejecting the idea that "opt-in" is bad. Opt-out is of course better, but "no choice" is not good.

I suppose you can even ship the test/logging allocator with your production build, and instruct your users to run your program with some option / env var set to activate it. This would allow to repro a problem right where it happens, hopefully with some info helpful for debugging attached.

Not a great approach for critical software, but may be much better than what C++ normally offers for e.g. game software, where the development speed definitely trumps correctness.

What that means, though, is that you have a choice between defining memory unsafely away completely with Rust or Swift, or trying to catch memory problems by a writing a bunch of additional code in Zig.
I’d argue that ‘a bunch of additional code’ to solve for memory safety is exactly what you’re doing in the ‘defining memory safety away’ example with Rust or Swift.

It’s just code you didn’t write and thus likely don’t understand as well.

This can potentially lead to performance and/or control flow issues that get incredibly difficult to debug.

That sounds a bit unfair. All that code that we neither wrote nor understood, I think in the case of Rust, it’s either the borrow checker or the compiler itself doing something it does best - i.e., “defining memory safety away”. If that’s the case, then labeling such tooling and language-enforced memory safety mechanisms as “a bunch of additional code…you didn’t write and…don’t understand” appears somewhat inaccurate, no?
It is quite fair as far as rust is concerned. For simple data structures, like doubly linked list,are hard problems for rust
So? That wasn't the claim. The GP poster said this:

> This can potentially lead to performance and/or control flow issues that get incredibly difficult to debug.

Writing a linked list in rust isn't difficult because of control flow issues, or because rust makes code harder to debug. (If you've spent any time in rust, you quickly learn that the opposite is true.) Linked lists are simply a bad match up for the constraints rust's borrow checker puts on your code.

In the same way, writing an OS kernel or a high performance b-tree is a hard problem for javascript. So what? Every language has things its bad at. Design your program differently or use a different language.

> This can potentially lead to performance and/or control flow issues that get incredibly difficult to debug.

The borrow checker only runs at compile-time. It doesn't change the semantic meaning - or the resulting performance - of your code.

The borrow checker makes rust a much more difficult and frustrating language to learn. The compiler will refuse to compile your code entirely if you violate its rules. But there's nothing magical going on in the compiler that changes your program. A rust binary is almost identical to the equivalent C binary.

Weird that Swift is your totem for "managed/collected runtime" and not Java (or C#/.NET, or Go, or even Javascript). I mean, it fits the bill, but it's hardly the best didactic choice.
I don't think they said anything about that?
The point was that basically no one knows Swift, and everyone knows Java. If you want to point out a memory safe language in the "managed garbage-collected runtime" family, you probably shouldn't pick Swift.
I wouldn’t put Swift in the same ‘managed garbage-collected runtime’ family as Java, C#/.NET, Go, and Javascript, so maybe they weren’t trying to do what you think.

Swift is more like a native systems programming language that makes it easy to trade performance for ergonomics (and does so by default).

What if -- stay with me now -- what if we solved it by just writing vastly less code, and having actually reusable code, instead of reinventing every type of wheel in every project? Maybe that's the real secret to sound code. Actual code reuse. I know it's a pipedream, but a man can dream, can't he?
The way we've done code reuse up to this point rarely lives up to its promises.

I don't know what the solution is, but these days I'm a lot more likely to simply copy code over to a new project rather than try to build general purpose libraries.

I feel like that's part of the mess Rust/Swift are getting themselves tangled up in, everything depends on everything which turns evolution into more and more of an uphill struggle.

Why? In C I'd understand. But cargo and the swift package manager work great.

By all means, rewrite little libraries instead of pulling in big ones. But if you're literally copy+pasting code between projects, it doesn't take much work to pull that code out into a shared library.

No, this doesn't solve the problem. Libraries have security issues like every other codebase.
Yeah that is the opposite take of recent posts that the Cargo/npm package dependence is way too heavy.

Saying we should rely on reusable modules is great and all, but that reusable code is going to be maintained by who now?

There's no sustainable pattern for this yet, most things are good graces of businesses or free time development, many become unmaintained over time- people who actually want to survive on developing and supporting reusable modules alone might actually be more rare than the unicorn devs.

I meant in programming in general, not specific to Rust or Cargo.