#panic-free subset of Rust

541 messages · Page 1 of 1 (latest)

fathom pond
#

here's some rust theory mumbo jumbo: when you look at a function's signature (ignoring doc comments), you can understand the following information:

  • the call convention extern "C" or native rust
  • the function arguments
  • the presence of unsafety contracts
  • argument types
  • generic arguments (optional)
  • generic bounds
  • constness
  • return type (and return control flow e.g. !)
  • variadicness

you know what the function signature doesn't communicate well? panics. all functions have the ability of implicitly panicking. a panic is a portion of code in the rust core library that gives any function the ability to just woop jump to that portion of code, terminate the entire program because it failed and not give 2 fucks about it. sure, it is better to panic than cause undefined behavior or memory unsafety, but we can do better.

imagine this: every function signature had an implicit Option<T> wrapping its return type. and if you don't handle this Option::None case, it's fine, because it bubbles out from the function (as if it had an implicit ? at the end of where it's been created at). Let's rename this Option to MayFail. now all rust functions have an implicit MayFail that can be optionally handled for failures. if you don't handle it, it's fine. the program will just go on with its day. this MayFail has no size overhead, and is just a transparent wrapper. does any of this feel familiar? lets try using it in practice:

#
fn my_function<T>(slice: &[T]) -> MayFail<T> {
    let ret = slice[69];
    // implicitly wrapped in `MayFail` 
    ret
}

fn main() {
    // very important task that must be running or else the world will burn
    let super_computer_instance = server::start();

    let my_array = [0, 1, 2]; // no 69th element
    let ret = my_function(my_array.as_slice()); // oh no! this function `MayFail`!
    // lets handle the case
    match ret {
        Some(ret) => {
            // all good
        }
        None => {
            // oops, let's abort the program
            std::process::exit(1);
        }
    }
}```
#

what happened here? well, there was a very important function/server running, and without and prior notice, we terminated the the program along with the important task (without gracefully shutting the task down) because our stupid array access failed (something totally unrelated to the program), and we handled the error by just exitting the program. could we have done better? sure.

now, where am i getting at you may ask?

#

turns out, all of this is actually real and present in contemporary rust code. consider this real rust function:

fn function<T>(slice: &[T]) -> T {
    slice[69]
}

fn main() {
    let array = [0, 1, 2];
    let ??? = function(array.as_slice());
}
#

turns out, the function actually has a return type of MayFail<T> and we handle the error case by doing the following:

use std::panicking::catch_unwind;
let res = catch_unwind(|| {
    function(array.as_slice())
};
match res {
    Ok(res) => res,
    Err(_) => std::process::exit(1),
}
#

(actually none of the MayFail functionality exists in either the core crate or the std crate, i am just using these terms as a demonstration of what i am trying to explain here)

#

well, there are 2 things wrong with this:

  • first, if you want to avoid all panics, you're out of luck, because for every function you call, you have to build a catch_unwind trampoline that catches the unwinding panic and lets the program resume. this feels very redundant and boilerplate-y
  • second, the language does not give you an opportunity to just avoid this leaky functionality, even with #![no_std].
#

the panicking functionality is part of the core crate and an integral part of rustc, used as a quick means of aborting the program if anything went wrong, instead of having to deal with the Option/Result API's as intended

#

for my ideal world, i would want 1 or more of the 4 following details to be implemented in the language:

  • give me a means of opting out of panics. C never needed panics, and it is still the lingua-franca of the programming world. It is always possible to encode failure cases in the function signature by using an erroring type. I want to be able to look at a function and determine that that specific function will either unconditionally succeed, or fail and give me an opportunity to handle the error case. this should be opt-in, not a default. i'm not suggesting we remove panics as a concept, i am suggesting a #![feature(no_panic)].
  • convert all panicking API's inside the core and standard libraries into infallible Option/Result oriented API's. there are downsides to this, such as many many API's simply turning horrendously hard to use, and falling in a soup of match blocks that handle errors or bubble it out from the function.
  • make the function signature be able to communicate panics, and introduce an easier way to handle panics across any rust function. catch_unwind is very heavyweight for the problem of just handling an error path. the panic code path sets many thread local counters (such as panic_count of sorts), and catch_unwind isn't even available in codebases with panic="abort". not to mention all panicking paths being #[cold] by default.
  • introduce the implicit MayFail wrapper with custom semantics for all functions that can use panic functionality, while being optable-out at the same time (not returning MayFail = no access to core::panicking or core::panic)
#

Now, to get to the MayFail type i am proposing, this is a magical type that every function return type is implicitly wrapped with, if the fourth method of solving this problem is taken. given a function signature fn name(arg: type) -> rettype, one can assume the function may panic, and handle the error case like the following:

fn function(arg: i32) -> /* Mayfail< */ i32 /* > */{ arg }

fn main() {
    let ret = function(10);
    match ret { // notice how it looks like we're trying to match an `i32`, but in reality, we're matching a `MayFail<i32>`
        Some(ret) => ret,
        None => return, // bubbles up MayFail<()> out from main, this is **NOT** the same with exiting the process immediately, as it gives time for stuff to gracefully shut down
    }
}```
#

as you can see this is not very ergonomic and implicitly doing things are never good, but we have to have some form of control over our program while having a good API at the same time. so instead of matching explicitly like this, MayFail has an unwrap function that bubbles the MayFail::None path out of the current function instead of running the panic codepath, giving the higher-order caller an opportunity to handle the error. the following code is the same as the above.

fn function(arg: i32) -> /* Mayfail< */ i32 /* > */{ arg }

fn main() {
    let ret = function(10).unwrap();
}```
#

and for those who hate the word unwrap, MayFail will implement FromResidual or whatever from the core library, and thus allow any function to be bubbled out from the current function, instead of panicking:

fn function(arg: i32) -> /* Mayfail< */ i32 /* > */{ arg }

fn main() {
    let ret = function(10)?; // <- notice how we can bubble anything out even though main doesn't return `Result` or `Option` (it actually returns MayFail<()> which implements FromResidual)
}```
#

now, coming back to the example of the most important task, our code now becomes this:

fn my_function<T>(slice: &[T]) -> T {
    slice[69]
}

fn main() {
    // very important task that must be running or else the world will burn
    let super_computer_instance = server::start();

    let my_array = [0, 1, 2]; // no 69th element
    my_function(my_array.as_slice())?; // oh no! this function `MayFail`!
    // no worries, main will just return, and `Drop` super_computer_instance in the process.
}
#

this is very similar to how rust currently handles panicking. instead of forcefully taking control and exiting the program, we bubble out the panic and hope that the higher-order callers have a case that handles the panic. if not, the main function can always handle the error case, since it is implemented by libstd

#

the presence of MayFail introduces a question: how is the function signature going to communicate the information that "this function CAN NOT fail"?

the answer is simple: if a function can not panic, then that function does not need panicking-related code to be present. as such, a function that can not panic will automatically opt out of the panicking implementation from libcore, leaving it with literally no way to panic. if you try to call Option::unwrap in a function that can not panic, you will get a compile error saying unresolved import: core/panicking.rs or something.

you can communicate this intent with, oh, i don't know, a proc macro?

#[feature(no_panic)]
fn return_something<T: Default>(s: &[T]) -> T {
    s.get(69).unwrap_or_default()
}
#

in contrast to what the standard library says, with the MayFail implementation, unwinding does not act as if every function returned instantly, it will make every function return instantly unless you provided a path for the error case. this makes unwind safety a very easy task to handle. if you have control of what code will be run on panic, you can simply do your cleanup and let the panic continue:

impl<T: Clone> Vec<T> {
    fn push_all(&mut self, to_push: &[T]) {
        self.reserve(to_push.len());
        unsafe {
            // can't overflow because we just reserved this
            self.set_len(self.len() + to_push.len());

            for (i, x) in to_push.iter().enumerate() {
                let clone = x.clone();
                match clone {
                    Some(cloned) => self.ptr().add(i).write(cloned),
                    None => {
                        // do your unwind-safety related cleanup and propagate the "panic"
                        return; // <- returns MayFail::<()>::None
                    }
                }
            }
        }
    }
}```
#

this idea all stemmed from the fact that while learning rust from the rust by example book, i was taught that the function signature tells me whether a function will fail, or unconditionally succeed.

looking at a function signature and seeing that there's an Option, and going "hey, this function can fail! i should handle this!" was pretty cool. until i met panics. panics are not communicated in the function signature, which i have since become to hate. i don't hate the concept of panics itself; easy, dirty, quick, cheap error handling by aborting the program has always been my go-to, but being forced to use a #[panic_handler] for a #![no_std] crate i wrote and specifically avoided unwraps and panics for is just sad. my only choice is to either write my own core library that implements one of the solutions i proposed, or talk nonsense in a discord channel and expect the language to magically evolve the way i want it to work

#

now, lets get to one of my points i want to get across. the never type ! is a type that can't be instantiated, and represents parts of code (or nothingness) that can't be possibly reached. this can be something as easy as anything defined after a loop that never terminates loop {}; let never = 5;, or a magical thing. let me explain this magical thing.

#

in this stupid drawing of a stupid sketch, i want you to ignore everything in the image, and focus on the layers of abstraction on the left.

#

each layer of abstraction defines a level of execution in which the CPU does its things™️. In this graph, the binary that's been compiled from Rust code and is to-be-run is everything above from layer 4. In other words, Rust has the possibility of controlling anything from layer 4 and above, and it has been spawned from layer 3. it also defines anything that is above layer 4, aka. the tasks, operations etc. are all made up. but the final abstraction layer (instructions) are always there, since it is the only abstraction layer the CPU understands (and really works on)

#

now, we said that the ! never type indicates unreachable code. but unreachable how? there are 2 types of unreachable code:

  • a condition that will never succeed (if false == true)
  • an exit hatch to a lower level of abstraction.

we already know the first case, we use it all the time to optimize unnecessary bounds checks or unnecessary match arms, but what is the second?

well, the second is basically what MayFail implemented. it gives the execution in the current layer of abstraction (lets say a task running an operation) a means to "panic" and immediately return to the lower abstraction level.

for std::proces::exit, for example, this just terminates the program defined in the specific OS we're running in, and gives the resources back to the OS, and code following the exit becomes !, because it doesn't exist after exit is called anymore. thefore effectively returning to the 3rd layer of abstraction.

By returning to the lower abstraction level, for example, when the operation fails and gives the control back to task immediately. Task can either handle the error (via today's catch_unwind or MayFail matching), or ignore it (bubble the "panic" by doing nothing). If the task chooses to bubble the "panic", the program now has control, and may provide an error path if desired. see where i am getting at? in contemporary rust, a panic™️ (a real panic) avoids all these lowerings of abstraction layers, and simply terminates the program for good. this is very destructive, and what i hate about panics the most. it gives no granularity about how it's implemented. it is either the most destructive, or doesn't exist at all. (or something in between with a clunky catch_unwind if you're lucky and not using panic="abort".) this is what MayFail is trying to solve, to give granularity, give control to the user when they demand it, and be able to opt out when they don't want the functionality.

barren thicket
barren thicket
dusk horizon
# fathom pond now, we said that the `!` never type indicates unreachable code. but unreachable...

You keep bringing it up, so i have to say something. It's a little bit of nitpick but yeah:

a panic does not abort the program, but aborts the thread. This would mean "the program" if you panic on the main thread, but not necessarily always "the program"

I would also argue that it's not something done if "anything" goes wrong, but rather when an irrecoverable error has happened. It is an indication of a bug, and Option/Result in my opinion should not reflect bugs.

We are probably working in way different spaces, so i'm not surprised at all we may experience it very differently :)

barren thicket
fathom pond
# dusk horizon You keep bringing it up, so i have to say *something*. It's a little bit of nitp...

thank you for the very good response. yes, i am not very familiar with multithreaded contexts, but i usually found myself working with Mutex::lock().unwrap(), meaning if any thread panics, the thread waiting for the lock will panic too, and i've never really handled poison errors or such myself (i don't even know how i would approach that). it may simply be a skill issue on my side.

regarding the "bug" usage of panics, nothing changes. if you handle your panic correctly, or just bubble it, you will notice the bug. the bubbling nature of the panic acts exactly like an unwind, but gives you control when desired. if your function does not need an Option/Result, so be it, your function will still panic as normal if you don't expect an error (a bug is never expected).

dusk horizon
# fathom pond thank you for the very good response. yes, i am not very familiar with multithre...

I appreciate the discussion :)

Do you have any examples of when you are panicing where it makes sense to not panic, and return a Option / Result instead? If you're accessing an array out of bounds using .get() instead of using the Index trait ([]), you won't get a panic. you'll just get a None. I can't personally think of anything off the top of my head where i'd rather keep the program going.

#

I'm not necessarily against something like a #[cannot_panic] marker by the way.

#

Though, i do see function docs as part of a functions signature, in the sense that i'm looking at it when i'm looking at the signature. So to me it's pretty clear when something can and cannot panic. This is mostly true for std and core, can't speak for 3rd party crates.

fathom pond
# dusk horizon I appreciate the discussion :\) Do you have any examples of when you are `panic...

for example, Vec::push could've been made into a Result API that returned the item back if Vec::push or Vec::grow_one failed. fn push(&mut self, item: T) -> Result<&mut T, T> denotes everything that can happen in the function. it can either push the item to the vec, and return a mutable reference to it (this API is currently being discussed in github as fn push(&mut self, item: T) -> &mut T), or not push the item and return it back, where you may choose to gracefully shut the vector down, save your progress elsewhere, and terminate the program yourself. or you can just unwrap on the Err variant and bubble the panic out where someone outside of your control may handle your errors as they fit

dusk horizon
# fathom pond for example, `Vec::push` could've been made into a `Result` API that returned th...

Having more control over how your program fails seems fair and reasonable, no doubt! Although considering Vec::grow_one only seems to panic when you try to allocate more than nine petabytes of capacity for it.. I feel like it's safe to say that you'd have bigger fish to fry in that case. Not to mention the roughly 2500 kilometre span of modern high capacity ram modules needed to fit all of it in (not trying to be demeaning, i'm just being silly)

I feel like it'd be safe to say that .push() won't panic, and that the everyday ergonomics of making it return a Result instead seems worse. Though, a separate function that does exactly what you mentioned seems like a great idea for those that want to use it.

dusk horizon
#

A little off-topic... i do think the force-panic of things like the Index trait on slices and vecs, is really bad. In no way shape or form is it communicated that subscripting a classical array, slice or vector using [] as in, my_vec[index] (like what a ton of people learning rust will be used to from other languages) will panic. I think it just shouldn't be a thing.

#

though, sized slices do actually give you compile time errors when you try to access an index greater than It's size, which is super cool.

limpid geode
# dusk horizon Having more control over how your program fails seems fair and reasonable, no do...

(Disclaimer: Not an embedded programmer so this is only from reading blog posts)
It looks like they would be talking more about embedded systems, where there is a very real chance the allocator just says no. I forget the exact blog post, but one talked about sometimes it can be a matter of there's no memory now, but if you wait 2ms you will get memory, and without an OS that doesn't happen automatically. This led me to find try_reserve which could also fix this, but I get wanting to be hyper-paranoid and be forced to handle every possible error case. There is also no_panic, but that only works for local reasoning. A new panic mode would let you have confidence that no panic from any dependency could crash your program either. And even catch_unwind isn't guaranteed to resolve all panics, which could also come from a dependency.

limpid geode
dusk horizon
dusk horizon
# limpid geode (Disclaimer: Not an embedded programmer so this is only from reading blog posts)...

Personally, i haven't worked on embedded systems with an allocator, only without.

The default global allocator doesn't even panic in rust, it just aborts. But yeah, probably on an embedded target that does have a custom allocator for that target, it might fail. I haven't seen it personally ever happening... but then again i dont have much experience in embedded systems with enough RAM to use a global allocator.

random nova
#

(technically, the allocator does not abort, per se. It returns a null pointer. However, the standard library generally handles this by calling handle_alloc_error, which is basically just an abort)

#

Which is how things like try_reserve are implemented

random nova
# fathom pond in this stupid drawing of a stupid sketch, i want you to ignore everything in th...

sure, it is better to panic than cause undefined behavior or memory unsafety, but we can do better.
Citation Needed.

Your entire post appears to hinge on the idea that panics happen because people could not be bothered to write proper error handling. Some panics happen because of that, this is true. However, consider the following categories of error:

  • How about "this is a bug and should never happen" internal errors? You cannot reliably continue doing whatever you were doing if one of these is it, because it means that for unknown reasons an invariant was violated and as such you can no longer trust the code to be correct
  • How about "this isn't a bug, but I cannot recover from it" errors? You cannot continue whatever you were doing by definition if these happen.

Removing the concept of panics does not make these errors go away. it doesn't even allow you to handle them, because by their very nature they cannot be handled. It just means a lot of error enums now grow variants called Internal and Unrecoverable

#

As best as I can tell, the main effect thereof is that you write a few more ?s, and where you match errors you add an e => return e; branch at the end of the error, or e @ (Error::Internal | Error::Unrecoverable) => return e if you're being precise. And then in main you print an error and return, presumably.

#

So, the ultimate question is: What did we gain by doing this?

#

To clarify, I don't have any particular issues with the elaborate scheme you invented to check panics in the typesystem. I can tell you for a fact you have absolutely zero chances of getting an RFC to this effect approved: your idea would need to be something like twenty times simpler in order to not be considered overly complicated. I can also tell you that while you can certainly make your own core, you should not expect it to last.

I have various disagreements with certain viewpoints you espouse, which I'll elaborate on later, but if I recognise that your idea would solve the problems you wish to solve.

My issue isn't with the how you plan to constrain panics. My issue is with the why.

#

being forced to use a #[panic_handler] for a #![no_std] crate i wrote and specifically avoided unwraps and panics for is just sad

#

Compile with panic-immediate-abort and you won't be.

#

Of course, this doesn't solve the problem that you'd like to statically know there are no panics, rather than caring about what happens on a panic, but, the thing is, real code has unrecoverable errors.

random nova
#

If you'd rather not have the normal push at all, you'll be happy to know it's gated under #[cfg(not(no_global_oom_handling))], which you can set by passing --cfg no_global_oom_handling to rustc, via RUSTFLAGS to get it through cargo. You also need -Zbuild-std, since you need to compile std for std cfgs to apply.

random nova
#

C "doesn't need" panics because if your code absolutely relies on something not erroring it's easier to just not check for error and truck on pretending it succeeded

#

Unrecoverable errors do have a tendency to get reported to the user, but since the user can't recover from them, people usually just pretend they can't happen.

#

Granted, neither of these issues would be possible in a system based on Result, but they make a strong argument that suggests we cannot in good faith claim C lacking panics is a good thing. It's at minimum a more complicated effect.

#

convert all panicking API's inside the core and standard libraries into infallible Option/Result oriented API's. there are downsides to this, such as many many API's simply turning horrendously hard to use
True. Nobody wants to handle alloc errors all over the place. This is why instead of converting we should add, like we already started doing.

make the function signature be able to communicate panics
This makes panics a new kind of Result, and as such defeats the purpose.

catch_unwind is very heavyweight for the problem of just handling an error path. the panic code path sets many thread local counters (such as panic_count of sorts), and catch_unwind isn't even available in codebases with panic="abort". not to mention all panicking paths being #[cold] by default.
This is a feature, not a bug. Except in terms of binary size, panics are completely free for code that does not error. Making them any less expensive is a performance penalty for code that does not actually panic. Incidentally, it strikes me as a little odd that you made an entire post about being sure panics don't happen, and then you inserted a point about how panics should be less expensive if they do happen?

fossil iron
#

i totally agree with the need to "see where panics can happen". I think the idea of making ErrorHandling nicer is also decent. but I don't think we should conflate the two.

To me, the main property of panics is that they are unrecoverable, by definition. Unfortunately, this is currently not quite true in rust:

  • By default, you can catch unwinding
  • By default, only the current thread is aborted
  • People often use panics because they are convenient, not unrecoverable

In that sense, I like the idea of making error handling easier, to the point where bubbling an error is more convenient than panicking. And if that's the case, I think it would be nice if panicking was always aborting the process for proper unrecoverable errors.

If you just want to know whether a function can panic or not, effects are nice for this, though I believe not currently in scope for rust.

worldly scarab
#

i havent read most of this current thread and just want to throw in my two cents from the perspective of kernel dev:
panics are annoying most of the time when certain panics could be reasonable be handled (catch_undwind is not really useable in a kernel imo), mostly a problem of certain apis not having a (stable) non panic result api version (for example allocator api)
on the other hand explicit panics like unhandled under/overflow and asserts are helpful

fathom pond
# random nova So, the ultimate question is: **What did we gain by doing this?**

i never said we should remove panics as a concept. and if i did, i correct myself to say that panics are a must, because if panics had not existed, any internal error would be undefined behavior because there is simply no code that can be executed in the error path.

we gain the following by implementing whatever solution fits us the best:

  1. easier handle and understand exception (unwind) safety
  2. being able to recover from "unrecoverable" errors (i'll get to this later).
  3. have more control over the specific entrance and exit points of my program (no panics = no "holes" in the program that cause it to exit early.) rust is a systems programming language, and from the kernel point of view, a panic is basically a blue screen of death. i wouldn't want to have a blue screen of death whenever a slice is indexed wrong in my kernel. i'd rather shut the specific part of the kernel down and alert the user, or if it is critical, bubble the panic to shut the computer down, preferably at the end of my kernel main function, and not panic handler code.

the case of unrecoverable errors is pretty easy to answer. it is simple: if it is "unrecoverable", you terminate your part of the code, not mine. i use your library, and your library fails. you can only terminate up to your level of abstraction, not lower.

let's say your library is in the 5th level of abstraction, just over the program level of abstraction. my program uses your library, and expect it to work. then your library fails, and terminates everything it is concerned with. anything up from layer 5 is gone, and i can observe this from layer 4. i choose to do whatever should happen with the lowest level (4) program, not you. because you dont make the program, you make the library. this is simply a problem of how privileged a layer of abstraction is.

#

lets make another analogy. you come home, you boot up your computer, enter windows(or linux), boot up steam, enter a game, join a match, open your inventory, drag an item outside from your inventory, and now your entire computer shuts down. that's impossible right? because a software running inside so many layers of abstraction (power on, kernel running, os started, steam running, game running, lobby joined, inventory opened), doesn't have access to the lowest level of abstraction (the game cant turn your computer off). but panics do allow this. it gives a form of privilege escalation for higher level abstractions to shut the program down because it itself failed. the panic is part of the library, not the program.

random nova
#

you terminate your part of the code, not mine. i use your library, and your library fails. you can only terminate up to your level of abstraction, not lower.
This assumes that all errors that can be handled locally can be handled at a higher level, which is not true

Consider the case where my library interacts with some remote metadata. It might be another process, a server, whatever, really. An internal sanity check fails and we have reason to believe an unknown amount of state is corrupted in unknown ways. We return an error, and you have strictly less context than me for how to handle that error. You also can't fix the state, because you relied on my library to interact with it.

Yes, you could just propagate the error same as I do, but that's what already happens when I panic, we didn't change much.

#

i'd rather shut the specific part of the kernel down and alert the user
We do have catch_unwind. You can wrap arbitrary sections of your kernel in panic-proof bubbles and control how much you bring down

#

Well, you can provided your kernel has unwinding at all, which is admittedly debatable

#

If it doesn't, guess you get to not use the [] operator, or panicky methods in general. There's an entirely valid reason why people are annoyed by libraries that panic on error.

#

For reference, I'm entirely happy to support a compiler flag or an attribute for "if any panicky code is written, fail to compile"

fathom pond
# random nova > you terminate your part of the code, not mine. i use your library, and your l...

i dont specifically handle the error, i handle how my program uses your part of the library to do what the program should do. what has been corrupted, is on your side of the library, because it was the library's task to make sure that data is sane, and if it's not, you terminate the library with a panic. i can choose to either terminate my kernel, or keep on going, because in the end, i am the one to determine whether i use your library as a critical part of my project or a not-so-important-but-good-to-have one

random nova
fathom pond
#

if we don't expect people to make better API's that return result or option, we should at least give the higher order programmer a way to handle the error in an ergonomic and expected way

random nova
#

Why would we not expect people to make better APIs?

#

We've been doing that for a decade

#

So far, it's worked pretty well

fathom pond
#

because people have the ability to panic, so they will

#

and we made sure to not remove panics as a concept

random nova
#

You can't write a technical solution to the social problem of "not all libraries are written for all use cases"

fathom pond
random nova
#

We have handleable panic semantics, insofar as they are implementable.

#

As for maypanic, adding it is the kind of thing that would make kernel devs extremely happy and high level programmers extremely sad

#

Which suggests it would be great as an opt-in tool, not an annotation in the signature

fathom pond
#

why would high level programmers be sad to realize that their code may panic? they already can

fathom pond
#

the current panic semantics are good as is

#

but we can do better by opting in to some stuff

#

much like how we can opt into no_std and no_main and sorts

random nova
#

People already complain about how Results have a tendency to have to be added to a dozen functions if you intrduce a new fallible thing

#

The issue is (most) high level devs would prefer a baseline understanding of "everything is maypanic at all times", because for their use cases it's not worth the effort and annoyance to make it more specific.

#

It's a bit like the fact that low-level-algorithm devs, like the person who made the standard library's sorts, are annoyed all the time by the fact they have to deal with user code panicking and the panic being caught. The "panics must leave you in a state that isn't UB to use" rule is a pain for them.

At the same time, the webserver people very much enjoy how a panic can automatically be turned into an HTTP 500 by their framework of choice.

fathom pond
#

speficially talking about the 2nd implementation i proposed, if they dont want to handle their errors, they can just panic, since as long as they're concerned, their library is no longer sane, and they decided to terminate their library. me as the user of the library can decide what happens next by optionally handling their library's error case. i can choose not to handle their panic, and panic alongside them

random nova
#

Erlang is notoriously really good at error handling

fathom pond
#

maybe i'll look into it in a later time, thanks for the recommendation

random nova
#

Erlang also runs on a VM and is effectively an implementation of the actor model

#

Which sort of limits its versatility

#

The core you need here is that an erlang program is made of "processes". Thousands of them.

Processes have access to process-local mutable state and shared immutable state, and they communicate via asynchronous message passing.

Except for sending a message that says "an error occurred", the only way a process has to signal an error is to crash, and in general that's how it should handle errors. Erlang's motto is "Let it crash" for a reason.

#

The parent process is responsible for noticing the crash, figuring out what happened if it can, and then usually either restarting the child in a known-good state or crashing itself

fathom pond
#

that does sound pretty cool, then the current aborting on panicking mechanism can be explained by an actor that has been spawned by another actor that has been spawned by another actor ... that has been spawned by another actor can just skip all the indirection to kill the root actor in 1 move

random nova
#

That aside, I like, in principle, the idea of "no panics, period". I like it being an opt-in, too, and it can probably be simplified to a #![no_panic] attribute atop the crate root: #![no_std] takes away std, #![no_panic] takes away panics.

But, just like the existence of #![no_std] doesn't make all crates compatible with core-only code, the existence of #![no_panic] doesn't make all crates compatible with non-panicking code.

This may not inherently be a problem: #![no_std] has proven wildly successful, after all.

fathom pond
#

what i had in mind was to not have it as a crate level attribute, but more granularly localizable, such as denoting a single function #[no_panic] and suddenly all functions called inside that specific function does not have access to the core panicking functionality anymore, as if the implementation just disappeared

random nova
random nova
#

The answer would be "if and only if T's implementation of Ord is #[no_panic]", which brings us to the same small nightmare we've been having with const traits for years

fathom pond
#

impl<T> PartialOrd T { should be denoted no_panic in this case

#

so this is tangentially related to const traits too then

random nova
#

And therein the complexity skyrockets

#

I suppose if we get lucky after we finish figuring out how const traits should even work we can build panic checks on the same framework

fathom pond
#

#[lang = "no_panic"]
unsafe auto trait NoPanic lol

fossil iron
#

note that no_panic is different to never panics

#

if I have unreachable code that panics, a function can only be no_panic if it can be proven by the compiler that it never panics

#

I suppose we do have unreachable_unchecked in case only the dev can prove unreachability, but that's a very scary function imo

pearl saffron
swift narwhal
#

FWIW my opinion is that the only way to get rid of panics is either:

  1. have functions unnecessarily return a Result or Option which will get very annoying
  2. replace panics with UB
  3. optimize them out with an infinitely smart compiler
fathom pond
#

please read the post

#

i propose a method of optionally-handled-otherwise-bubbled-option type that prevents the result/option api from becoming infinitely annoying

#

you can think of it as the catch_unwind/panic system integrated into the type system

#

and what's more important is that you can completely ignore the feature and all code keeps working as normal

random nova
#

Cool, but extremely breaking in practice. Would probably have needed to be in Rust 1.0 ten years ago

#

You can make it an effect much like how Sized is a bound: it's there by default unless you opt out

#

Though you also need to solve the const traits issue: "this function is panicky iff the impl of this trait for this generic is"

stiff acorn
stiff acorn
#

having read voxell's original post and all of the monologue that followed it, twice, ...how does catch_unwind not do what you want, exactly?

  • libraries shouldn't set panic = "abort" unless they rely on it for soundness, and so binaries should probably not be trying to override panic = "abort".
  • even if you somehow override panic = "abort" and make the result do something sound, there will still be panics you cannot catch, like double or triple panics.
    setting aside those cases, catch_unwind bubbles panics up until they are handled and lets the handler decide what to do next on a case-by-case basis.
fathom pond
#

i dont think it is ergonomic enough to be used properly

#

also, all panicking code paths are #[cold] by default, making the optimizer worse on places where you may expect a panic

stiff acorn
#

you should only need catch_unwind when you intend to catch the unwind and not propagate it. for cases like your Vec::push_all example, where there's just cleanup to do as you unwind past this stack frame, scopeguard works just as well

#

in what cases would you expect a panic? in what cases would you prefer to optimize panicking, potentially at the expense of optimizing normal returns? genuine question

fathom pond
#

because i can't for sure know whether they panic or not

#

since being able to panic is not reflected in the function signature

#

it is not a breaking change for that library's maintainer to suddenly introduce a panic in their function

stiff acorn
#

right, panics are an invisible side effect not reflected in the function signature, with you there

#

not understanding why the potential for a library to panic means panicking needs to be fast, though. panicking is by definition off the hot path, it is for when something goes exceptionally wrong

fathom pond
#

right, when that something goes exceptionally wrong, it is exceptionally wrong only for the context of the library i am using, it is simply not privileged enough to shut my program down. since i decide whether that the library is being used as a critical (i will panic if the library panics) or non-critical (i will ignore the library panic or handle it differently) part of my program, it shouldn't tell the program to shut down, it should tell the program to shut the library down

stiff acorn
fathom pond
#

i dont understand what you are asking for

#

can you reword the question?

stiff acorn
fathom pond
#

if i am expecting a panic, shouldn't it be optimized just as any other path? if i am not expecting it, then it can be cold

#

by expecting i mean having a catch_unwind handler or not

stiff acorn
#

expecting as in "expecting to happen often" or "expecting to happen at all"?

fathom pond
#

not sure

#

||i havent programmed in 2 weeks, the ideas and the mindset i had 2 weeks ago has jaded, i dont remember much||

stiff acorn
#

panics are #[cold] for the same reason that the code path where Vec::push reallocates is #[cold]: the hot path is executed more often, so optimizing it to be as fast as possible (even if that involves pessimizing the cold path) is a net gain

#

that's understandable

worldly scarab
#

If you expect a panic then using a panic is the wrong approach and a result would be better, also it's annoying that panics are transparent it would be helpful if absence of panics could be encoded into the function signature

fathom pond
#

you can't control how all API's behave

worldly scarab
#

Well then you would have to work around that sadly

#

Crates io should have a flag for such crates that violate language contracts

#

And making panic encoding in the function signature as can panic and the absence indicate no panic it would intentionally break most code on the next edition

#

As in implement the panic in the function signature and warn if it's not there but the function can panic and on the next edition turn that warn into a compile error

#

It would be a drastic approach but I don't really see a less drastic way for something this fundamental and sometimes falsely used feature

fathom pond
#
fn foo(x: u32) -> u32 {
    if x > 10 {
        panic!() // panic_handler isn't even known in this scope, which means it must be determined at callsite (at compiletime)
    }
    x
}
let val: Option<u32> = 'block: {
    let panic_handler = |_: core::panicking::PanicInfo| -> ! {
        break 'block None;
    };
    // code...
    let x: u32 = Option::unwrap(None); // calls panic_handler somehow
    let res: u32 = foo(x); // calls panic_handler somehow
    Some(res)
};
match val {
    Some(val) => (),
    None => println!("val panicked"),
}

(pseudo) compiles to

let val: Option<u32> = 'block: {
    let x: u32 = match None {
        // this is just Option::unwrap with the None case replaced with our panic handler
        None => break 'block None,
        Some(val) => val,
    };
    let res: u32 = /* inlined foo to be able to reach 'block */ {
        if x > 5 {
            // panic!() replaced with our panic handler
            break 'block None;
        }
        x
    };
    Some(res)
};
match val {
    Some(val) => (),
    None => println!("val panicked"),
}
#

i'd been thinking of solutions to this problem, and one idea incorporated panic handlers that break out of blocks to emulate a ! which can then be used as a substitute for panic. when you break out of the block, you don't ever return to the callsite, giving us the ability to conjure up a ! at the callsite out of nowhere

#

the current limitation is that Option::unwrap() doesn't know which panic_handler it will call, or how it will call it

#

and you can't even break 'block None;

fathom pond
#

is probably robust to panic = "abort" (unless you don't want it to)

#

maybe a substitute for catch_unwind

quartz eagle
fathom pond
#

does it even unwind?

quartz eagle
#

If you want to cleanup the stack

#

You could choose to just forget everything

fathom pond
#

@quartz eagle

fathom pond
#
macro_rules! panic_block {
    ($label:lifetime: $block: block
    ) => {
        $label: {
            Some($block)
        }
    };
}
macro_rules! unwrap {
    ($label: lifetime, $option: ident) => {
        match $option {
            ::core::option::Option::None => break $label None,
            ::core::option::Option::Some(t) => t,
        }
    };

    ($label: lifetime, $option: expr) => {
        match $option {
            ::core::option::Option::None => break $label None,
            ::core::option::Option::Some(t) => t,
        }
    }
}
let caught: Option<i32> = panic_block! {
    'phandler: {
        let my_option: Option<&str> = Some("hello, world!");
        let str: &str = unwrap!('phandler, my_option);
        let str2: &str = unwrap!('phandler, str.get(1..));
        println!("{}", str2);
        str.len() as i32
    }
};
match caught {
    None => println!("a panic was caught!"),
    Some(val) => println!("Operation successful, length of the string: {}", val),
}
#

i've managed to implement non-panicking unwraps to some extent

#

expands to this code:

let caught: Option<i32> = 'phandler: {
    Some({
        let my_option: Option<&str> = Some("hello, world!");
        let str: &str = match my_option {
            ::core::option::Option::None => break 'phandler None,
            ::core::option::Option::Some(t) => t,
        };
        let str2: &str = match (str.get(1..)) {
            ::core::option::Option::None => break 'phandler None,
            ::core::option::Option::Some(t) => t,
        };
        println!("{}", str2);;
        str.len() as i32
    })
};    

fathom pond
#

this feels very much like option handling

#
let mut i32 = 0;
panic_block!('a: {
    let x: Option<&str> = None;
    let some_str: &str = Option_unwrap!(x, 'a);
    let res: Result<(), String> = Err("lol".into());
    let nothing: () = Result_unwrap!(res, 'a);
    i32 = 1 + some_str.len() as i32 + size_of_val(&nothing) as i32;
});

#

now it feels like a try block

unique pumice
#

This is quite relevant for compilers which cannot tolerate panics, e.g. eBPF targers

#

LLVM just treats the whole thing as undefined behaviour and the verifier explodes

stiff acorn
#

if this relies on inlining, how does it handle recursive functions?

fathom pond
#

as that would require language level support for passing block labels to functions

#

and you can't do that even if you try to use something like closures

swift narwhal
#

say if the i32 addition overflows

fathom pond
#

yes it doesnt

swift narwhal
#

creating a string has the possibility of immediately aborting which is technically not a panic but you'd probably want to catch that as well

fathom pond
#

im afraid this is nothing but a fancy way of writing breaks and block labels

fathom pond
fathom pond
swift narwhal
fathom pond
#

i dont think you get the picture here

#

in order to make handle_alloc_error "return" a !, we break to the label inside handle_alloc_error (by having handle_alloc_error be some sort of generic over a panic handler function impl FnOnce(&core::panic::PanicInfo) -> ! with a fallback of std panic), and since break statements "return" !, we'll just return that as the result of handle_alloc_error

#

this is actually 2 proposals in one: functions need to be able to accept block labels as generic arguments, and closures can capture block labels from the parent scope

fathom pond
quartz eagle
#

Isn't there an isomorphism between this and returning an enum

#

Since a call can be ```rs
match thing() {
Return(x) => x,
BreakA(y) => break 'a y,
BreakB(z) => break 'b z,
}

#

And to chain we replace a break with a return

fathom pond
quartz eagle
#

The example is for a hypothetical thing<'a_block, 'b_block>()

fathom pond
#
'a: {
    'b: {
        let a: || -> ! {
            // hypothetical part
            break 'a;
        };

        let b: || -> ! {
            // hypothetical part
            break 'b;
        };

        let none: i32 = match external {
            1 => a(),
            2 => b(),
            _ => 3,
        };
        // if external was _, we're here
    };
    // if external was 2, we're here.
};
// if external was 1, we're here
#

if you want to converge all of these control flow branches to one, you'd need to use an enum, yes

quartz eagle
#

Yeah my point is, is there a meaningful distinction between being generic over break labels and returning enums

#

From a functional perspective

fathom pond
#

well, enums change the result type of the function, and may widen it

#

and break labels don't touch the return type, they're just control flow, and compile time available

fathom pond
#

they do change the function signature i am sorry

#

misworded a little bit

quartz eagle
#

I'm not trying to say it's not meaningful, but we need some compelling reason for this to be more than "I would like a nicer syntax"

fathom pond
#

because ABI

#

wrapping your error with Option to indicate errors for example changes your return type from T to Option<T>, which may widen the resulting type

#

but having a label to break to and never return gives you a way to not widen the function signature

#

its not intrusive at all

#

also i dont get the part with "i would like a nicer syntax"

#

the syntax is really the last concern

quartz eagle
#

Would this just be a way to force this type of codegen

fathom pond
#

even if the code could get eliminated, it is not obligated nor guaranteed to do so, you still need to explicitly handle the none case in a function that returns an option, but you don't need to care about the panicking case of a function that breaks out and never returns to you in case something goes wrong

quartz eagle
fathom pond
#

yes but there is not a catch-all pattern here

#

for example, what if i wanted to catch the panic of an external Rust library that explicitly calls std::panic!() using my block label

#

the library author would have to introduce a bunch of block-label generic code just to support this

quartz eagle
#

Yes I understand that this system is opt in as the implementor

#

Same as changing the return type in that regard

fathom pond
quartz eagle
#

I don't see how it's useful enough to motivate adding the required features (syntax and modifications to the mir) to implement it

fathom pond
#

kernel development

#

you spawn tasks (block labels) that do some operation, and if they fail by panicking, instead of crashing your kernel you fail the task, and possibly restart it

#

it narrows the scope of error from program-wide to scope wide

quartz eagle
#

Why is it worth it to not just change the function signature to return an enum (or equivalent)

fathom pond
#

what if you're calling into another rust library

#

as i said

#

we start by controlling std::panic

quartz eagle
swift narwhal
#

@fathom pond I think I figured something out

#

?play

#![feature(alloc_error_hook)]
#![feature(allocator_api)]

use std::{
    alloc::{AllocError, set_alloc_error_hook},
    arch::asm,
    sync::atomic::{AtomicBool, AtomicUsize, Ordering},
};

static SAVED_IP: AtomicUsize = AtomicUsize::new(0);
static SAVED_SP: AtomicUsize = AtomicUsize::new(0);
static ALLOC_FAIL: AtomicBool = AtomicBool::new(false);

fn fallible_allocations(callback: fn()) -> Result<(), AllocError> {
    unsafe {
        let ip;
        let sp;
        asm! {
            "lea {}, [rip]",
            "mov {}, rsp",
            out(reg) ip,
            out(reg) sp
        }
        SAVED_IP.store(ip, Ordering::SeqCst);
        SAVED_SP.store(sp, Ordering::SeqCst);
    }

    if ALLOC_FAIL.load(Ordering::SeqCst) {
        ALLOC_FAIL.fetch_not(Ordering::SeqCst);
        return Err(AllocError);
    }

    set_alloc_error_hook(|_| {
        ALLOC_FAIL.fetch_not(Ordering::SeqCst);
        unsafe {
            asm! {
                "mov rsp, {}",
                "push {}",
                "ret",
                in(reg) SAVED_SP.load(Ordering::SeqCst),
                in(reg) SAVED_IP.load(Ordering::SeqCst)
            }
        }
    });

    callback();

    Ok(())
}

fn main() {
    let res: Result<(), AllocError> = fallible_allocations(|| {
        let _s: String = "a".repeat(isize::MAX as usize);
    });

    if res.is_err() {
        println!("the allocation failed");
    }

    println!("success")
}
mystic spireBOT
#
the allocation failed
success```
fathom pond
swift narwhal
quartz eagle
fathom pond
#

any form of Rust panic goes through the std handler, so if we control std panic, we control all valid panics, thats my reasoning

#

if you're calling ffi then all bets are off

#

unwinding from ffi into rust is ub

swift narwhal
quartz eagle
fathom pond
#

exactly

#

though we dont really unwind per se

#

the callsites are never returned to

quartz eagle
#

It would be using the same codegen

#

Since that's how the mir does it

fathom pond
#

oh ok

quartz eagle
#

The mir does support many return points

#

That's only exposed as panics

quartz eagle
# fathom pond exactly

So if we have ```rs
let x = panicky_function();

We opt in with
```rs
let y = 'a: {
  let x = panicky_function::<'a>();
};

Which becomes something of the form (using existing constructs)

let y = a': {
  let x = match catch_unwind(|| {
    panicky_function()
  }) {
    Ok(x) => x,
    Err(err) => break 'a err,
  };
};
fathom pond
#

as long as the panic handler is a lang item and not external we have a way of controlling all panics

fathom pond
#

though for that we'd need to control the codegen of panicky_function, which has been compiled before we did

#

there are some nuances

quartz eagle
#

Which if we are fine with could definitely be accepted

swift narwhal
fathom pond
#

what leaking?

fathom pond
quartz eagle
#

Unwinding is the process by which we can do out of band stack cleanup

#

Without it we lose that ability

fathom pond
swift narwhal
fathom pond
#

is what i'm referring to "cleaning the stack" isomorphic to unwinding?

#

if it is we'd optimally have unwinding too

fathom pond
#

i dont

quartz eagle
fathom pond
#

well, as i said, if we treat panic like an all-dropping operator that expands to something like drop(name); drop(other_values_in_scope); panic_handler(&panicinfo), this kind of works

quartz eagle
#

To not leave behind leaked data/resources

fathom pond
#

what is the problem with unwinding then

#

can we not unwind and call the panic handler at the same time?

quartz eagle
fathom pond
#

or does this operation not even make sense

quartz eagle
#

When unwinding is disabled then it doesn't add this

fathom pond
#

in which stage of compilation do we lower unwinding?

quartz eagle
#

Mir to llvm-ir is what implements it as real code instead of just jumping between blocks

#

In mir it's a form of multi return

fathom pond
#

well then it kind of compiles to a goto in llvm if it even exists

quartz eagle
fathom pond
#

makes sense

quartz eagle
#
pub fn demo(x: bool) {
    let _y = String::from("hi");

    if x {
        panic!();
    }
}

becomes

fn demo(_1: bool) -> () {
    debug x => _1;
    let mut _0: ();
    let _2: std::string::String;
    let _3: !;
    scope 1 {
        debug _y => _2;
    }

    bb0: {
        _2 = <String as From<&str>>::from(const "hi") -> [return: bb1, unwind continue];
    }

    bb1: {
        switchInt(copy _1) -> [0: bb3, otherwise: bb2];
    }

    bb2: {
        _3 = panic_cold_explicit() -> bb5;
    }

    bb3: {
        drop(_2) -> [return: bb4, unwind continue];
    }

    bb4: {
        return;
    }

    bb5 (cleanup): {
        drop(_2) -> [return: bb6, unwind terminate(cleanup)];
    }

    bb6 (cleanup): {
        resume;
    }
}

notice how we have [return: bb1, unwind continue] and (cleanup) blocks

#

in mir returns have a normal path and an unwind path which use different sequences of blocks

#

which is why panics do actually return in mir

#

they just return on the unwind path

fathom pond
#

how are break's handled in mir

#

i want to see the same function except not a function but a labeled block with a break on panic

#

the block evaluates to nothing just like demo

stiff acorn
swift narwhal
#

?godbolt

use std::hint::black_box;

#[unsafe(no_mangle)]
#[inline(never)]
fn may_panic(cond: bool) {
    if cond {
        panic!()
    }
}

#[unsafe(no_mangle)]
fn calls_panicking() {
    may_panic(black_box(false))
}
mystic spireBOT
#
may_panic:
        test    edi, edi
        jne     .LBB0_2
        ret
.LBB0_2:
        push    rax
        call    example::may_panic::panic_cold_explicit

calls_panicking:
        push    rax
        mov     byte ptr [rsp + 7], 0
        lea     rax, [rsp + 7]
        movzx   edi, byte ptr [rsp + 7]
        call    qword ptr [rip + may_panic@GOTPCREL]
        pop     rax
        ret

example::may_panic::panic_cold_explicit:
        push    rax
        lea     rdi, [rip + .Lanon.97af7616b7c7e084c1975bae30a41ed3.1]
        call    qword ptr [rip + core::panicking::panic_explicit@GOTPCREL]

.Lanon.97af7616b7c7e084c1975bae30a41ed3.0:
        .ascii  "/app/example.rs"

.Lanon.97af7616b7c7e084c1975bae30a41ed3.1:
        .quad   .Lanon.97af7616b7c7e084c1975bae30a41ed3.0
        .asciz  "\017\000\000\000\000\000\000\000\007\000\000\000\t\000\000"
```Note: only `pub fn` at file scope are shown
swift narwhal
#

for calls_panicking there's only one return path

quartz eagle
#

bb2 has the extra logic to drop the string

stiff acorn
#

this seems like it's evolving into a version of algebraic effects, where panicking is an effect and you can install effect handlers more locally than a process-wide panic handler

fathom pond
quartz eagle
stiff acorn
#

you might have luck nailing down the specifics of what you actually want by looking at work done on #![feature(effects)]

#

ah ok

quartz eagle
#

this is actually orthogonal to that since it's about wrapping existing code

stiff acorn
fathom pond
#

then in that case does 'a (the block label itself) have a lifetime?

#

we have to prevent code paths where we jump to already-past code

#

the block label must strictly be in the parent scopes

#

that aligns well with lifetimes

stiff acorn
#

oh hm, that might work - treat block labels as lifetimes that can go out of scope like variables

swift narwhal
#

does f borrow from... 'a? or the underscore?

stiff acorn
#

f would borrow the label 'a, which coincidentally could be treated as a lifetime 'a without changes - iirc lifetimes and block labels aren't allowed to shadow one another

fathom pond
#

since we need our breaks to occur in the 'a lifetime, we borrow from 'a and not the underscore

barren thicket
#

if you want you can add 'a or infer types

#

and no, blocking continue doesn't fix this

unique pumice
#

Surely, this is not valid code

fathom pond
#

you still can't use this code because || continue 'a has lifetime 'a which doesn't live long enough to be pushed into the vec

#

what you're written is the equivalent of this in closures instead of &str

#

?play rs fn main() { let mut v: Vec<Box<dyn FnOnce()>> = Vec::default(); for i in 0..10 { let short = "hey"; v.push(Box::new(|| println!("{}", short))); } for v in v.into_iter(){ v() } }

mystic spireBOT
#
warning: unused variable: `i`
 --> src/main.rs:3:9
  |
3 |     for i in 0..10 {
  |         ^ help: if this is intentional, prefix it with an underscore: `_i`
  |
  = note: `#[warn(unused_variables)]` on by default

error[E0597]: `short` does not live long enough
 --> src/main.rs:5:41
  |
4 |       let short = "hey";
  |           ----- binding `short` declared here
5 |       v.push(Box::new(|| println!("{}", short)));
  |       -               --                ^^^^^ borrowed value does not live long enough
  |       |               |
  |       |               value captured here
  |       borrow later used here
6 |     }
  |     - `short` dropped here while still borrowed

For more information about this error, try `rustc --explain E0597`.```
fathom pond
#

^

#

you'd get this exact error with the block

#

in other words, 'a is valid only inside the block labeled 'a:

'a: {
    // valid here
    break 'a;
};
// invalid here
break 'a;
#

and you can't really move them since it's semantically "borrowing" from the block

#

if this feature ever gets implemented in real rust as an unstable feature or something, we will have cut down the gray area's bottom from "os level" to "task level" or up, essentially reducing the impact of a panic where needed

#

Recap

In order to reduce the impact of std::panic!()s or calls to the #[panic_handler], i proposed a solution that on demand, opt in, reduces the scope of termination from program-level-termination to task-level-termination, using block labels that align really well with the rust's lifetime and borrow checking system.

The solution proposes that the calls to std::panic or #[panic_handler] in any function call be modifiable by the caller on-demand by use of block labels. This operation is completely transparent to the function, and the function has to change absolutely nothing about their implementation to still be valid. This is not a breaking change.

All panics in Rust eventually have to call either the std panic handler or the custom #[panic_handler] in #![no_std] contexts. This means if we can control std or calls to #[panic_handler] (which is a lang item and defined in every Rust crate one way or another), we control the behavior of all panics.

All the external function cares about is that something went wrong with itself, and it has to terminate some higher level task associated with said function. The function calls panic!() (which is defined by std and core), and expects the call to never return back again. And we will do exactly that, just in a different way: by jumping to a block label defined in a higher scope.

In order to achieve this, I currently came up with 3 methods, ordered by ascending ergonomicness:

  • Function items (fn some_fn()) can take block label generics, which it can use to jump to said block and never return back to itself again. This one is the least ergonomic, because it changes the function signature on use, and doesn't override std::panic!(). This one is really intrusive
  • Custom panic handlers by closures impl FnOnce(&core::panic::PanicInfo) -> ! that replace what panic!() expands to. This is pretty straightforward, but requires that closures can capture block labesl from the environment.
#
  • New syntax, keyword or lang item that lets us override the panic!() macro (and by extension the #[panic_handler]) in a limited context (the function call) that breaks to the specified block label. This is the least intrusive, as it is completely transparent to the function, the function still calls panic!() wherever it wants to.

There could probably be more methods to achieve this, and I'd appreciate if you shared your thoughts with me.

The implementation of this feature in the rustc compiler will not be very straightforward, and there are 2 reasons for that:

  • MIR already has the ability to communicate multiple exit points from a function, which we can utilize to encode these local panic breaks, although we don't know how to inform the codegen of the change.
  • When we're generating the codegen for our function foo() which overrides the panic!() of an external call bar(), we need to override the codegen of bar() to not terminate the system but rather produce a jump/unwind of sorts. When we're codegenning foo(), bar has likely generated code before us, and without bar() we can't codegen our function totally. Therefore, this may lead to backtracking in the codegen department

These are everything I have thought up about this feature up until now, and there may be inconsistencies or design holes where we need to come up with new solutions, please do share your opinions and ideas too so we can come up with a complete solution

#

// END Recap

fathom junco
#

i'm very interested in the idea of at the very least being able to identify functions were panics can occur, so critical code will never crash.
I'm happy to put work in if there's a centralised place for it.

worldly scarab
#

maybe something like unsafe fn just with nopanic fn?

#

would also offer some help

fathom junco
#

where you can only use other functions marked with nopanic inside that block?

worldly scarab
#

basicly yes

#

or you need to catch the panic somehow

fathom junco
#

i feel like that would take a really long time to gain adoption in all of the crates

worldly scarab
#

i would prefer a opt in aproach with panic fn but that isnt practically atm i assume

fathom junco
#

i don't know how rust would cope with a breaking change like that except through an edition

worldly scarab
#

there could be a clippy warning maybe that warns when a no panic fn is not labeld as such

#

i expect that it would be only really implementable with a new edition

#

at least the transition to that would be fairly easy as rust could traverse fn calls and check if it reaches the panic handler and then add panic to all upper functions

#

and in the old edition the panic fn could be optional with just a clippy info/warn

fathom junco
worldly scarab
#

this aparently works on the linker level

#

and given the list of caveats sounds suboptimal as a solution

#

also it doesnt encode it in the function signature making it harder to see if a function can panic or not

#

imo

#

after thinking about it if we add panic keyword like unsafe to the function signature and set unmarked functions panic behavior to may panic and add a clippy check for the keyword usage (basicly opt in), it could be added without breaking changes and later the internal default assumption is set to no panic on a edition change

#

the only surface level changes would be:
new fn signature keyword panic
new clippy check for unused panic
new (temporary) clippy check for call into a panic fn from a no panic fn

#

and the panic handler would be declared as panic with allow(clippy::unused_panic)

fathom junco
#

i'm not sure if "panic" would be the best keyword as it implies that it will panic, not that it may panic

#

also alot alot more support will be needed to get something like this implemented

worldly scarab
#

i mean unsafe in the function signature is kinda the same in that regard imo
yes definitely, the required back-end changes might be significant

fathom junco
#

it depends if there is a standard method to initiate panics (i haven't done any research on this)

fathom pond
#

i don't really like the panic keyword to be honest

fathom pond
#

when i first started theorizing on this topic i had ideas of nopanic keywords and whatnot too

fathom junco
#

how can it be ensured that the state returned to is a valid state after panicking?

#

as from my understanding it seems that you want a runtime solution to panicking

worldly scarab
#

also that isnt a solution to the whole no panic topic, its a way how to catch a panic, which definitely sounds usefull but isnt on topic imho

#

you still panic and whatever operation you where doing at that moment is now undefined, which for hardware interacting code is very bad

#

maybe undefined is a bit to strong but the underlying hardware for example might actually go into a undefined/faulty state because the io sequence was unexpectedly halted

#

see the rtc and cmos on x86 platforms, if a non maskable interrupt fires while you are programming the rtc it may go into a undefined state and as its battery buffered requires user intervention to recover

#

there is a way to mask these non maskable interrupts just to safely programm the rtc

#

tl dr your idea is usefull for when you want to have multiple panic hooks that behave differently and maybe could even be set at runtime (maybe per thread/worker panic function even?) but its not the solution to no panic

oblique plaza
#

mostly just regurgitating a previous discussion i've had on this topic (and sorry if it's already been discussed in this thread), but the thing about modelling panics as an (opt-out) effect (which is essentially what a nopanic keyword would be, similar to the const keyword) is that there are at least two kinds of panics:

  • panics due to user error (eg indexing a slice out of bounds)
  • assertions to detect bugs in library code (should never occur if the library code is correct, are generally truly unrecoverable if they do happen, and skipping them may lead to undetected ub)

i'd personally only be interested in guaranteeing the absence of the first category of panics in my code, and i certainly would not want library authors to avoid using assertions in their code simply to make their code nopanic compatible. so i think that for something like this to work, we'd probably want some kind of escape hatch that allows for panics even in nopanic functions for the sake of asserting invariants, something like an invariant {} block similar to unsafe {} blocks. that would still allow for easy auditing of nopanic code by simply searching for any such blocks to verify that they are indeed only used for internal assertions

worldly scarab
#

if we change the definition of asserts from beeing just a renamed panic!() to something else then we wouldnt need a escape hatch

oblique plaza
#

people use assert!() both for testing internal invariants and for detecting invalid user input, imo those should be treated separately

#

but another set of more explicitly named assert macros could do the trick also

worldly scarab
#

on the other hand, code that needs assertions should never be declared as nopanic

fathom junco
#

true

#

if invariants cannot be guaranteed at compile time then a panic seems to be the only option if the verification fails at runtime

#

i agree with the escape hatch though, as humans can verify code that the compiler can't

oblique plaza
#

i don't think i agree. there are eg data structures that, if implemented correctly, you wouldn't expect to panic and which would be perfectly sensible to use in panic-free code, but where you'd still want to have internal assertions on the off chance that there is a bug somewhere

worldly scarab
#

the escape hatch would probably be using unsafe to change the panic to undefined behavior if it happens

oblique plaza
#

i don't want to have to choose between "linked list that may have unchecked ub" and "linked list that can't be used in nopanic"

fathom junco
#

i mean technically any code that interacts with IO can potentially cause errors

#

as a computer is a machine that is almost always infallible

worldly scarab
fathom junco
#

at that point almost every function will have a result return type

oblique plaza
fathom junco
#

allocating memory could fail, which is something that would have to be dealt with too often to be ergonomic

worldly scarab
#

alternatively the linked list has to be formaly verified

fathom junco
#

if the results are there to act as a buffer from undefined behaviour and panic early instead. How should that be handled?

worldly scarab
#

maybe we should split panic into panic and abort

oblique plaza
#

i mean formal verification is nice and all but i think having an escape hatch is more pragmatic. otherwise i'm afraid that library authors are just going to skip adding any assertions at all (while also not doing the formal verification).

unless we want to restrict nopanic to not allow unsafe that is... at which point i'm not sure how useful it is. couldn't even use a vec in nopanic code at that point

worldly scarab
#

like panic is your usual "i dont want to handle that code path or whatever" and abort is like ram corruption

oblique plaza
#

sure that would make sense to me

worldly scarab
#

but that means that panics would be fully catchable and abort would literally be the process shitting itself

#

and assert split into input_assert and assert?

fathom junco
#

which is a topic that people would need to be educated on

worldly scarab
#

maybe make abort!() unsafe?

fathom junco
#

potentially a good solution

worldly scarab
#

also the more i think about it the more i come to the conclusion that the whole panic system is flawed from the start, there should have never been a way outside of results to say "i dont want to handle that code path"

fathom junco
worldly scarab
fathom junco
#

to me the potential of ub & lazieness are two completely different catogries

oblique plaza
#

(though tbf it's very rare that i actually explicitly index into a slice rather than iterating over it...)

fathom junco
#

it already returns an option?

oblique plaza
#

get() returns an option, my_vec[i] does not

quartz eagle
#

I think indexing is a bad example (as in it shouldn't have panicked)

fathom junco
#

i dislike that the latter exists

#

(if it's possible in safe rust anyway)

worldly scarab
#

the latter case should have just returned a Option<T>

#

or marked as unsafe

fathom junco
oblique plaza
#

it's not unsafe tho

#

it does bounds checking

quartz eagle
#

The example I have for "why panic should exist in some form" is replace_with

fathom junco
oblique plaza
#

yeah

oblique plaza
worldly scarab
fathom junco
quartz eagle
fathom junco
#

i assume

oblique plaza
quartz eagle
#

It requires an abort for safety

worldly scarab
oblique plaza
#

not sure i follow

worldly scarab
oblique plaza
#

the indexing operation my_vec[i] is the same as *my_vec.index(i)

#

it's implemented in the Index trait

quartz eagle
oblique plaza
#

that's different from get_unchecked and is not what i'm referring to

fathom junco
fathom junco
quartz eagle
oblique plaza
# worldly scarab or marked as unsafe

i was responding to the suggestion that standard indexing should be unsafe ^ which doesn't really work since index() actually does bounds checking unlike get_unchecked()

quartz eagle
#

Alright we can always fall back to wgpu

fathom junco
# quartz eagle true...

if it could be verified at compile time that the given function will not panic then i think that it will not have to abort.

oblique plaza
fathom junco
quartz eagle
#

wgpu will panic if the GPU driver does something bad

oblique plaza
#

though i'd still prefer some kind of split, with a nopanic opt-out that disallows panics but still allows aborts

quartz eagle
#

Since it doesn't have a way to recover from a corrupted state

fathom junco
#

that seems valid

worldly scarab
#

maybe the whole no panic thing isnt about panics as in forbidd them but rather a result of the fundamental flaw in how panic is used/implemented (like operations that could have been done without panic and that panic can unwind/is catchable instead of instant death)

#

and now we have to fix that or at least put a bandaid on it

fathom junco
#

i overall agree with that

oblique plaza
#

i mean as i said it's an ergonomic tradeoff, and there are certainly cases where you wouldn't want a panic to abort the entire process unless truly unrecoverable (eg long-running webservers)

quartz eagle
#

Really it should be called a trap

fathom junco
#

choosing to panic should either be a last resort or when fundamental assumptions are no longer valid

quartz eagle
fathom junco
#

true

fathom junco
oblique plaza
#

i don't think you'll have much luck convincing people to entirely remove standard indexing (as opposed to providing the ability to choose between panic-on-oob and None-on-oob)

fathom junco
quartz eagle
#

Option should have been the default

worldly scarab
quartz eagle
#

You should have had to ask for the killing the process version

oblique plaza
fathom junco
fathom junco
quartz eagle
#

Indexing

fathom junco
#

so true

#

but i think that's a different topic to no panic rust

worldly scarab
oblique plaza
#

i mean i'm not opposed to having Option be the default, but you'd still need to provide unwrap() which in this situation would fall into the "panic due to user error" category rather than the "panic due to a violated internal variant" category

quartz eagle
#

I think the best course of action would be to have a mode that disables unwinding and removes the panic macros and instead adds a trap!() (Which invokes the panic handler trap)

#

Since we now have the panic macros which assume unwinding is probably available

#

This way that code fails to compile under this mode

worldly scarab
fathom junco
#

honestly the closest to no panic that we might get is the const functions

oblique plaza
#

const functions can panic

fathom junco
#

as that seems to be halfway there (imo)

#

isn't that's compile time?

oblique plaza
#

depends on if you call them at compile time or not

worldly scarab
#

maybe something like trap!() which would be designed to be fully and easily catchable and abort!() which insta kills the process

fathom junco
quartz eagle
#

As said abort is not always possible

worldly scarab
quartz eagle
#

On embedded abort would just be getting stuck in a loop (aka a trap)

fathom junco
quartz eagle
oblique plaza
worldly scarab
#

the abort handler would have to be supplied by the programmer for no std targets

quartz eagle
fathom junco
oblique plaza
#

i mean that's what i'm talking about re opt-out effects, that's what const is

#

hopefully the effect system that they're working on for const traits is general enough that it can be repurposed for things like nopanic

quartz eagle
#

From what I understand that project is kind of dead

oblique plaza
#

wait what seriously?

worldly scarab
#

maybe we could reuse panic!() as in making it fully catchable and stating it has to be used when a condition is reached from which the current thread cant recover as in it doesnt make sense not state corruption and add abort!() which kills the process and a unhandled panic will propagate until it reaches the entry point which could just exit the process

oblique plaza
#

i thought i saw some movement on const traits just earlier this year

quartz eagle
worldly scarab
#

will be back after eating

fathom junco
quartz eagle
fathom junco
quartz eagle
#

Which I'm not sure is doable

fathom junco
#

i'm in general against lots of different "types" of panics, as this starts to sound more like exception handling in other languages

oblique plaza
quartz eagle
oblique plaza
#

god the comment section on that rfc is a doozy, i feel like i'd need to spend a day to trawl through that entire discussion + the linked zulip thread

fathom junco
#

is there any rfc for no panic?

oblique plaza
#

not to my knowledge

fathom junco
#

might help drum up some new ideas

worldly scarab
#

From my Point of view there are two possible Solutions:

  1. Nuke the current Panic System. Mandate that safe code can never Panic, this means that all implicit Panics such as indexing a Array/slice have to be removed (for example indexing a slice gives you a Option<T>). The only Panic Sources would be assert!() and similar. Its UB to even try to catch stack unwinding caused by a Panic. Cargo/Rustc should get the feature to list all panic sites across the entire dependency graph. A panic indicates that UB has already happend and all safetly gurrantees do not apply anymore (including that the assert might never run as the ub that it checks for prevent it from running in the first place). Maybe add some macros as syntactic sugar for user/function input validation to replace the usage of the old panic!().

  2. Split old panic into panic and abort, abort is basicly defined in 1. and panic could happen in safe code and would be fully catchable (given that the target platform supports the required features). Functions that can panic have to have panic fn signature.

oblique plaza
#

i mean i can sympathise with (1) and in principle i agree, but i don't think it's very realistic. in particular there are long-running applications like web backends that (a) have a natural unwind boundary (the endpoint handler), (b) you absolutely would not want to hard abort on anything that's not 100% unrecoverable, and (c) would be very annoying to write if you had to require basically every single function to return a result/option just because some deeply nested function happens to index a list or something

#

having an unwinding unwrap() i think is a necessary escape hatch for certain classes of applications

fathom pond
# worldly scarab also that isnt a solution to the whole no panic topic, its a way how to catch a ...

catching panics by overriding the panic hook in a scope will catch all valid ways to panic and thus provide no panic semantincs on demand.

lets say you're working with hardware and you have to panic. so what? you just terminated your program and left the os to deal with all the messy state the hardware is in. even though we have a way to override the std panic, you can still exit the program the normal ways through core::intrinsics::abort or std::process:exit.

oblique plaza
#

though i would probably not be opposed to having unwrap()/expect() be the only way of unwinding, to encourage using option/result as the default way of handling errors, so it's up to the caller to decide whether to unwind

fathom pond
#

we're not forcing any semantincs to anyone, neither are we changing the entire language by implementing this. it's just a way to provide even more control to the programmer wherever they demand

#

its literally converting panics to control flow without using catch unwind

#

we can already have the same exact semantics on today's rust just by inlining some function bodies up until the depth we've encountered all reachable panics and replace calls to panic! with break 'label None;s

worldly scarab
oblique plaza
#

if we had a time machine, sure

#

i don't particularly care about the exact naming. if i were designing a new rust today, then something like that would probably make sense. for something to be integrated into rust as it already exists though, i'm still leaning towards a nopanic fn opt-out similar to const fn, since that wouldn't require changing any existing code, and allows you to gradually make your code nopanic

fathom pond
stiff acorn
stiff acorn
#

iirc voxell started this thread talking about how a panic shouldn't abort the code completely. the example was an os kernel, where a panic in one part of the code (a driver, say) shouldn't take down the whole kernel, just that driver. (voxell please correct this if it's wrong)

so, anything that promotes "panic" to "unconditional, uncatchable abort" is the exact opposite of what this thread was about

stiff acorn
# worldly scarab From my Point of view there are two possible Solutions: 1. Nuke the current Pan...

Functions that can panic have to have panic fn signature.
changing fn to panic fn is presumably a breaking change. in that case having fn (not panic fn) is guaranteeing "never in a million years will this function have any internal invariants that aren't expressed in its signature, nor will any of the functions it ever calls in any future compatible version of this library". any library with stability commitments - the stdlib and tokio, for instance - would likely mark everything but the most trivial functions as panic fn, because doing otherwise creates a massive backwards compatibility hazard. and now we're back to square one but with more typing

fathom junco
#

From what i can gather the most commonly agreed upon thing in this thread is:
If a function can/may panic, the caller should be able to know that without examining the function body.

#

Whether this information is gathered through hard-coded keywords, marcros, or even LSP analysis (i don't know if this could be possible) is still up for debate

#

The main issue i have with panicking currently is that as a function caller i do not know whether i need to attempt to catch a panic in critical code or not.

pearl saffron
#

If a panic annotation means "there's a codepath in this callstack that calls panic" and every caller requires the annotation, i think it would be a mess

#

But I'm all for a lint similar to the "Panics" section in the documentation

fathom junco
#

a lint would be a nice staring point

brave sequoia
# fathom junco From what i can gather the most commonly agreed upon thing in this thread is: If...

from reading the thread it seems its less about whether or not a function can panic but more so if a function can panic in a way I (the implementor of the library) can recover from. So from what I can see i think splitting panics into 2 levels like with the (abort and panic suggested before) with one representing misuse or wrong local state and the other representing a monumental fuckup (ideally this should have a modifier like unsafe to not encourage usage unless you know what you are doing). You could then expose the first kind of "panic" via whatever method and keep the other for the intended use of panic.

#

then the "recoverable" panics should be the type that is exposed as part of a functions type signature

brave sequoia
#

on further thought I feel like some of the problems i have with panic is how readily the standard library uses it. In my eyes panicking should be a nuclear option on par with unsafe but the standard library is littered with panics (ie unwrap, array indexing). Unwrap especially seems like it should probably be something implemented as a third party crate rather than the standard library as it encourages people to reach for that first

fathom junco
#

i agree