Making Rust core and alloc panic free

I have been working with Rust in embedded contexts for a while now. While I absolutely love the rust compiler and borrow checker, I am still not completely sold on Rust panics.

I do understand panics in Rust std; they make the language a lot better to work with. However, I do not like the existence of implicit panics in core and alloc. Generally, Rust panics should be used in unrecoverable situations. It easier to argue which situations can be considered as unrecoverable in the context of Rust std since the targets that support std are limited and mostly have some underlying system.

The core and alloc (to some extent) seem to be present to allow using Rust where only basic C or similar low-level language can be used. However, this also means it is challenging to use make assumptions of what these use cases entail. The best example I can think of is allocation error. It might be unrecoverable in case you are writing something to run on an OS. However, when writing an OS or firmware itself, it might simply be a minor inconvenience.

There have been some efforts to improve the situation by providing try_* methods which are useful. However, they seem like a stop gap (which some discussions admit they are). So I just wanted to know about some of the efforts going on to fix this (and maybe contribute to those if I get time).

Personally, I would love to see a core and alloc without panics. But feel free to tell me why I might be wrong.

Note: I am in no way against panic in Rust as a whole. Just not panicing in core and alloc would be nice.

7 Likes

I think this is a very general statement that most people would agree with, but more would likely come of it if there were concrete APIs proposed (which could then have an ACP).

One of the promising solutions I have seen is Keyword Generics.

I just wish to know the different efforts going on to tackle this and maybe contribute to them so that they can evolve into ACPs. Maybe a formal discussion about this will allow more eyes on the issue.

Can you please point out specific APIs and how exactly panics should be elided?

For example, Option::unwrap is defined in core. How do you propose to remove that panic?

2 Likes

I think I was a bit vague in the original post. I do not mind explicit panic like Option::unwrap or the panic! macro. Having them in core and alloc is fine.

What I am talking about is implicit panic. Stuff-like allocation errors. For example String::with_capacity will panic if the allocation fails. Seeing it right now, the docs should probably be updated to say it can panic on allocation failure.

The allocation situation is the most obvious since it is a problem for use inside the Linux Kernel. There are probably some other similar assumptions throughout core and alloc.

As for how to fix it while not making Rust a pain to use, I am not quite sure. I have seen some suggestions to maybe use compiler flags for this. Then there is Keyword Generics which may help if they ever land.

4 Likes

Hypothetically if core/std had a mechanism like Cargo features (ala the std-aware Cargo work), one of the on-by-default features could be panic.

Disabling the feature could disable the entire panic subsystem and any APIs which use it.

This would be particularly nice as a way of ensuring code is panic-free: instead of linting code with "panic detectors", anything that causes a panic would become a compile error.

This is beneficial for many environments where panics are considered an antifeature, including the aforementioned embedded space, but also the Linux kernel.

14 Likes

Can you give examples other than allocation? Your original post even mentions the WIP towards providing fallible APIs. That's the status quo as far as I know.

Keyword generics are in their infancy and I don't know of anyone working on how to apply that to allocation more generally.

I think it's just not quite clear what you're asking for here. If you're looking to join something, then I'd say the fallible allocation stuff is probably the place to do it. Other than that, I dunno what you're really asking for.

3 Likes

I'm actually not looking for anything in particular. It is a complex problem, and I have seen suggested solutions sprinkled across multiple PRs and Issues. So I just wanted to collect them here to have a more constructive discussion, maybe.

The current fallible APIs are supposed to be a stop-gap and will most probably never going to be stabilized, according to the discussion in the PR. So it would be a good idea to think about better solutions.

It reminded me async std crate. Probably, we need features to have async/sync/panic/result std realizations

Probably naming: "async-panic" (async std crate), "async-result", "sync-panic" (standart), "sync-result" (your desire)

At one time there was a proposal to have a PanicFreeSlice type that could offer the same operations as [T] but without panicking (yielding Option or Result instead). As far as I understand the current thinking is that there are an number of a similar axes that we might want to abstract over (like panic support/desirability, fallibility, allocator availability, constness) and we might want a better understanding at the language level if it's possible to avoid a combinatorial explosion of method implementations.

(This was from a conversation with @mara which I hope I'm doing justice.)

2 Likes

The fact that out-of-bounds indexing with slice[index] panics is fairly inviolate. This is because indexing syntax is special in that it produces a place, and not a value of some type.

slice[index] effectively desugars to *Index::index(&slice, index) or *IndexMut::index_mut(&mut slice, index) depending on how the place is used. This "mut genericism" is only possible due to the syntax producing a place.

2 Likes

I will say it sure would be nice to continue to use slice[index] in a no-panic subset of Rust, but only in cases where the compiler can elide the bounds check because it can statically assert the access is in-bounds (or otherwise make it a compile error).

But that's beginning to sound a lot more than something that just resembles a Cargo feature per my proposal above.

2 Likes

This will be quite speculative, but subtyping with pattern types could be useful for functions where the potential panics are due to required preconditions.

The simplest (and essentially useless) example of Option::unwrap could look like

// This would be the non-panicking signature for unwrap.
// In practice you'd want a way to merge it with the standard signature.
impl<T> Option<T> in Some(_) {
    fn unwrap(self) -> T {
        let Some(x) = self;
        x
    }
}

For indexing, with a bunch more imaginary syntax and associated handwaving, you might conceive something like:

impl<virtual N: usize, T> Index<usize in 0..N> for [T] in [_; N..] {
    fn index(&self, index: usize in 0..N) -> &Self::Output;
}

To be read as: For any virtual usize N, slices with at least N elements can be indexed with any usize less than N. By virtual I mean like const but may not monomorphize, so the body cannot depend on the value of N, which is only there to relate the types in the signature. Much like lifetimes.

For most general cases you'd still need to use alternatives like get, but at least this could cover some of the more obvious cases like indexing an array with some remainder of division by its constant length.

In a panic-free subset of Rust, you might then be able to use many APIs that would normally panic if some precondition is not met, as long as the inputs statically do meet them.

E.g.

#![deny(panic)]

let x: usize = ...
// ERR: usize / usize may panic
55 / x
// hint: usize / (usize in 1..) is panic-free, consider a match like:
match x {
    // OK because d has type usize in 1..
    d @ 1.. => 55 / d, 
    // OK because the literal 7 is not 0
    0 => 55 / 7, 
}

That's dependant types - a distinctive feature of proof languages. I'd love to see a "normal" language with them, but it seems they all already have enough problems!

2 Likes

I'm pretty sure this "mut genericism" and being able to yield "a place" is absolutely required if you want to be able to implement nice-to-use datastructures in any language. In particular you need something[index].something_else()[another_index] = 4 to work correctly. Look at delphi for an example of various ways to implement user defined indexed structures without this.

It's kinda why C++ has references at all, like allowing rebinding of the return value of the subscript makes stuff like arr[i] = 4 break.

This topic was automatically closed 90 days after the last reply. New replies are no longer allowed.