The approach at the end of declaring invariants to the compiler so the compiler can eliminate panics seems accidentally genius. You can now add the same invariants as panicking asserts at the end of each function, and the compiler will prove to you that your functions are upholding the invariants. And of course you can add more panicking asserts to show other claims to be true, all tested at compile time. You've basically built a little proof system.
Sure, Rust is hardly the first language to include something like that and adoption of such systems tends to be ... spotty. But if it was reliable enough and had a better interface (that preferably allowed the rest of your program to sill have panics) this might be very useful for writing correct software.
Groxx 22 hours ago [-]
>and the compiler will prove to you that your functions are upholding the invariants
From the article and only vague background Rust knowledge, I'm under the impression that the opposite is true: the compiler does not prove that. Hence why it's "assert_unchecked" - you are informing the compiler that you know more than it does.
You do get panics during debug, which is great for checking your assumptions, but that relies on you having adequate tests.
tga_d 7 hours ago [-]
That's my understanding as well. The thing I was wondering as I read it: how difficult would it be for someone to make an extension or fork of Rust that allows annotating sufficient type information to prove these kinds of invariants, like F*?
saghm 1 days ago [-]
This isn't quite the same, but it reminds me of something a bit less clever (and a lot less powerful) I came up with a little while back when writing some code to handle a binary format that used a lot of 32-bit integers that I needed to use for math on indexes in vectors. I was fairly confident that the code would never need to run on 16-bit platforms, but converting from a 32-bit integer to a usize in Rust technically is considered fallible due to the fact that you can't necessarily assume that a usize is more 16 bits, and frustrating `usize` only implements `TryFrom<u32>` rather than conditionally implementing `From<u32>` on 32-bit and 64-bit platforms. I wanted to avoid having to do any casting that could silently get messed up if I happened to switch any of the integer types I used later, but I also was irrationally upset at the idea of having to check at runtime for something that should be obvious at compile time. The solution I came up with was putting a static assertion that the target pointer width was either 32 or 64 bits inside the error-handling path, followed by marking the code path as `unreachable!()` that would never get executed (because either the error handling path wouldn't be taken, or the static assertion would stop the code from having been compiled in the first place. Even though this wasn't meaningfully different from just conditionally compiling to make sure the platform was suitable and then putting `unreachable!()` unconditionally in the error handling path, having the compile-time insertion locally in the spot where the error was being handled felt like I magically turned the runtime error into a compile-time one; it was quite literally possible to write it as a function that could be dropped into any codebase without having to make any other changes to ensure it was used safely.
lilyball 1 days ago [-]
What about just doing something like
#[cfg(any(target_pointer_width = "32", target_pointer_width = "64"))]
#[inline(always)]
const fn usize_to_u32(x: usize) -> u32 {
x as u32
}
and this way you can just call this function and you'll get a compile-time error (no such function) if you're on a 16-bit platform.
saghm 24 hours ago [-]
Even though I can visually verify that it's safe in this context, I really don't like casting integers as a rule when there's a reasonable alternative. The solution I came up with is pretty much equally readable in my opinion but has the distinction of not having code that might in other contexts look like it could silently have issues (compared to an `unreachable!()` macro which might also look sketchy but certainly wouldn't be quiet if it accidentally was used in the wrong spot). I also prefer having a compiler error explaining the invariant that's expected rather than a missing function (which could just as easily be due to a typo or something). You could put a `compile_error!()` invocation to conditionally compile when the pointer width isn't at least 32, but I'd argue that tilts the balance even more in favor of the solution I came up with; having a single item instead of two defined is more readable in my opinion.
This wasn't a concern for me but I could also imagine some sort of linting being used to ensure that potentially lossy casts aren't done, and while it presumably could be manually suppressed, that also would just add to the noisiness.
lilyball 5 hours ago [-]
Oops, I flipped the conversion here, that should have been `u32_to_usize()`.
haberman 1 days ago [-]
While I would love this to be true, I'm not sure that this design can statically prove anything. For an assert to fail, you would have to actually execute a code sequence that causes the invariant to be violated. I don't see how the compiler could prove at compile time that the invariants are upheld.
wongarsu 1 days ago [-]
An assert is just a fancy `if condition { panic(message) }`. If the optimizer can show that condition is always false, the panic is declared as dead code and eliminated. The post uses that to get the compiler to remove all panics to reduce the binary size. But you can also just check if panic code was generated (or associated code linked in), and if it was then the optimizer wasn't able to show that your assert can't happen.
Of course this doesn't prove that the assert will happen. You would have to execute the code for that. But you can treat the fact that the optimizer couldn't eliminate your assert as failure, showing that either your code violates the assert, your preconditions in combination with the code aren't enough to show that the assert isn't violated, or the whole thing was too complicated for the optimizer to figure out and you have to restructure some code
haberman 1 days ago [-]
Ah, I see what you are saying. Yes, if the optimizer is able to eliminate the postcondition check, I agree that it would constitute a proof that the code upholds the invariant.
The big question is how much real-world code the optimizer would be capable of "solving" in this way.
I wonder if most algorithms would eventually be solvable if you keep breaking them down into smaller pieces. Or if some would have some step of irreducible complexity that the optimizer cannot figure out, now matter how much you break it down.
Yoric 14 hours ago [-]
I wonder if there is a simple way to stop compilation quickly, with a readable error message, if the optimizer isn't able to eliminate the check.
> Functions that require some amount of optimization to prove that they do not panic may no longer compile in debug mode after being marked #[no_panic].
So you’re probably going to have to protect this with a cfg_attr to only apply the no_panic in release only.
vlovich123 10 hours ago [-]
To be clear, the optimizer doesn’t uphold the post condition. It just says “the post condition is usable to elide other checks”. But if the condition itself is incorrect, the compiler will STILL elide those checks. If the compiler could prove the condition on its own it would have done so. That’s why that assert is named unchecked and is itself unsafe!
saagarjha 15 hours ago [-]
Generally you run into the halting problem and whatnot real quick.
vlovich123 10 hours ago [-]
To be clear as there’s a lot of nuance. Assert unchecked is telling the compiler the condition must always hold. The optimizer and compiler don’t make any assumption about the assert. That information is then used by the compiler to optimize away checks it otherwise would have to do (eg making sure an Option is Some if you call unwrap).
If you have an assumption that gives unhelpful information, the optimizer will emit panic code. Worse, if the assumption is incorrect, then the compiler can easily miscompile the code (both in terms of UB because of an incorrectly omitted panic path AND because it can miscompile surprising deductions you didn’t think of that your assumption enables).
I would use the assume crate for this before this got standardized but very carefully in carefully profiled hotspots. Wrapping it in a safe call as in this article would have been unthinkable - the unsafe needs to live exactly where you are making the assumption, there’s no safety provided by the wrapper. Indeed I see this a lot where the safety is spuriously added at the function call boundary instead of making the safety the responsibility of the caller when your function wrapper doesn’t actually guarantee any of the safety invariants hold.
jwatte 1 days ago [-]
For systems where correctness is actually important, not just a nice-to-have (in most systems, it's nice-to-have,) we have had an increasing number of options over the years.
From tools like "spin" and "tla+" to proof assistants like Coq to full languages like Idris and Agda.
Some of the stronger-typed languages already give us some of those benefits (Haskell, OCaml) and with restricted effects (like Haskell) we can even make the compiler do much of this work without it leaking into other parts of the program if we don't want it to.
Is fundamentally unsound `check_invariant` needs to be unsafe as it doesn't actually check the invariant but tells the compiler to blindly assume they hold. Should probably also be named `assume_invariant_holds()` instead of `check_invariant()`.
vlovich123 10 hours ago [-]
I personally would have used the assume crate but I guess this got standardized more recently. They call out the safeness requirement and that it’s a sharp edge but like you I think they understate the sharpness and danger.
rtpg 1 days ago [-]
I've had an unpleasant amount of crashes with Rust software because people are way too quick to grab `panic!` as an out.
This was most shocking to me in some of the Rust code Mozilla had integrated into Firefox (the CSS styling code). There was some font cache shenanigans that was causing their font loading to work only semi-consistently, and that would outright crash this subsystem, and tofu-ify CJK text entirely as a result.
And the underlying panic was totally recoverable in theory if you looked at the call stack! Just people had decided to not Result-ify a bunch of falliable code.
ninetyninenine 1 days ago [-]
Sometimes the program is in an invalid state. You don't want to keep running the program. Better to fail spectacularly and clearly then to fail silently and try to hobble along.
rtpg 21 hours ago [-]
I understand this belief abstractly. In the cases I was hitting, there would have been easy recovery mechanisms possible (that would have been wanted because there are many ways for the system to hit the error!), but due to the lowest level "key lookup" step just blowing up rather than Result (or Option)-ing their lookup, not only would the patch have been messy, but it would have required me to make many decisions in "unrelated" code in the meanwhile.
I understand your point in general, I just find that if you're writing a program that is running on unconstrained environments, not panic'ing (or at least not doing it so bluntly at a low level) can at the very least help with debugging.
At least have the courtesy to put the panic at a higher level to provide context beyond "key not found!"!
ratorx 20 hours ago [-]
Without knowing the exact situation, if you follow the guidelines in this article, this is a library bug (documentation or actual code).
Either the library should have enforced the invariant of the key existing (and returned an equivalent error, or handled it internally), or documented the preconditions at a higher level function that you could see.
jwatte 1 days ago [-]
The thing with functional programming (specifically, immutable data,) is that as long as the invalid state is immutable, you can just back up to some previous caller, and they can figure out whether to deal with it or whether to reject up the its previous caller.
This is why Result (or Maybe, or runExceptT, and so on in other languages) is a perfectly safe way of handling unexpected or invalid data. As long as you enforce your invariants in pure code (code without side effects) then failure is safe.
This is also why effects should ideally be restricted and traceable by the compiler, which, unfortunately, Rust, ML, and that chain of the evolution tree didn't quite stretch to encompass.
duped 1 days ago [-]
Say a function has some return type Result<T, E>. If our only error handling mechanism is Err(e) then were restricted to E representing the set of errors due to invalid arguments and state, and the set of errors due to the program itself being implemented incorrectly.
In a good software architecture (imo) panics and other hard failure mechanisms are there for splitting E into E1 and E2, where E1 is the set of errors that can happen due to the caller screwing up and E2 being the set of errors that the caller screwed up. The caller shouldn't have to reason about the callee possibly being incorrect!
Functional programming doesn't really come into the discussion here - oftentimes this crops up in imperative or object oriented code where function signatures are lossy because code relies on side effects or state that the type system can't/won't capture (for example, a database or file persisted somewhere). Thats where you'll drop an assert or panic - not as a routine part of error handling.
turboponyy 19 hours ago [-]
You shouldn't pass invalid values to a function. If a function can return some sensible value for some input, then the input is not invalid - even if the return type is an error by name.
Ideally, you can constrain the set of inputs to only valid ones by leveraging types. But if that's not possible and a truly invalid input is passed, then you should panic. At least that's the mental model that Rust is going with.
You do lose out on the ability to "catch" programming errors in subcomponents of your program. For example, it's extremely useful to catch exceptions related to programming errors for called code in response to a web request, and return a 500 in those cases. One could imagine a "try" "catch" for panics.
The thing is, it takes a lot of discipline by authors to not riddle their code with panics/exceptions when the language provides a try/catch mechanism (see C# and Java), even when a sensible error as value could be returned. So Rust opts to not introduce the footgun and extra complexity, at the expense of ungraceful handling of programming errors.
gardaani 17 hours ago [-]
> Ideally, you can constrain the set of inputs to only valid ones by leveraging types. But if that's not possible and a truly invalid input is passed, then you should panic.
But how can the caller know what is "a truly invalid input"? The article has an example: "we unfortunately cannot rely on panic annotations in API documentation to determine a priori whether some Rust code is no-panic or not."
It means that calling a function is like a lottery: some input values may panic and some may not panic. The only way to ensure that it doesn't panic is to test it with all possible input values, but that is impossible for complex functions.
It would be better to always return an error and let the caller decide how to handle it. Many Rust libraries have a policy that if the library panics, then it is a bug in the library. It's sad that the Rust standard library doesn't take the same approach. For println!(), it would mean returning an error instead of panicking.
ninetyninenine 22 hours ago [-]
The program can detect invalid state, but your intention was to never get to that state in the first place. The fact that the program arrived there is a Logic error in your program. No amount of runtime shenanigans can repair it because the error exists without your knowledge of where it came from. You just know it's invalid state and you made a mistake in your code.
The best way to handle this is to crash the program. If you need constant uptime, then restart the program. If you absolutely need to keep things running then, yeah try to recover then. The last option isn't as bad for something like an http server where one request caused it to error and you just handle that error and keep the other threads running.
But for something like a 3D video game. If you arrive at erroneous state, man. Don't try to keep that thing going. Kill it now.
mrkeen 18 hours ago [-]
> The fact that the program arrived there is a Logic error in your program.
No, your program correctly determined that user input was invalid.
Or your parser backtracked from parsing a Bool and decided to try to parse an Int instead.
ninetyninenine 14 hours ago [-]
That’s not invalid state. Your program correctly determined input is invalid.
Say user input is a number and can never exceed 5. If the user input exceeds 5 your program should handle that gracefully. This is not invalid state. It is handling invalid input while remaining in valid state.
Let say it does exceed 5 and You forget to check that it should never exceeds 5 and this leads to a division by zero further down your program. You never intended for this to happen and you don’t k ow why this happened. This is invalid state.
Now either this division by zero leads to an error value or it can throw an exception. It doesn’t matter. Do you know how to recover? You don’t even know where the bug is. A lot of times your don’t even know what to do. This error can bubble up all the way to main and now what do you do with it?
You crash the program. Or you can put in a fail safe before the error bubbles up to main and do something else but now (if your program retains and mutates that state) has invalid values in it and a known bug as well.
Imagine that input number represented enumeration values for movement of some robot. You now have one movement that doesn’t exist or was supposed to be something else. Thus if you keep your program running the robot ends up in an unexpected and erroneous place. That’s why I’m saying a program should crash. It should not be allowed to continue running with invalid state.
Like imagine if this was a mission critical auto pilot and you detect negative 45 meters for altitude. Bro crash and restart. Don’t try to keep that program running by doing something crazy in attempt to make the altitude positive and correct. Reset it and hope it never goes to the invalid state again.
mrkeen 11 hours ago [-]
> You crash the program.
No thank you.
> Or you can put in a fail safe before the error bubbles up to main and do something else
In other words,
>> your parser backtracked from parsing a Bool and decided to try to parse an Int instead
> but now (if your program retains and mutates that state) has invalid values in it and a known bug as well.
Unless,
>>> The thing with functional programming (specifically, immutable data,) is that as long as the invalid state is immutable, you can just back up to some previous caller, and they can figure out whether to deal with it or whether to reject up the its previous caller.
ninetyninenine 10 hours ago [-]
Your parser backtracking from a bool when it tried to parse an int is VALID behavior. Your parser tries to parse text into several possible types at runtime. Backtracking is STILL valid state. You’re not thinking clearly.
Look at my auto pilot example. You have a bug in your program you don’t know about. Altitude reads -9999 meters your heading and direction is reading at the speed of light. Your program sees these values and recognizes invalid state. There is a BUG in your program. Your program WAS never designed to go here.
You want to try to recover your autopilot program? How the fuck are you gonna do that? You don’t even know where the bug is. Does your recovery routine involve patching and debugging? Or are you gonna let your autopilot keep operating the plane with those nonsense values? That autopilot might hit the wind breaks to try to slow down the plane and end up crashing the thing.
You don’t recover this. You restart the program and pray it doesn’t hit that state again then when you are on the ground you debug it.
>>> The thing with functional programming (specifically, immutable data,) is that as long as the invalid state is immutable, you can just back up to some previous caller, and they can figure out whether to deal with it or whether to reject up the its previous caller.
This makes no fucking sense. Where the hell will you back up to? Your error came from somewhere but you don’t know where. Return an error value in the child function the parent function receives the error and returns an error itself and this keeps going until you bubble up to the main function because you don’t know where that error came from.
Now you have an error in the main function. Wtf are you gonna do? How do you handle an error in the main function that you have no idea where it came from? Here’s an idea. You have the main function restart the program loop. See a similarity here? It’s called crashing and restarting the program. Same effect!
This isn’t a functional programming versus non functional thing. It’s a concept that you’re not seeing.
petee 12 hours ago [-]
I know this is not your point for the last paragraph, but have you read about the C-130 complete navigation system failure while trying land below sea level at the Dead Sea? :)
> This is why Result (or Maybe, or runExceptT, and so on in other languages) is a perfectly safe way of handling unexpected or invalid data.
They are great for handling expected errors that make sense to handle explicitly.
If you try to wrap up any possible error that could ever happen in them you will generate horrendous code, always having to unwrap things, everything is a Maybe. No thanks.
I know it is tempting to think "I will write the perfect program and handle all possible errors and it will never crash" but that just results in overly complex code that ends up having more bugs and makes debugging harder. Let it crash. At the point where the error happened, don't just kick the bucket down the road. Just log the the problem and call it a day. Exceptions are an amazing tool to have for things that are.. exceptions.
dralley 1 days ago [-]
At least sources of panic! are easily greppable. Cutting corners on error handling is usually pretty obvious
haberman 1 days ago [-]
I don't think grepping for panics is practical, unless you are trying to depend on exclusively no-panic libraries.
Even if you are no_std, core has tons of APIs like unwrap(), index slicing, etc. that can panic if you violate the preconditions. It's not practical to grep for all of them.
wongarsu 1 days ago [-]
There is panic-analyzer [1] that searches for code that needlessly panics. You can also use the no-panic macro [2] to turn possible panics in a specific function (including main) into a compile error
Panic-analyzer looks like it is based on heuristics, searching for known-panicing APIs. I tried it on a workspace that uses io::stdout() and it did not flag this as potentially panicing.
No-panic looks nifty: it appears to be reliable, which is great. I wish there was an easy way to automatically apply this annotation to every single function in a given file or crate.
GolDDranks 23 hours ago [-]
I think the article is wrong in that std::io::stdout would be panicking, or that "the panic is reachable somehow". It's just the optimizer doesn't see it doesn't panic.
But it only panicks if it is being used in a wrong way, which it isn't. The usage is contained within the implementation of std::io::stdout, so it's an implementation detail.
It's a shame that there are no better ways to eliminate panics in case they are impossible to trigger. The article shows some tricks, but I think the language is missing still some expressability around this, and the stdlib should also thrive harder to actually get rid of hard-to-optimize links to panic runtime in case of APIs that don't actually panic.
PoignardAzur 14 hours ago [-]
If the only problem was OnceLock, the simplest solution would be to replace it with a const initializer.
The main reason getting panic-free stdout is hard, is that the STDOUT initializers both call allocating constructors, and LineWriter doesn't have non-allocating constructors.
Solving this elegantly seems hard.
rtpg 21 hours ago [-]
It is interesting to consider how `panic!` serves as some documentation of explicitly giving up. Easy to see in a pull request. And having the string packed alongside it is nice.
Still miffed, but we'll get there.
sophacles 12 hours ago [-]
There are panics that aren't greppable that way. For instance `some_array[past_bounds]` causes a panic.
pluto_modadic 1 days ago [-]
I mean... rust modules aren't typically in your CWD, no? they're not in some node_modules that you can grep, but in a cargo folder with /all of the libraries you ever used/, not just the ones you have for this one project.
gpm 1 days ago [-]
Putting them all in the project root takes just a single `cargo vendor` command.
But I would assume that for mozilla their entire CSS subsystem is pulled in as a git (hg?) submodule or something anyways.
est31 1 days ago [-]
not sure how the CSS subsystem is included (I remember it is shared with Servo?), but in general all of the Rust dependencies in mozilla-central is vendored, so nothing is in your ~/.cargo directory.
eru 1 days ago [-]
For what it's worth, eg vscode can jump to definition even when your code is in a different crate that's not in your repository.
est31 1 days ago [-]
If you run cargo vendor, they end up in a neat directory.
duped 1 days ago [-]
While sure, more things could be baked as results, most of the time when you see a panic that's not the case. It's a violation of the callee's invariants that the caller fucked up.
Essentially an error means that the caller failed in a way that's expected. A panic means the caller broke some contract that wasn't expressed in the arguments.
A good example of this is array indexing. If you're using it you're saying that the caller (whoever is indexing into the array) has already agreed not to access out of bounds. But we still have to double check if that's the case.
And if you were to say that hey, that implies that the checks and branches should just be elided - you can! But not in safe rust, because safe code can't invoke undefined behavior.
rtpg 21 hours ago [-]
I understand the value of panic when your invariants really are no longer holding. What I have seen is many cases of "oh a micro-invariant I kind of half believe to be true isn't being held, and so I will panic".
Obviously context-free this is very hand wave-y, but would you want Firefox to crash every time a website prematurely closes its connection to your browser for whatever reason? No, right? You would want Firefox to fail gracefully. That is what I wanted.
1 days ago [-]
dathinab 13 hours ago [-]
> Unrecoverable
panics are very much designed to be recoverable at some well defined boundaries (e.g. the request handler of a web server, a thread in a thread pool etc.)
this is where most of it's overhead comes from
you can use panic=abort setting to abort on panics and there is a funny (but unpractical) hack with which somewhat can make sure that no not-dead-code-eliminated code path can hit a panic (you link the panic->abort handler to a invalid symbol)
nicce 13 hours ago [-]
I would say that you are using them incorrectly if you assume them as recoverable. You should make everything you can so that they never happen.
However, since it is still possible to have them in a place where the exiting the process is not okay, it was beneficial to add a way to recover from them. It does not mean that they are designed to be recoverable.
> this is where most of it's overhead comes from
Overhead comes from the cleaning process. If you don't clean properly, you might leak information or allocate more resources than you should.
dathinab 12 hours ago [-]
> I would say that you are using them incorrectly if you assume them as recoverable.
no it's them being recoverable at well defined boundaries is a _fundamental_ design aspect of rust
> Overhead comes from the cleaning process. If you don't clean properly, you might leak information or allocate more resources than you should.
and that same cleanup process makes it recoverable
nicce 11 hours ago [-]
Even the book uses the word "unrecoverable". The wording is communication. The intention is not to recover them, while it could be possible.
There is a social norm to treat panics as unrecoverable (in most cases — some do use panics to perform cancellation in non-async code).
andyferris 1 days ago [-]
This seems to obviate a lot of Rust's advantages (like a good std library). I wonder what it would take to write a nopanic-std library?
Panics really seem bad for composability. And relying on the optimzer here seems like a fragile approach.
(And how is there no -nopanic compiler flag?)
gpm 1 days ago [-]
Rust doesn't want to add any proof-system that isn't 100% repeatable, reliable, and forwards compatible to the language. The borrow checker is ok, because it meets those requirements. The optimizer based "no panic" proof system is not. It will break between releases as LLVM optimizations change, and there's no way to avoid it.
Trying to enforce no-panics without a proof system helping out is just not a very practical approach to programming. Consider code like
This code is obviously correct. It never panics. There's no better way to write it. The optimizer will instantly see that and remove the panicing branch. The language itself doesn't want to be in the business of trying to see things like that.
Or consider code like
let mut count: usize = 0;
for item in some_vec {
// Do some stuff with item
if some_cond() {
count += 1;
}
}
This code never panics. Integer arithmetic contains a hidden panic path on overflow, but that can't occur here because the length of a vector is always less than usize::MAX.
Or so on.
Basically every practical language has some form of "this should never happen" root. Rust's is panics. C's is undefined behavior. Java's is exceptions.
Finally consider that this same mechanism is used for things like stack overflows, which can't be statically guaranteed to not occur short of rejecting recursion and knowledge of the runtime environment that rustc does not have.
---
Proof systems on top of rust like creusot or kani do tend to try to prove the absence of panics, because they don't have the same compunctions about not approving code today that they aren't absolutely sure they will approve tomorrow as well.
RainyDayTmrw 21 hours ago [-]
To add to this, I believe that there will always be some amount of "should never happen but I can't prove it" due to Rice's Theorem[1].
It doesn't panic within the code you typed, but it absolutely still can panic on OOM. Which is sort of the problem with "no panic"-style code in any language - you start hitting fundamental constructs that can can't be treated as infallible.
> Basically every practical language has some form of "this should never happen" root.
99% of "unrecoverable failures" like this, in pretty much every language, are because we treat memory allocation as infallible when it actually isn't. It feels like there is room in the language design space for one that treats allocation as a first-class construct, with suitable error-handling behaviour...
deschutes 10 minutes ago [-]
Assuming memory is infinite for the purposes of a program is a very reasonable assumption for the vast majority of programs. In the rare contexts where you need to deal with the allocation failure it comes at a great engineering cost.
It's not really what this is about IMV. The vast majority of unrecoverable errors are simply bugs.
A context free example many will be familiar with is a deadlock condition. The programmer's mental model of the program was incomplete or they were otherwise ignorant. You can't statically eliminate deadlocks in an arbitrary program without introducing more expensive problems. In practice programmers employ a variety of heuristics to avoid them and just fix the bugs when they are detected.
wongarsu 1 days ago [-]
The standard library is slowly adding non-panicking options. The article shows some of them (like vec.push_within_capacity()) and ignores some others (vec.get_unchecked()). There is still a lot of work to do, but it is an area where a lot of work gets done. The issue is just that a) Rust is still a fairly young language, barely a decade old counting from 1.0 release, and b) Rust is really slow and methodical in adding anything to stdlib because of how hard/impossible it is to reverse bad decisions in the stdlib.
The same article written a couple years in the future would look very different
swiftcoder 11 hours ago [-]
To be slightly pedantic, Vec::get() is the non-panicking version. Vec::get_unchecked() is just the version thereof that elides the bounds check.
hathawsh 1 days ago [-]
What I would like to see is a reliable distinction of different types of panics. In the environments where software I write is typically run, panics due to heap allocation failure are generally acceptable and rarely an indication of fragility. (By the time a heap allocation failure occurs, the computer is probably already thrashing and needs to be rebooted.) On the other hand, other kinds of panics are a bad sign. For example, I would frown on any library that panics just because it can't reach the Internet.
In other environments, like embedded or safety-critical devices, I would need a guarantee that even heap allocation failure can not cause a panic.
staunton 1 days ago [-]
This website makes by browser freeze... No idea why. Not able to read the article.
Author here -- that is surprising. What browser/OS are you on? I haven't had anyone else report this problem before.
TallonRain 1 days ago [-]
I’m seeing the same problem, the page crashes on Safari on iOS, saying a problem repeatedly occurred. Haven’t seen a webpage do that in quite a while.
faitswulff 1 days ago [-]
Yep, same experience, same platform. I guess straight to reader mode, it is.
EDIT - shockingly, reader mode also fails completely after the page reloads itself
ubj 1 days ago [-]
I'm seeing the same problem--the page crashes on mobile (Brave). On desktop it loads, but all of the code cells have a crashed page symbol in them.
flohofwoe 14 hours ago [-]
Same here: Chrome on a Google Pixel 4a. Page freezes while scrolling down and eventually oh-snaps.
IX-103 1 days ago [-]
I'm also seeing this on Android Chrome. When I opened the page on my Linux desktop, I also saw the crashes (though they only affected the godbolt iframes).
Note that on Android process separation is not usually as good, so a crashing iframe can bring down the whole page.
wbobeirne 1 days ago [-]
Same issue on Android using Brave.
lacraig2 1 days ago [-]
same on chrome android
anymouse123456 1 days ago [-]
same for me. Chrome on Pixel 8
DemetriousJones 1 days ago [-]
Same, the web view in my Android client crashed after a couple seconds.
Yeah, I think it's all those iframes. I'm seeing something weird on my Linux desktop - all the godbolt iframes crash on reload unless I have another tab with godbolt open. I didn't see anything obvious in Chrome's log.
I can't replicate the crash at all on my Linux cloud VM though. Usually the only difference there is that advertisers tend to not buy ads for clients on cloud IPs.
wavemode 1 days ago [-]
Other pages on the site work fine for me yeah. But the OP blog post is crashing my Android browser, like the other commenters have mentioned.
DemetriousJones 15 hours ago [-]
Yeah no problem on other pages
nektro 19 hours ago [-]
OP sounds like they'd be very interested in Zig to tackle this particular problem. they'd get to a very similar place and not have to fight the language or the standard library to get there.
pedromsrocha 16 hours ago [-]
This blog post is very interesting, using Rust’s compiler optimizer as a theorem prover. This makes me wonder: are there any formal specifications on the complexity of this "optimizer as theorem prover"?
Specifically, how does it handle recursion? Consider, for example, the following function, which decrements a number until it reaches zero. At each step, it asserts that the number is nonzero before recursing:
fn recursive_countdown(n: u32) -> u32 {
assert!(n > 0, "n should always be positive");
if n == 1 {
return 1;
}
recursive_countdown(n - 1)
}
Can the compiler prove that the assertion always holds and possibly optimize it away? Or does the presence of recursion introduce limitations in its ability to reason about the program?
jerf 10 hours ago [-]
> This makes me wonder: are there any formal specifications on the complexity of this "optimizer as theorem prover"?
Basically, the promise here "We formally promise not to promise anything other than the fact that optimized code should have 'broadly' the same effects and outputs as non-optimized code, and if you want to dig into exactly what 'broadly' means prepare to spend a lot of time on it". Not only are there no promises about complexity, there's no promises that it will work the same on the same code in later versions, nor that any given optimization will continue firing the same way as you add code.
You can program this way. Another semi-common example is taking something like Javascript code and carefully twiddling with it such that a particular JIT will optimize it in a particular way, or if you're some combination of insane and lucky, multiple JITs (including multiple versions) will do some critical optimization. But it's the sort of programming I try very, very hard to avoid. It is a path of pain to depend on programming like this and there better be some darned good reason to start down that path which will, yes, forever dominate that code's destiny.
saagarjha 15 hours ago [-]
Compilers can typically reason fairly well about tail recursion like this. In this case the compiler cannot remove the assertion because you could pass in 0. But if you change the assert to be a >= 0 (which is vacuously true, as the warning indicates) it will optimize the code to "return 1" and eliminate the recursive call: https://godbolt.org/z/jad3Eh9Pf
davisp 1 days ago [-]
Does anyone know if there's an obvious reason that adding a `no_panic` crate attribute wouldn't be feasible? It certainly seems like an "obvious" thing to add so I'm hesitant to take the obvious nerd snipe bait.
hathawsh 1 days ago [-]
The standard library has a significant amount of code that panics, so a `no_panic` crate attribute would currently only work for crates that don't depend on the standard library. I imagine most interesting crates depend on the standard library.
davisp 1 days ago [-]
What caught my eye in the article was the desire to have something that doesn't panic with a release profile, while allowing for panics in dev profiles. Based on other comments I think the general "allow use of std, but don't panic" seems like something that could be useful purely on the "Wait, why doesn't that exist?" reactions.
7e 1 days ago [-]
You could do it, but I would prefer guarantees on a per-call chain basis using a sanitizer. It should be quite easy to write.
davisp 1 days ago [-]
I'm no rustc expert, but from what little I know it seems like disabling panics for a crate would be an obvious first step. You make a great point though. Turning that into a compiler assertion of "this function will never panic" would also be useful.
alkonaut 17 hours ago [-]
Why worry about the code size if the code size is up to the library consumer (through their choice of panic handler)?
If the consumer worries about code size, then their application has a minimal panic handler. If the consuming application does not have a minimal panic handler, then it must not worry about code size?
Is there some context I'm missing here? Is this to be used from non-Rust applications for example?
hkwerf 16 hours ago [-]
As the author mentions, `panic!` is also not an acceptable failure mode in some applications. If you're developing safety-critical software and a process stopping is part of your risk analysis, many frameworks will ask you about the frequency of that happening. In that analysis, you may be required to set all systematic contributions to that frequency to zero. This happens, for example, if you try to control the associated risk using redundancy. If there is a code path that may panic, you may not be able to do this at all as you maybe just cannot conclude that your code does not panic systematically.
alkonaut 14 hours ago [-]
Yes that condition I understand. But that seems orthogonal to the code size issue. Having no panics in code where the stdlib is riddled with panics for exceptional situations (allocation failure, for example) seems like a situation where you would just always go with no_std?
hkwerf 13 hours ago [-]
It is orthogonal, yes. To your question, I have an example from the same domain, where it is reasonable to mix unrolling panic with code that never panics.
Typically, safety-related processes are set up in two phases. First they set up, then they indicate readiness and perform their safe operation. A robot, for example, may have some process checking the position of the robot against a virtual fence. If the probability for passing through that fence passes some limit, this requires the process to engage breaks. The fence will need to be loaded from a configuration, communication with the position sensors will need to be established, the fence will generally need to be transformed into coordinates that can be guaranteed to be checked safely, taking momentum and today's breaking performance in account, for example. The breaks itself may need to be checked. All that is fine to do in an unsafe state with panics that don't just abort but unroll and full std. Then that process indicates readiness to the higher-level robot control process.
Once that readiness has been established, the software must be restricted to a much simpler set of functions. If libraries can guarantee that they won't call panic!, that's one item off our checklist that we can still use them in that state.
virtualritz 12 hours ago [-]
This page reliably crashes Chrome on my Android phone as a well as on my Ubuntu laptop.
vollbrecht 18 hours ago [-]
Most people are using a prebuild standard library. That comes with the problem that it comes with the features it was build for. Most of the bloat around panic for example can be eliminated by just compiling the std library yourself. This is done via the `-Zbuild-std` flag.
Using this flag one than can use `panic_abort`. This will eliminate the unwinding part but would still give a "nice" printout on a panic itself. This reduces, in most cases, the mention bloat by a lot. Though nice printouts also cost binary space. For eliminating that `panic_immidiate_abort` exists.
But yeah the above is only about bloat and not the core goal to eliminate potential path's in your program, that would lead to a panic condition itself.
Also currently building the std library yourself needs a nightly compiler. There is afaik work on bringing this to a stable compiler but how exactly is still work in progress.
usefulcat 11 hours ago [-]
> not panic'ing (or at least not doing it so bluntly at a low level) can at the very least help with debugging.
Wouldn’t panicking asap make debugging easier?
14 hours ago [-]
btown 1 days ago [-]
Does Rust have something like a deep-codemodding macro that could be used to un-panic-fy an entire function etc. automatically?
Something like: Given a function, rewrite its signature to return a Result if it doesn't already, rewrite each non-Resulty return site to a Some(), add a ? to every function call, then recurse into each called function and do the same.
dccsillag 1 days ago [-]
It has `catch_unwind` [1], but that still retains the panicking runtime, so not sufficient in the context of the post.
It's also not guaranteed to catch every panic - sometimes (notably if a destructor panics during unwinding) a panic can turn into a process-abort.
LegionMammal978 1 days ago [-]
To add to that, Rust code is generally not written to be 'exception-safe' when panics occur: if a third-party function causes a panic, or if your own code panics from within a callback, then memory may be leaked, and objects in use may end up in an incorrect or unusable state.
You really want to avoid sharing mutable objects across a catch_unwind() boundary, and also avoid using it on a regular basis. Aside from memory leaks, panicking runs the thread's panic hook, which by default prints a stacktrace. You can override the panic hook to be a no-op, but then you won't see anything for actual panics.
Instead of serializing data (to disk, not the network), it would be much faster if Rust allowed us to allocate datastructures directly in an mmapped file, and allowed us to read back the data (basically patching the pointers so they become valid if the base address changed).
lilyball 1 days ago [-]
This is basically what Cap'n Proto does, and it uses offsets instead of pointers so that way the mmapped data can be used as-is.
amelius 15 hours ago [-]
Good to know, but does Rust have the machinery to make it work with standard Rust data structures?
lilyball 5 hours ago [-]
What do you mean by "standard Rust data structures"? Like protobuf and other such formats, capnp uses code generation. The tooling for Rust is at https://github.com/capnproto/capnproto-rust and the generated code gives you structs that wrap borrowed data and give you accessors rather than direct field access.
amelius 3 hours ago [-]
Well, it would be super convenient if you could use the standard collections like Vec, LinkedList, HashMap, etc. just like you would use them normally except now they are allocated inside an mmapped file.
> If we are trying to port a C library to Rust, we really do not want to introduce panics in the code, even for unusual error condition.
"We'd much rather like to make library to corrupt the memory of the rest of the application and generally make the demons fly out of the users' noses, as it does when written in C"?
I believe implementations of C stdio also can abort on program startup if somehow the pthreads' locking mechanism is broken (or if e.g. fcntl(2)/open(2) keeps returning -1), Rust is not that unique in this regard.
sneilan1 14 hours ago [-]
This website panics on my iOS version 18.1.1 in Safari. Is anyone else having issues on mobile?
7e 1 days ago [-]
It should be possible to write a sanitizer which verifies no panic behavior on a call graph, just as you can to verify no blocking, or no races.
scotty79 15 hours ago [-]
Funnily enough this website hard-crashes Android Chrome and DuckDuckGo browsers after loading.
XorNot 1 days ago [-]
This seems..absurd for a programming language with goals like Rust. Why isn't this a compiler option? Just set -nopanics and the compiler errors and flags anything which is pulling in a panic at the very least?
wongarsu 1 days ago [-]
You can set panic: abort [1] if you don't want the unwinding mechanism. You still get a nice error message on panic, which causes the compiler to link in some formatting code from the standard library. I'm not sure if you can get rid of that.
On the same page are also the options for controlling debug assertions and overflow checks, which would get rid of the "panics in debug, but not release", if that behavior bugs you
no-panic uses link-time shenanigans to prevent panics in the compiled binary, but this isn't 100% reliable (as explained in the README, just pointing this out)
surajrmal 19 hours ago [-]
After using rust for several years, I'm shocked this isn't the default. Making panics recoverable leads to them being used incorrectly.
exDM69 15 hours ago [-]
Panics also unwind the stack and run all your Drop destructors.
Setting `panic = abort` disables unwinding and this means leaking memory, not closing file descriptors, not unlocking mutexes and not rolling back database transactions on panic. It's fine for some applications to leave this to the operating system on process exit but I would argue that the default unwinding behavior is better for typical userspace applications.
ninkendo 13 hours ago [-]
All the things you describe are done automatically on program exit, even if the program is SIGKILL’ed. The kernel cleans up file descriptors, database transactions are rolled back automatically when the client disconnects (which it should be observed to be when the program exits and the kernel closes the connection as part of cleanup), and I’m not sure what you mean about mutexes, but if you mean in-memory ones, those don’t matter because the program is gone (if you mean like, file-based ones, those also should be implicitly unlocked by the kernel when the program exits, at least that’s how a good implementation is supposed to work, e.g. by writing the pid to the file or something.)
The whole of modern operating systems are already very familiar with the idea of programs not being able to exit gracefully, and there’s already a well understood category of things that happen automatically even if your program crashes ungracefully. Whole systems are designed around this (databases issuing rollbacks when the client disconnects, being a perfect example.) The best thing to do is embrace this and never, ever rely on a Drop trait being executed for correctness. Always assume you could be SIGKILLed at any time (which you always can. Someone can issue a kill -9, or you could get OOM killed, etc.)
exDM69 13 hours ago [-]
I'm well aware of this and good that the option exists to bail out with abort instead.
But there are still cases where you would like to fsync your mmaps, print out a warning message or just make sure your #[should_panic] negative tests don't trigger false positives in your tooling (like leak detectors or GPU validators) or abort the whole test run.
It's not perfect by any means but it's better than potentially corrupting your data when a trivial assert fires or making negative tests spew warnings in ci runs.
It's very easy to opt out from, and I don't consider the price of panic handlers and unwinding very expensive for most use cases.
ninkendo 11 hours ago [-]
Right, sorry for the patronizing tone, I’m sure you know all this.
But I tend to lament the overall tendency for people to write cleanup code in general for this kind of thing. It’s one of those “lies programmers believe about X” kinds of scenarios. Your program will crash, and you will hit situations where your cleanup code will not run. You could get OOM killed. The user can force quit you. Hell, the power could go out! (Or the battery could go dead, etc.)
Nobody should ever write code that is only correct if they are given the opportunity to perfectly clean up after any failure that happens.
I see this all the time: CLI apps that trap Ctrl-C and tell you you can’t quit (yes I bloody well can, kill -9 is a thing), apps which don’t bother double checking that the files they left behind on a previous invocation are actually still used (stale pid files!!!), coworkers writing gobs of cleanup code that could have been avoided by simply doing nothing, etc etc.
winstonewert 1 days ago [-]
Well, if they did that, then people could expect/demand stability with regard to what scenarios get the checks/panics optimized out. This would be a bit of a burden for the Rust maintainers. It would effectively make the optimizer part of the language specification, and that's undesireable.
dccsillag 1 days ago [-]
Yeah, I'm fairly sure that there is such a flag/toplevel attribute... and if there isn't, there should be one.
It also feels like most of the pains on avoiding panics centers around allocations which, though a bit unfortunate, makes sense; it was an intentional design choice to make allocations panic instead of return Results, because most users of the language would probably crash on allocation fails anyways and it would introduce a lot of clutter. There was some effort some while ago on having better fallible allocations, but I'm not sure what happened over there.
goodpoint 18 hours ago [-]
It's sad. If anything panics in the stdlib should be opt-in, not opt-out.
rizky05 18 hours ago [-]
[dead]
Rendered at 02:02:01 GMT+0000 (Coordinated Universal Time) with Vercel.
Sure, Rust is hardly the first language to include something like that and adoption of such systems tends to be ... spotty. But if it was reliable enough and had a better interface (that preferably allowed the rest of your program to sill have panics) this might be very useful for writing correct software.
From the article and only vague background Rust knowledge, I'm under the impression that the opposite is true: the compiler does not prove that. Hence why it's "assert_unchecked" - you are informing the compiler that you know more than it does.
You do get panics during debug, which is great for checking your assumptions, but that relies on you having adequate tests.
This wasn't a concern for me but I could also imagine some sort of linting being used to ensure that potentially lossy casts aren't done, and while it presumably could be manually suppressed, that also would just add to the noisiness.
Of course this doesn't prove that the assert will happen. You would have to execute the code for that. But you can treat the fact that the optimizer couldn't eliminate your assert as failure, showing that either your code violates the assert, your preconditions in combination with the code aren't enough to show that the assert isn't violated, or the whole thing was too complicated for the optimizer to figure out and you have to restructure some code
The big question is how much real-world code the optimizer would be capable of "solving" in this way.
I wonder if most algorithms would eventually be solvable if you keep breaking them down into smaller pieces. Or if some would have some step of irreducible complexity that the optimizer cannot figure out, now matter how much you break it down.
edit: There is https://github.com/dtolnay/no-panic
So you’re probably going to have to protect this with a cfg_attr to only apply the no_panic in release only.
If you have an assumption that gives unhelpful information, the optimizer will emit panic code. Worse, if the assumption is incorrect, then the compiler can easily miscompile the code (both in terms of UB because of an incorrectly omitted panic path AND because it can miscompile surprising deductions you didn’t think of that your assumption enables).
I would use the assume crate for this before this got standardized but very carefully in carefully profiled hotspots. Wrapping it in a safe call as in this article would have been unthinkable - the unsafe needs to live exactly where you are making the assumption, there’s no safety provided by the wrapper. Indeed I see this a lot where the safety is spuriously added at the function call boundary instead of making the safety the responsibility of the caller when your function wrapper doesn’t actually guarantee any of the safety invariants hold.
From tools like "spin" and "tla+" to proof assistants like Coq to full languages like Idris and Agda.
Some of the stronger-typed languages already give us some of those benefits (Haskell, OCaml) and with restricted effects (like Haskell) we can even make the compiler do much of this work without it leaking into other parts of the program if we don't want it to.
Is fundamentally unsound `check_invariant` needs to be unsafe as it doesn't actually check the invariant but tells the compiler to blindly assume they hold. Should probably also be named `assume_invariant_holds()` instead of `check_invariant()`.
This was most shocking to me in some of the Rust code Mozilla had integrated into Firefox (the CSS styling code). There was some font cache shenanigans that was causing their font loading to work only semi-consistently, and that would outright crash this subsystem, and tofu-ify CJK text entirely as a result.
And the underlying panic was totally recoverable in theory if you looked at the call stack! Just people had decided to not Result-ify a bunch of falliable code.
I understand your point in general, I just find that if you're writing a program that is running on unconstrained environments, not panic'ing (or at least not doing it so bluntly at a low level) can at the very least help with debugging.
At least have the courtesy to put the panic at a higher level to provide context beyond "key not found!"!
Either the library should have enforced the invariant of the key existing (and returned an equivalent error, or handled it internally), or documented the preconditions at a higher level function that you could see.
This is why Result (or Maybe, or runExceptT, and so on in other languages) is a perfectly safe way of handling unexpected or invalid data. As long as you enforce your invariants in pure code (code without side effects) then failure is safe.
This is also why effects should ideally be restricted and traceable by the compiler, which, unfortunately, Rust, ML, and that chain of the evolution tree didn't quite stretch to encompass.
In a good software architecture (imo) panics and other hard failure mechanisms are there for splitting E into E1 and E2, where E1 is the set of errors that can happen due to the caller screwing up and E2 being the set of errors that the caller screwed up. The caller shouldn't have to reason about the callee possibly being incorrect!
Functional programming doesn't really come into the discussion here - oftentimes this crops up in imperative or object oriented code where function signatures are lossy because code relies on side effects or state that the type system can't/won't capture (for example, a database or file persisted somewhere). Thats where you'll drop an assert or panic - not as a routine part of error handling.
Ideally, you can constrain the set of inputs to only valid ones by leveraging types. But if that's not possible and a truly invalid input is passed, then you should panic. At least that's the mental model that Rust is going with.
You do lose out on the ability to "catch" programming errors in subcomponents of your program. For example, it's extremely useful to catch exceptions related to programming errors for called code in response to a web request, and return a 500 in those cases. One could imagine a "try" "catch" for panics.
The thing is, it takes a lot of discipline by authors to not riddle their code with panics/exceptions when the language provides a try/catch mechanism (see C# and Java), even when a sensible error as value could be returned. So Rust opts to not introduce the footgun and extra complexity, at the expense of ungraceful handling of programming errors.
But how can the caller know what is "a truly invalid input"? The article has an example: "we unfortunately cannot rely on panic annotations in API documentation to determine a priori whether some Rust code is no-panic or not."
It means that calling a function is like a lottery: some input values may panic and some may not panic. The only way to ensure that it doesn't panic is to test it with all possible input values, but that is impossible for complex functions.
It would be better to always return an error and let the caller decide how to handle it. Many Rust libraries have a policy that if the library panics, then it is a bug in the library. It's sad that the Rust standard library doesn't take the same approach. For println!(), it would mean returning an error instead of panicking.
The best way to handle this is to crash the program. If you need constant uptime, then restart the program. If you absolutely need to keep things running then, yeah try to recover then. The last option isn't as bad for something like an http server where one request caused it to error and you just handle that error and keep the other threads running.
But for something like a 3D video game. If you arrive at erroneous state, man. Don't try to keep that thing going. Kill it now.
No, your program correctly determined that user input was invalid.
Or your parser backtracked from parsing a Bool and decided to try to parse an Int instead.
Say user input is a number and can never exceed 5. If the user input exceeds 5 your program should handle that gracefully. This is not invalid state. It is handling invalid input while remaining in valid state.
Let say it does exceed 5 and You forget to check that it should never exceeds 5 and this leads to a division by zero further down your program. You never intended for this to happen and you don’t k ow why this happened. This is invalid state.
Now either this division by zero leads to an error value or it can throw an exception. It doesn’t matter. Do you know how to recover? You don’t even know where the bug is. A lot of times your don’t even know what to do. This error can bubble up all the way to main and now what do you do with it?
You crash the program. Or you can put in a fail safe before the error bubbles up to main and do something else but now (if your program retains and mutates that state) has invalid values in it and a known bug as well.
Imagine that input number represented enumeration values for movement of some robot. You now have one movement that doesn’t exist or was supposed to be something else. Thus if you keep your program running the robot ends up in an unexpected and erroneous place. That’s why I’m saying a program should crash. It should not be allowed to continue running with invalid state.
Like imagine if this was a mission critical auto pilot and you detect negative 45 meters for altitude. Bro crash and restart. Don’t try to keep that program running by doing something crazy in attempt to make the altitude positive and correct. Reset it and hope it never goes to the invalid state again.
No thank you.
> Or you can put in a fail safe before the error bubbles up to main and do something else
In other words,
>> your parser backtracked from parsing a Bool and decided to try to parse an Int instead
> but now (if your program retains and mutates that state) has invalid values in it and a known bug as well.
Unless,
>>> The thing with functional programming (specifically, immutable data,) is that as long as the invalid state is immutable, you can just back up to some previous caller, and they can figure out whether to deal with it or whether to reject up the its previous caller.
Look at my auto pilot example. You have a bug in your program you don’t know about. Altitude reads -9999 meters your heading and direction is reading at the speed of light. Your program sees these values and recognizes invalid state. There is a BUG in your program. Your program WAS never designed to go here.
You want to try to recover your autopilot program? How the fuck are you gonna do that? You don’t even know where the bug is. Does your recovery routine involve patching and debugging? Or are you gonna let your autopilot keep operating the plane with those nonsense values? That autopilot might hit the wind breaks to try to slow down the plane and end up crashing the thing.
You don’t recover this. You restart the program and pray it doesn’t hit that state again then when you are on the ground you debug it.
>>> The thing with functional programming (specifically, immutable data,) is that as long as the invalid state is immutable, you can just back up to some previous caller, and they can figure out whether to deal with it or whether to reject up the its previous caller.
This makes no fucking sense. Where the hell will you back up to? Your error came from somewhere but you don’t know where. Return an error value in the child function the parent function receives the error and returns an error itself and this keeps going until you bubble up to the main function because you don’t know where that error came from.
Now you have an error in the main function. Wtf are you gonna do? How do you handle an error in the main function that you have no idea where it came from? Here’s an idea. You have the main function restart the program loop. See a similarity here? It’s called crashing and restarting the program. Same effect!
This isn’t a functional programming versus non functional thing. It’s a concept that you’re not seeing.
https://news.ycombinator.com/item?id=14409950
They are great for handling expected errors that make sense to handle explicitly.
If you try to wrap up any possible error that could ever happen in them you will generate horrendous code, always having to unwrap things, everything is a Maybe. No thanks.
I know it is tempting to think "I will write the perfect program and handle all possible errors and it will never crash" but that just results in overly complex code that ends up having more bugs and makes debugging harder. Let it crash. At the point where the error happened, don't just kick the bucket down the road. Just log the the problem and call it a day. Exceptions are an amazing tool to have for things that are.. exceptions.
Even if you are no_std, core has tons of APIs like unwrap(), index slicing, etc. that can panic if you violate the preconditions. It's not practical to grep for all of them.
1: https://crates.io/crates/panic-analyzer
2: https://crates.io/crates/no-panic
No-panic looks nifty: it appears to be reliable, which is great. I wish there was an easy way to automatically apply this annotation to every single function in a given file or crate.
https://doc.rust-lang.org/beta/src/std/io/stdio.rs.html#674-...
The implementation calls indeed a panicking API, OnceLock::get_or_init:
https://doc.rust-lang.org/beta/std/sync/struct.OnceLock.html...
But it only panicks if it is being used in a wrong way, which it isn't. The usage is contained within the implementation of std::io::stdout, so it's an implementation detail.
It's a shame that there are no better ways to eliminate panics in case they are impossible to trigger. The article shows some tricks, but I think the language is missing still some expressability around this, and the stdlib should also thrive harder to actually get rid of hard-to-optimize links to panic runtime in case of APIs that don't actually panic.
The main reason getting panic-free stdout is hard, is that the STDOUT initializers both call allocating constructors, and LineWriter doesn't have non-allocating constructors.
Solving this elegantly seems hard.
Still miffed, but we'll get there.
But I would assume that for mozilla their entire CSS subsystem is pulled in as a git (hg?) submodule or something anyways.
Essentially an error means that the caller failed in a way that's expected. A panic means the caller broke some contract that wasn't expressed in the arguments.
A good example of this is array indexing. If you're using it you're saying that the caller (whoever is indexing into the array) has already agreed not to access out of bounds. But we still have to double check if that's the case.
And if you were to say that hey, that implies that the checks and branches should just be elided - you can! But not in safe rust, because safe code can't invoke undefined behavior.
Obviously context-free this is very hand wave-y, but would you want Firefox to crash every time a website prematurely closes its connection to your browser for whatever reason? No, right? You would want Firefox to fail gracefully. That is what I wanted.
panics are very much designed to be recoverable at some well defined boundaries (e.g. the request handler of a web server, a thread in a thread pool etc.)
this is where most of it's overhead comes from
you can use panic=abort setting to abort on panics and there is a funny (but unpractical) hack with which somewhat can make sure that no not-dead-code-eliminated code path can hit a panic (you link the panic->abort handler to a invalid symbol)
However, since it is still possible to have them in a place where the exiting the process is not okay, it was beneficial to add a way to recover from them. It does not mean that they are designed to be recoverable.
> this is where most of it's overhead comes from
Overhead comes from the cleaning process. If you don't clean properly, you might leak information or allocate more resources than you should.
no it's them being recoverable at well defined boundaries is a _fundamental_ design aspect of rust
> Overhead comes from the cleaning process. If you don't clean properly, you might leak information or allocate more resources than you should.
and that same cleanup process makes it recoverable
https://doc.rust-lang.org/book/ch09-01-unrecoverable-errors-...
Panics really seem bad for composability. And relying on the optimzer here seems like a fragile approach.
(And how is there no -nopanic compiler flag?)
Trying to enforce no-panics without a proof system helping out is just not a very practical approach to programming. Consider code like
This code is obviously correct. It never panics. There's no better way to write it. The optimizer will instantly see that and remove the panicing branch. The language itself doesn't want to be in the business of trying to see things like that.Or consider code like
This code never panics. Integer arithmetic contains a hidden panic path on overflow, but that can't occur here because the length of a vector is always less than usize::MAX.Or so on.
Basically every practical language has some form of "this should never happen" root. Rust's is panics. C's is undefined behavior. Java's is exceptions.
Finally consider that this same mechanism is used for things like stack overflows, which can't be statically guaranteed to not occur short of rejecting recursion and knowledge of the runtime environment that rustc does not have.
---
Proof systems on top of rust like creusot or kani do tend to try to prove the absence of panics, because they don't have the same compunctions about not approving code today that they aren't absolutely sure they will approve tomorrow as well.
[1]: https://en.wikipedia.org/wiki/Rice%27s_theorem
It doesn't panic within the code you typed, but it absolutely still can panic on OOM. Which is sort of the problem with "no panic"-style code in any language - you start hitting fundamental constructs that can can't be treated as infallible.
> Basically every practical language has some form of "this should never happen" root.
99% of "unrecoverable failures" like this, in pretty much every language, are because we treat memory allocation as infallible when it actually isn't. It feels like there is room in the language design space for one that treats allocation as a first-class construct, with suitable error-handling behaviour...
It's not really what this is about IMV. The vast majority of unrecoverable errors are simply bugs.
A context free example many will be familiar with is a deadlock condition. The programmer's mental model of the program was incomplete or they were otherwise ignorant. You can't statically eliminate deadlocks in an arbitrary program without introducing more expensive problems. In practice programmers employ a variety of heuristics to avoid them and just fix the bugs when they are detected.
The same article written a couple years in the future would look very different
In other environments, like embedded or safety-critical devices, I would need a guarantee that even heap allocation failure can not cause a panic.
Here’s a archived link:
https://web.archive.org/web/20250204050500/https://blog.reve...
EDIT - shockingly, reader mode also fails completely after the page reloads itself
Note that on Android process separation is not usually as good, so a crashing iframe can bring down the whole page.
I can't replicate the crash at all on my Linux cloud VM though. Usually the only difference there is that advertisers tend to not buy ads for clients on cloud IPs.
Specifically, how does it handle recursion? Consider, for example, the following function, which decrements a number until it reaches zero. At each step, it asserts that the number is nonzero before recursing:
fn recursive_countdown(n: u32) -> u32 { assert!(n > 0, "n should always be positive"); if n == 1 { return 1; } recursive_countdown(n - 1) }
Can the compiler prove that the assertion always holds and possibly optimize it away? Or does the presence of recursion introduce limitations in its ability to reason about the program?
Basically, the promise here "We formally promise not to promise anything other than the fact that optimized code should have 'broadly' the same effects and outputs as non-optimized code, and if you want to dig into exactly what 'broadly' means prepare to spend a lot of time on it". Not only are there no promises about complexity, there's no promises that it will work the same on the same code in later versions, nor that any given optimization will continue firing the same way as you add code.
You can program this way. Another semi-common example is taking something like Javascript code and carefully twiddling with it such that a particular JIT will optimize it in a particular way, or if you're some combination of insane and lucky, multiple JITs (including multiple versions) will do some critical optimization. But it's the sort of programming I try very, very hard to avoid. It is a path of pain to depend on programming like this and there better be some darned good reason to start down that path which will, yes, forever dominate that code's destiny.
Is there some context I'm missing here? Is this to be used from non-Rust applications for example?
Typically, safety-related processes are set up in two phases. First they set up, then they indicate readiness and perform their safe operation. A robot, for example, may have some process checking the position of the robot against a virtual fence. If the probability for passing through that fence passes some limit, this requires the process to engage breaks. The fence will need to be loaded from a configuration, communication with the position sensors will need to be established, the fence will generally need to be transformed into coordinates that can be guaranteed to be checked safely, taking momentum and today's breaking performance in account, for example. The breaks itself may need to be checked. All that is fine to do in an unsafe state with panics that don't just abort but unroll and full std. Then that process indicates readiness to the higher-level robot control process.
Once that readiness has been established, the software must be restricted to a much simpler set of functions. If libraries can guarantee that they won't call panic!, that's one item off our checklist that we can still use them in that state.
Using this flag one than can use `panic_abort`. This will eliminate the unwinding part but would still give a "nice" printout on a panic itself. This reduces, in most cases, the mention bloat by a lot. Though nice printouts also cost binary space. For eliminating that `panic_immidiate_abort` exists.
But yeah the above is only about bloat and not the core goal to eliminate potential path's in your program, that would lead to a panic condition itself.
Also currently building the std library yourself needs a nightly compiler. There is afaik work on bringing this to a stable compiler but how exactly is still work in progress.
Wouldn’t panicking asap make debugging easier?
Something like: Given a function, rewrite its signature to return a Result if it doesn't already, rewrite each non-Resulty return site to a Some(), add a ? to every function call, then recurse into each called function and do the same.
[1] https://doc.rust-lang.org/std/panic/fn.catch_unwind.html
You really want to avoid sharing mutable objects across a catch_unwind() boundary, and also avoid using it on a regular basis. Aside from memory leaks, panicking runs the thread's panic hook, which by default prints a stacktrace. You can override the panic hook to be a no-op, but then you won't see anything for actual panics.
Instead of serializing data (to disk, not the network), it would be much faster if Rust allowed us to allocate datastructures directly in an mmapped file, and allowed us to read back the data (basically patching the pointers so they become valid if the base address changed).
"We'd much rather like to make library to corrupt the memory of the rest of the application and generally make the demons fly out of the users' noses, as it does when written in C"?
I believe implementations of C stdio also can abort on program startup if somehow the pthreads' locking mechanism is broken (or if e.g. fcntl(2)/open(2) keeps returning -1), Rust is not that unique in this regard.
On the same page are also the options for controlling debug assertions and overflow checks, which would get rid of the "panics in debug, but not release", if that behavior bugs you
1: https://doc.rust-lang.org/cargo/reference/profiles.html#pani...
``` [profile.release] panic = 'abort' ```
1: https://crates.io/crates/no-panic
2: https://crates.io/crates/no-panics-whatsoever
Setting `panic = abort` disables unwinding and this means leaking memory, not closing file descriptors, not unlocking mutexes and not rolling back database transactions on panic. It's fine for some applications to leave this to the operating system on process exit but I would argue that the default unwinding behavior is better for typical userspace applications.
The whole of modern operating systems are already very familiar with the idea of programs not being able to exit gracefully, and there’s already a well understood category of things that happen automatically even if your program crashes ungracefully. Whole systems are designed around this (databases issuing rollbacks when the client disconnects, being a perfect example.) The best thing to do is embrace this and never, ever rely on a Drop trait being executed for correctness. Always assume you could be SIGKILLed at any time (which you always can. Someone can issue a kill -9, or you could get OOM killed, etc.)
But there are still cases where you would like to fsync your mmaps, print out a warning message or just make sure your #[should_panic] negative tests don't trigger false positives in your tooling (like leak detectors or GPU validators) or abort the whole test run.
It's not perfect by any means but it's better than potentially corrupting your data when a trivial assert fires or making negative tests spew warnings in ci runs.
It's very easy to opt out from, and I don't consider the price of panic handlers and unwinding very expensive for most use cases.
But I tend to lament the overall tendency for people to write cleanup code in general for this kind of thing. It’s one of those “lies programmers believe about X” kinds of scenarios. Your program will crash, and you will hit situations where your cleanup code will not run. You could get OOM killed. The user can force quit you. Hell, the power could go out! (Or the battery could go dead, etc.)
Nobody should ever write code that is only correct if they are given the opportunity to perfectly clean up after any failure that happens.
I see this all the time: CLI apps that trap Ctrl-C and tell you you can’t quit (yes I bloody well can, kill -9 is a thing), apps which don’t bother double checking that the files they left behind on a previous invocation are actually still used (stale pid files!!!), coworkers writing gobs of cleanup code that could have been avoided by simply doing nothing, etc etc.
It also feels like most of the pains on avoiding panics centers around allocations which, though a bit unfortunate, makes sense; it was an intentional design choice to make allocations panic instead of return Results, because most users of the language would probably crash on allocation fails anyways and it would introduce a lot of clutter. There was some effort some while ago on having better fallible allocations, but I'm not sure what happened over there.