Hacker News new | ask | show | jobs
by awesan 99 days ago
IMO rust started at this from the wrong direction. Comparing to something like zig which just cannot panic unless the developer wrote the thing that does the panic, cannot allocate unless the developer wrote the allocation, etc.

Rust instead has all these implicit things that just happen, and now needs ways to specify that in particular cases, it doesn't.

3 comments

The problem isn't implicit things happening.

He's talking about this problem. Can this code panic?

    foo();
You can't easily answer that in Rust or Zig. In both cases you have to walk the entire call graph of the function (which could be arbitrarily large) and check for panics. It's not feasible to do by hand. The compiler could do it though.
"Panic-free" labels are so difficult to ascribe without being misleading because temporal memory effects can cause panics. Pusher too much onto your stack because the function happened to be preceded by a ton of other stack allocations? Crash. Heap too full and malloc failed? Crash. These things can happen from user input, so labelling a function no_panic just because it doesn't do any unchecked indexing can dangerously mislead readers into thinking code can't crash when it can.
There's plenty of independent interest in properly bounding stack usage because this would open up new use cases in deep embedded and Rust-on-the-GPU. Basically, if you statically exclude unbounded stack use, you don't even need memory protection to implement guard pages (or similar) for your call stack usage, which Rust now requires. But this probably requires work on the LLVM side, not just on Rust itself.

Failable memory allocations are already needed for Rust-on-Linux, so that also has independent interest.

What about doing something that Java does with the throws keyword? Would that make the checking easier?
I think that's exactly what's being asked for here (via the "panic effect" that the article refers to)

although, I think i'd prefer a "doesn't panic" effect just to keep backwards compatibility (allowing functions to panic by default)

Or effect aliases. But given that it's strictly a syntactic transformation it seems like make the wrong default today, fix it in the next edition. (Editions come with tools to update syntax changes)
Something like that, except you probably also want to be able to express things like “whatever the callback I’m passed can throw, I can throw all of that and also FooException”. And correctly handle the cases when the callback can throw FooException itself, and when one of the potential exceptions is dependent on a type parameter, and you see how this becomes a whole thing when done properly. But it’s doable.
> Comparing to something like zig which just cannot panic unless the developer wrote the thing that does the panic

The zig compiler can’t possibly guarantee this without knowing which parts of the code were written by you and which by other people (which is impossible).

So really it’s not “the developer” wrote the thing that does the panic, it’s “some developer” wrote it. And how is that different from rust?

Huh? It seems to me that in these respects the two languages are almost identical. If I tell the program to panic, it panics, and if I divide an integer by zero it... panics and either those are both "the developer wrote the thing" or neither is.
In Zig, dividing by 0 does not panic unless you decide that it should or go out of your way to use unsafe primitives [1]. Same for trying to allocate more memory than is available. The general difference is as follows (IMO):

Rust tries to prevent developers from doing bad things, then has to include ways to avoid these checks for cases where it cannot prove that bad things are actually OK. Zig (and many others such as Odin, Jai, etc.) allow anything by default, but surface the fact that issues can occur in its API design. In practice the result is the same, but Rust needs to be much more complex both to do the proving and to allow the developers to ignore its rules.

[1]: https://ziglang.org/documentation/0.15.2/std/#std.math.divEx...

Could you clarify what's going on in the Zig docs[0], then? My reading of them is that Zig definitely allows you to try to divide by 0 in a way the compiler doesn't catch, and this results in a panic at runtime.

I'd be interested if this weren't true, since the only feasible compiler solutions to preventing division-by-0 errors are either: defining the behaviour, which always ends up surprising people later on, or; incredibly cumbersome or underperformant type systems/analyses which ensure that denominators are never 0.

It doesn't look like Zig does either of these.

[0]: https://ziglang.org/documentation/master/#Division-by-Zero

> the only feasible compiler solutions to preventing division-by-0 errors are either: defining the behaviour, which always ends up surprising people later on, or; incredibly cumbersome or underperformant type systems/analyses which ensure that denominators are never 0.

I don't think it's very cumbersome if the compiler checks if the divisor could be zero. Some programming languages (Kotlin, Swift, Rust, Typescript...) already do something similar for possible null pointer access: they require that you add a check "if s == null" before the access. The same can be done for division (and remainder / modulo). In my own programming language, this is what I do: you can not have a division by zero at runtime, because the compiler does not allow it [1]. In my experience, integer division by a variable is not all that common in reality. (And floating point division does not panic, and integer division by a non-zero constant doesn't panic either). If needed, one could use a static function that returns 0 or panics or whatever is best.

[1] https://github.com/thomasmueller/bau-lang/blob/main/README.m...

>Some programming languages (Kotlin, Swift, Rust, Typescript...) already do something similar for possible null pointer access: they require that you add a check "if s == null" before the access.

For Rust, this is not accurate (though I don't know for the other languages). The type system instead simply enforces that pointers are non-null, and no checks are necessary. Such a check appears if the programmer opts in to the nullable pointer type.

The comparison between pointers and integers is not a sensible one, since it's easy to stay in the world of non-null pointers once you start there. There's no equivalent ergonomics for the type of non-zero integers, since you have to forbid many operations that can produce 0 even on non-0 inputs (or onerously check that they never yield 0 at runtime).

>The same can be done for division (and remainder / modulo). In my own programming language, this is what I do: you can not have a division by zero at runtime, because the compiler does not allow it... In my experience, integer division by a variable is not all that common in reality

That's another option, but I hardly find it a real solution, since it involves the programmer inserting a lot of boilerplate to handle a case that might actually never come up in most code, and where a panic would often be totally fine.

Coming back to the actual article, this is where an effect system would be quite useful: programmers who actually want to have their code be panic-free, and who therefore want or need to insert these checks, can mark their code as lacking the panic effect. But I think it's fine for division to be exposed as a panicking operation by default, since it's expected and not so annoying to use.

The syntax in Kotin is: "val name: String? = getName(); if (name != null) { println(name.length) // safe: compiler knows it's not null }" So, there is no explicit type conversion needed.

I'm arguing for integer / and %, there is no need for an explicit "non-zero integer" type: the divisor is just an integer, and the compiler need to have a prove that the value is not zero. For places where panic is fine, there could be a method that explicitly panics in case of zero.

I agree an annotation / effect system would be useful, where you can mark sections of the code "panic-free" or "safe" in some sense. But "safe" has many flavors: array-out-of-bounds, division by zero, stack overflow, out-of-memory, endless loop. Ada SPARK allows to prove absence of runtime errors using "pragma annotate". Also Dafny, Lean have similar features (in Lean you can give a prove).

> I think it's fine for division to be exposed as a panicking operation by default

That might be true. I think division (by non-constants) is not very common, but it would be good to analyze this in more detail, maybe by analyzing a large codebase... Division by zero does cause issues sometimes, and so the question is, how much of a problem is it if you disallow unchecked division, versus the problems if you don't check.

More specifically, Zig will return an error type from the division and if this isn't handled THEN it will panic, kind of like an exception except it can be handled with proper pattern matching.
I can't find anything related to division returning an error type. Looking at std.math.divExact, rem, mod, add, sub, etc. it looks to me like you're expected to use these if you don't want to panic.
Actually you're right, I was going by the source code which was in the link of the comment you replied to, but I missed that that was specifically for divExact and not just primitive division.