Both std::panic!()
and core::panic!()
are macro_rules which handle
the single-argument case specially, both in their own incompatible way.
This note discusses the problems and proposes changes to fix the inconsistencies,
both to prevent mistakes and confusion, and to make room for RFC #2795.
(This document is a response to this comment.)
UPDATE: This document is now outdated. Latest version is in the RFC.
In the case of std::panic!(x)
, x
does not have to be a string literal,
but can be of any (Any + Send
) type.
This means that std::panic!("{}")
and even std::panic!(&"hi")
compile without errors or warnings,
even though these are most likely mistakes.
In the case of core::panic!(x)
, x
must be a &str
, but does not have to
be a string literal, or even 'static
.
This means that core::panic!("{}")
and core::panic!(string.as_str())
compile
fine, which is somewhat surprising.
These inconsistencies become a much bigger problem once RFC #2795 is implemented. This RFC adds implicitly captured formatting arguments, as follows:
let a = 4;
println!("a is {a}");
It modifies format_args!()
to automatically capture variables that are named
in a formatting placeholder. See the RFC for details.
With the current implementations of panic!()
, the result would be unfortunate:
let a = 4;
println!("{}", a); // prints `4`
panic!("{}", a); // panics with `4`
println!("{a}"); // prints `4`
panic!("{a}"); // panics with `{a}` :(
Std panic call | Thrown value | Type (in a Box<Any + Send> ) |
notes |
---|---|---|---|
std::panic!("hello") |
"hello" |
&'static str |
|
std::panic!("hello {}") |
"hello {}" |
&'static str |
1 |
std::panic!("hello {}", 123) |
"hello 123" |
String |
|
std::panic!(concat!("he", "llo")) |
"hello" |
&'static str |
|
std::panic!(STATIC_STR) |
"abc" |
&'static str |
|
std::panic!(string.as_str()) |
Error: Need 'static |
||
std::panic!(123) |
123 |
i32 |
2 |
Core panic call | fmt::Arguments in PanicInfo::message() |
notes |
---|---|---|
core::panic!("hello") |
format_args!("hello") |
|
core::panic!("hello {}") |
format_args!("hello {{}}") |
1 |
core::panic!("hello {}", 123) |
format_args("hello {}", 123) |
|
core::panic!(concat!("he", "llo")) |
format_args!("{}", "hello") |
3 |
core::panic!(STATIC_STR) |
format_args!("{}", "abc") |
3 |
core::panic!(string.as_str()) |
format_args!("{}", string.as_str()) |
3 |
core::panic!(123) |
Error: Need &str |
1: No warning or error about missing the formatting argument.
2: Works for any type that implements Any + Send
.
3: These fmt::Arguments
return None
from their as_str()
because they are not of the form format_args!("...")
.
This means that showing these will have to pull in unecessary string formatting/padding code.
Ideally, the both panic macros would have equal behaviour, and
not handle the single-argument case differently.
They'd pass every invocation through format_args!()
to allow for
implicit arguments, and checking for missing placeholders.
In addition, a new function std::panic_any
would still allow
panicking with other types.
// core
macro_rules! panic {
() => (
$crate::panic!("explicit panic")
);
($($t:tt)*) => (
$crate::panicking::panic_fmt($crate::format_args!($($t)+))
);
}
// std
macro_rules! panic {
() => (
$crate::panic!("explicit panic")
);
($($t:tt)*) => (
$crate::rt::begin_panic_fmt(&$crate::format_args!($($t)+))
);
}
pub fn panic_any<M: Any + Send>(msg: M) -> ! {
crate::panicking::begin_panic(msg);
}
This will be a breaking change, as the behaviour will be different in some situations:
Std panic call | Thrown value | Type (in a Box<Any + Send> ) |
Same as before |
---|---|---|---|
std::panic!("hello") |
"hello" |
String or &'static str * |
Image Not Showing
Possible Reasons
|
std::panic!("hello {}") |
- | Error: Missing argument |
Image Not Showing
Possible Reasons
|
std::panic!("hello {}", 123) |
"hello 123" |
String |
Image Not Showing
Possible Reasons
|
std::panic!(concat!("he", "llo")) |
"hello" |
String or &'static str * |
Image Not Showing
Possible Reasons
|
std::panic!(STATIC_STR) |
Error: Use panic_any(STATIC_STR) instead** |
Image Not Showing
Possible Reasons
|
|
std::panic!(string.as_str()) |
- | Error: Need 'static |
Image Not Showing
Possible Reasons
|
std::panic!(123) |
Error: Use panic_any(123) instead |
Image Not Showing
Possible Reasons
|
Core panic call | fmt::Arguments in PanicInfo::message() |
Same as before |
---|---|---|
core::panic!("hello") |
format_args!("hello") |
Image Not Showing
Possible Reasons
|
core::panic!("hello {}") |
Error: Missing argument |
Image Not Showing
Possible Reasons
|
core::panic!("hello {}", 123) |
format_args("hello {}", 123) |
Image Not Showing
Possible Reasons
|
core::panic!(concat!("he", "llo")) |
format_args!("hello") |
Image Not Showing
Possible Reasons
|
core::panic!(STATIC_STR) |
Error: Need string literal, use panic!("{}", ..) |
Image Not Showing
Possible Reasons
|
core::panic!(string.as_str()) |
Error: Need 'static , use panic!("{}", ..) instead |
Image Not Showing
Possible Reasons
|
core::panic!(123) |
Error: Need &str |
Image Not Showing
Possible Reasons
|
*: This can be &'static str
if begin_panic_fmt
checks fmt::Arguments::as_str()
before formatting into a String
.
**: Or panic!("{}", STATIC_STR)
.
***: This could work again if formatting is implemented as a const fn. Although that has some interesting interactions with RFC 2795.
Unfortunately, we can't just break things :(
Three ways forward:
Tie this change to an edition.
panic!()
s in Rust 2021 would behave slightly different than in Rust 2018.
Rust 2018 is left with inconsistencies in panic!()
.
Slow deprecation path.
panic!("{}")
, panic!(non_literal)
, etc. start giving deprecation/lint warnings in all versions.
We provide good alternatives like panic!("{}", ..)
and panic_any(..)
.
After some months, we make the hard switch to the incompatible behaviour, hoping that everyone acted upon the deprecation warnings and moved away from relying on the behaviour we're now breaking.
A combination of both.
The deprecation/lint warnings are given in Rust 2018/2015, but the hard switch will only happen for Rust 2021.
This way, Rust 2018/2015 users will be made aware of potential mistakes and future incompatibilities, but existing code will not be broken.
(Are there other good ways?)
All of these options have some implementation challenges:
A (deprecation) warning/lint would require a custom lint implementation to trigger only on
string literals that format_args!()
would not accept without extra arguments.
(Just the panic
macro_rules
macro cannot recognise panic!(concat!("a", "b"))
is okay,
while panic!(other_macro!())
is not.
Similarly, it can't distinguish between panic!("ok")
and panic!("bad {}")
.)
Tying the behaviour of the panic macros to the edition requires them to be a builtin macro (or at least use a builtin macro) which tracks down from which crate it was called, and what edition that crate is using.
Note that we could pick different paths for different breakages. panic!("{")
could follow path 2 while panic!(STATIC_STR)
could follow path 1.
panic!("something containing a {")
From these grep.app results,
it seems like using {
in a panic message without formatting arguments rarely happens.
The only two cases that pop up in this search are clearly mistakes.
Breaking those is probably a good thing.
panic!(STATIC_STR)
Also not used very often, but usages are valid and not mistakes: grep.app results
This comment also shows people do use this: comment on #51999.
panic!(some_expr)
Most of these are panic!(format!(..))
(which should've been just panic!(..)
) and other ways to produce a String
, such as
panic!(vec![..].join('\n'))
and panic!(self.get_error())
.
Very few cases for panic!(not_a_string)
.
Only one case of core::panic!(non_static_str)
: In Miri's unit tests.
Although important to note here is that most embedded code (where this is most likely to be used) is probably not open source,
so many usages might stay hidden from searches like this.