This idea encapsulates the usage of a custom core library that does not provide panicking functions and instead makes every API return an Option or Result of sorts, or use fallback approaches wherever applicable. I'm talking about this panic-free subset of Rust, not panic="abort" or the std library panic_immediate_abort features.
I'd love to tackle this problem by myself, but I currently lack a development environment suitable for developing an alternate libcore and rustc itself. Is there anyone who's tackling this problem nowadays? What is the current status? What may be some hard-to-answer questions? I'm sure this is not a change as easy as replacing all API's with Option and Result.
To my knowledge, no one is working on an alternate core/alloc/std that has had all panicking functions removed, and I don't expect such a library to ever become a core part of Rust.
If you want code that can't panic under any conditions, I'd expect this would be the easiest approach:
Make a lint that disallows calling any std function or macro that can panic (probably easiest done by forking clippy, they have a lot of lints for specific macros/functions that can panic, but nothing targeting everything), manually looking at all the std functions to see which ones panic.
Forbid the lint on all of your code (and ensure other dependencies of yours don't hit the lint, but I'd expect you're mostly rewriting from scratch if you disallow any panics), so you know for sure you don't have hidden panics.
If you find any functions in std that can panic and don't have a fallible version that returns Result/Option, make an RFC/ACP/PR to add such a corresponding function.
Alternatively, there is the no-panic crate that does linker-time checks to ensure code can't panic, but it allows for panics that the optimizer is able to rule out (e.g. if false { panic!() } will optimize out, so no-panic won't fail) and it has some other limitations (listed on their readme).
What, exactly is your goal for this? You've described a solution, not a problem.
(For example, what if the implementation of Option::insert has a panic internally, but that panic is fine because it's unreachable? Is that a problem? If so, why?)
Indexing syntax is unavoidably a problem, because container[index] syntax creates a place, not a value, and you can only have an Option<_> of a value. And indexing creating a place is a fundamental necessity for &v[i] and &mut v[i] to reference the item in the container.
Division, on the other hand, could theoretically be subset to panic-free already by only allowing integer division by NonZero instead of by primitive integers.
But also, just using methods isn't completely unreasonable. It won't look like particularly nice Rust code anymore, but that's unfortunately a known compromise required to get panic freedom. The entire reason panics exist is because some way to respond to "impossible" situations is necessary for ergonomics, and a panic is much nicer than just a raw abort crash. It's a standard error response hook for fatal errors.
It certainly would be nice if all code precisely tracked what conditions potentially cause panics, with similar precision to how conventional Rust tracks unsafety (potential UB), but it's just reality that bugs happen. Personally, I'd rather have code written that panics when some case thought to be entirely impossible happens, instead of getting UB. And in some cases, getting an erroneous None result when Some(_) is correct instead of a bug indicating panic is actually worse for overall resiliency, since None is an expected result for other cases.
The general "solution" for panic freedom is to return Result<T, AppError> everywhere. But the funny thing is, this is only really an improvement if that code path was already producing a Result. If it just returns T otherwise, sure side table unwinding isn't free, but it's still better than diluting icache with unused error resiliency code. (The optimization argument doesn't apply, since handling Result has the same barriers to optimization that unwinding does.) Even more so since parts of the
Unwinding is a general purpose solution which does consistently okay in roughly all cases. Propagation of Result::Err will be better in some cases and worse in others. Really, I think the "ideal" handling for panics in high-resiliency systems is to loop { thread::sleep() } in the panic handler/hook, and turn them into a leak of resources held by the thread. Rust programs' resource usage is good enough that this can still remain competitive.
I'd say it's not even necessarily an improvement even if it was already returning a Result.
I once worked in a large C++ code base whose reliability substantially improved when it started using a CRASH_UNLESS macro pervasively. If the dev thinks something is impossible, it's much better to crash and make them address that misunderstanding -- at the point where the crash dump actually has useful information! -- rather than end up in untested error handling code that leaves you in a weird state that ends up causing strange behaviour later with nothing to show for it except a completely-unhelpful "Option was None" top-level message.
(This does assume things like "you can ship fixes promptly" and "you have good telemetry for crashes" and "you have a recovery method for user data on crashes", but those are all things you ought to have anyway.)
I'm not fond of a "crash unless" approach as a general recommendation. It needs to be stated with big caveats about which domains it applies to.
I work on human safety critical software, and for all errors you need to be able to go into a safe failure. Which is generally not an all out crash.
Some such software there might not even be a safe way to fail in (for example pace makers or ABS brakes, where not working might lead to deaths). Thankfully I don't work on that, for what I do "tell hardware to apply estop circuit" is a safe way to fail. So you can apply that from a crash handler, assuming you can guarantee it runs quickly enough after the issue happens, which is usually doable, or the watchdog will do it anyway.
One thing I think explicitly worth admitting is that while it's imho a good thing that Rust pushes us to using the "correct" solutions, setting up e.g. full resiliency is significant effort that can be difficult to justify when a partial solution can be "good enough" to satisfy the letter of requirements, if not the spirit. And in some cases, it can feel like Rust can make the "good enough" solution harder to match the level of the proper one, instead of making the proper solution easier to the level of the "good enough" one. (Usually they meet somewhere in the middle, in practice.)
I'm not fully certain where fault resiliency falls on this scale. Imho, panic unwinding in practice is the "good enough" option for the vast majority of cases, but the reported reality is that Rust makes it more difficult to handle when unwinding isn't appropriate for whatever reason. (Even if that can sometimes be a misguided understanding of tradeoffs, it can also be legitimate.)
I think it could be a really nice thing if Rust could add an option to use a userspace unwind mechanism, so "targets without unwinding support" isn't as strong of a concern. It'd be annoying and likely inefficient[1], but it'd at least partially serve a decent subset of the requests for unwind freedom.
Namely, LLVM's exception handling only works with side table unwinding, currently meaning DWARF, SJLJ, or SEH. So we'd need to reify out our exception ABI before handing it to LLVM, and LLVM doesn't like to do optimizations based on exposing alternate call ABIs, whereas it can optimize an unwinding call to a non-unwinding call if it knows no unwinds can occur. So it'd likely lead to many regressions in what panics can get optimized out consistently.
But I still think it'd still be a nice thing to have, as long as it's clear that code shouldn't be contorted to behave better on the alternate backend. At worst, it'd be a useful teaching tool, and a decent comparison of unwinding versus result handling at scale. ↩︎
The unwinding crate should work on pretty much every target other than wasm afaik. Or at least it can support pretty much every architecture provided that you write some inline asm for the architecture to save and restore registers. (unwinding/src/unwinder/arch at trunk · nbdd0121/unwinding · GitHub) It fully supports no_std targets and we already use it in libstd for xous, which doesn't have a system unwinder.
I want to mention that in many contexts one only wants to exclude some causes of panic. For example, a parser should not ever panic because of invalid input, but panicking due to memory allocation failure or internal assertion failures may still be acceptable.
Right now there are tricks you can pull to exclude all panic, but I think the only available way to weed out just some causes of panic is via testing, which is hard. Continuing the example, if we could have a way to turn off container[index] syntax without also affecting Vec::with_capacity and .unwrap(), that would make it significantly easier to write this hypothetical parser and be confident it is robust (in this sense) to invalid input.
(Yes, I'd be perfectly fine with having to write method calls instead of &v[a..b] in this mode. I'm already doing that, I just have no good way to be sure I didn't miss one.)
I generally think that adding "unless you're working on a nuclear reactor or something" on all advice wouldn't be worth the noise.
I expect that people working in safety-critical software know that the rules there are different, and don't need me to tell them that -- after all, if they do, then we're really in trouble
And even in critical things, designing so that it's ok for as much as possible to crash without severe consequences is good. Yes, the autopilot for airbus Direct Law cannot crash, period, but it also is much simpler. Whereas I could imagine a design such that it's totally fine for the Pitch/Bank protection to crash in Normal Law, since it's obviously better to crash the software than cause bad control outputs (and possibly crash the plane), and there's Alternate Law to fall back to if those Pitch/Bank protections fail.
Interesting, flight is not a domain I work in (my knowledge there is limited to playing around in flight sims as a teenager), but those fallbacks seem unusually well thought out. [1]
And yes, you should minimise the code that cannot crash[2]. This is what watchdogs are for. You then have one (or usually more) watchdog microcontroller(s) watching the heartbeat (over a CAN bus for example) and triggering an automatic emergency response such as estop, switching flight mode, etc when the watchdog times out. Of course the watchdog software itself falls in the must-not-crash category.
Which does bring us back to some pain points with current panic in Rust.
Most targets cannot do unwinding. Only those running on an OS can do that.
If you need to write panic free code (for whatever reason), this is rather tricky in Rust today.
Way too many things can panic. While in my line of work we allocate up front or even statically, that is one example that is a pain point in the kernel. But there are a ton of other examples.
There is in general not a good way to discover what can panic. The docs might say it, but sometimes the panic can be several layers down, and then it might be undocumented.
I'm also not aware of any stability guarantees around adding panics in std.
An effects systems for panic/nopanic would be a boon. Panic will have to be the default to not break backward compatibility. So it will have to be like const: opt in to not have the effect. For the majority that is probably the right default anyway. I haven't heard anything recent on the effects/keyword generic effort, which seems relevant here. Has it stalled?
Though "mechanical backup" seems suspect to me, there appears to only be two sets of controls (pitch and yaw, no roll?) which I don't see how you would bank with? But that is very much unrelated to Rust, or even computers in general. ↩︎
You should also minimise code that needs to run as hard realtime, and minimise code in general would be a good idea too. And write straightforward simple code. Etc, etc. ↩︎
This reminds me of a thing I learned about traffic light control a while back:
Apparently they have a physical system in place so that if the software even sends conflicting greens, it burns out some fuses that forces all the lights to red and cannot be reset without manual intervention.
In more concrete terms (since that last one is problematic): Delay the dereferencing (* or .deref()) to the place where the value (Index::Output) is first needed as a place instead of doing it immediately:
&v[i]: Taking a reference of something requires a place, so this matches the current behavior
Same for &mut v[i]
v[i] = expression: Assigning something requires a place, so this also matches the current behavior.
v[i]? = expression: The question-mark operator doesn't require a place, it is happy to work on a reference (see below). But the expression requires a place, so it is dereferenced then: *(v.index(i)?)
v[i].map(|x| x).map(|_| &mut other_variable).unwrap() = expression (same for assignments): This is the most ridiculous example I could come up with and this approach still works: map is fine with working on Option<&mut T>, same for unwrap(). Only the assignment requires a place and thus the dereference happens just before the assignment.
v[i].map(|x| x = 10);: This one is a bit tricky. The assignment in the closure requires a place and thus this has to be written as or desugared to v.index(i).map(|x| *x = 10);, but the rule of desugaring just before a place is needed still holds.
I'm not saying it's easy, this is definitely a lot more involved than the simple *v.index(i) used at the moment, but especially those that don't involve closures should be doable.
The examples above are based on currently working rust, using let mut value: Option together with value.as_mut() in place of v.index(i):
fn main() {run();}
fn run() -> Option<()> {
let i = 0;
let mut v = vec![0, 0, 0];
*(if true {&mut v[i]} else {&mut v[i+1]}) = 7;
dbg!(v);
let mut value = Some(5);
*(value.as_mut()?) = 7;
dbg!(value);
let mut other_variable = 3;
*(value.as_mut().map(|x| x).map(|x| &mut other_variable).unwrap()) = 9;
dbg!(other_variable);
value.as_mut().map(|x| *x = 10);
dbg!(value);
None
}
I'm not pushing for this to be the case/used, but it would be a way to avoid the syntax issue around indexing. You still need something like v[i].unwrap() + optimizations or similar if you're checking bounds outside of the indexing of course.
One advantage of this is that error handling can be really short if the function already returns an Option, while still clearly indicating what happens: v[i]? = 5;
Note: This may cause issues if working with non-mutable references, but the same concept should still apply.