Summary
Allow functions and closures to be classified based on whether they may or may not panic. Add an unsafe attribute that allows the compiler to assume that a particular section of code will never panic.
Motivation
- Some functions should not panic. Examples include:
drop
(possibly bad idea?), closure argument totake_mut
, Rust callbacks called from C (FFI). If they panic, the program would abort, or in the case of FFI callbacks undefined behavior would result. - Removing panics may slightly improve performance.
Detailed design
Overall goals
- Maintain as much backward compatibility as possible
- Enable a reasonable amount of expressiveness without compromising type inference
- Minimize the amount of magic built into the type system (or try to … at least)
Panic parameter
Functions are classified into two categories:
- Functions known to panic
- Functions known to not panic
The categorization is done by adding a new special parameter to the Fn
traits called 'panic
:
pub trait Fn<'panic, Args>: FnMut<'panic='panic, Args> {
extern "rust-call" fn call(&self, args: Args)<'panic> -> Self::Output;
}
pub trait FnMut<'panic, Args>: FnOnce<'panic='panic, Args> {
extern "rust-call" fn call_mut(&mut self, args: Args)<'panic> -> Self::Output;
}
pub trait FnOnce<'panic: *Panic, Args> {
type Output;
extern "rust-call" fn call_once(self, args: Args)<'panic> -> Self::Output;
}
The 'panic
parameter is special: it is neither a lifetime parameter nor a type parameter. The kind of the 'panic
parameter is *Panic
. The ordinary fn(…) -> …
and Fn(…) -> …
syntax for types would be extended to:
fn::<'panic=…>(…) -> …
Fn::<'panic=…>(…) -> …
For definitions of functions and closures:
fn …(…)<'panic=…> -> … { … }
|…|<'panic=…> -> … { … }
Functions that never panic have 'panic=false
. Functions that do panic have 'panic=true
.
For backward compatibility reasons:
- The parameter is optional in both
Fn(…) -> …
andfn(…) -> …
. If omitted, it is inferred (see “Panic inference” below), similar to lifetimes. Therefore, the'panic
parameter must be specified using the keyword syntax (the'panic=
part is mandatory). - If the
'panic
parameter is omitted fromimpl<'panic> Fn<'panic, …>
, then it is presumedtrue
. - If the
'panic
parameter is omitted from the method of any trait, then it is presumedtrue
.TheDrop::drop
method could be modified to'panic=false
. This is not a breaking change (see “Panic unification” below).
Panic inference
The inference of the 'panic
parameter is based on the contents of the function:
- If a function
f
calls any other function with'panic=true
, then the functionf
has'panic=true
as well. - Otherwise, if the function
f
calls various generic functions with'panic=p1
,'panic=p2
, …,'panic=pN
, then the functionf
has'panic=p1 || p2 || … || pN
where||
is Boolean OR. - Otherwise, the function
f
has'panic=false
.
Functions imported from C are always 'panic=false
.
Panic unification
If the panic checker expects 'panic=false
for some function/closure argument, but the function argument is actually 'panic=true
, then:
- The frontend will issue a warning, with a note telling the user that they can silence the warning by overriding the inferred
'panic
on the function being passed as an argument. - Any panics within the function will be translated into termination.
The above can also happen if the panic checker finds a function exported to C that is inferred to be 'panic=true
.
It is permissible for the panic checker to expect 'panic=true
for some function/closure argument when the function argument is actually 'panic=false
. No warnings will be issued.
If a function is declared as 'panic=false
, but the function is inferred to be 'panic=true
, then any panics within the function will be translated into termination. No warnings will be issued, as the user is presumed to know what they are doing by overriding panic inference.
If a function is declared as 'panic=true
, but the function is inferred to be 'panic=false
, then there is no effect.
Optimizations
If a region of code is inferred to be 'panic=false
, the compiler is permitted to use this assumption for optimizations.
Unsafe no-panic assumption
A new block attribute is added: #[unsafe_assume(no_panic)]
. This can only be attached to an unsafe
block:
#[unsafe_assume(no_panic)]
unsafe {
…
}
It is undefined behavior if any of the enclosed code panics. In some sense, #[unsafe_assume(no_panic)]
is like declaring 'panic=false
, but there are no guard rails to protect the program from undefined behavior if it does happen.
Potential use: if an area of code uses indexing and/or arithmetic heavily, changing panics into UB allows the compiler to optimize away checks if the code gets inlined. (Obviously this is extremely dangerous and its use should be strongly discouraged!)
How We Teach This
Because it does not require changes to existing code, beginners probably won’t stumble into it until they start using Drop
(?), take_mut
, or FFI callbacks.
The panic parameter should remain mostly invisible in the code, but in the documentation it would be a good idea to show it explicitly.
#[unsafe_assume(no_panic)
should be banished to the Rustonomicon. It’s a really dangerous tool and should not be used lightly.
Drawbacks
- The panic tracking system seems awfully complicated for a relatively minor benefit.
- Unsafe assumptions about whether the code panics can be very dangerous since a lot of the seemingly harmless code like
x + y
can panic!
Alternatives
- Convert
panic
conflicts into errors instead of warnings. - Implement only
#[unsafe_assume(no_panic)]
to allow performance optimizations, but disregard the parameter-based panic-tracking system because the latter adds a lot of complexity. - Not bother tracking panics at all and just rely on documented invariants.
¯\_(ツ)_/¯
Unresolved questions
Can panics be tracked without introducing so much magic into the type system?
Could the technique be generalized to track other useful invariants and/or function attributes (e.g. purity)?