- Start Date: 2020-10-25
- RFC PR: rust-lang/rfcs#3007
- Rust Issue: #80162
Summary
This RFC proposes to make core::panic!
and std::panic!
identical and consistent in Rust 2021,
and proposes a way to deal with the differences in earlier editions without breaking code.
Problems
core::panic!
and std::panic!
behave mostly the same, but have their own incompatible quirks for the single-argument case.
This leads to several different problems, which would all be solved if they didnโt special-case panic!(one_argument)
.
For multiple-arguments (e.g. panic!("error: {}", e)
), both already behave identical.
Panic
Both do not use format_args!("..")
for panic!("..")
like they do for multiple arguments, but use the string literally.
๐ Problem 1: panic!("error: {}")
is probably a mistake, but compiles fine.
๐ Problem 2: panic!("Here's a brace: {{")
outputs two braces ({{
), not one ({
).
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.
๐ Problem 3: panic!(123)
, panic!(&"..")
, panic!(b"..")
, etc. are probably mistakes, but compile fine with std
.
In the case of core::panic!(x)
, x
must be a &str
, but does not have to be a string literal, nor does it have to be 'static
.
This means that core::panic!("{}")
and core::panic!(string.as_str())
compile fine.
๐ Problem 4: let error = String::from("error"); panic!(&error);
works fine in no_std
code, but no longer compiles when switching no_std
off.
๐ Problem 5: panic!(CustomError::Error);
works with std, but no longer compiles when switching no_std
on.
Assert
assert!(expr, args..)
and assert_debug(expr, args..)
expand to panic!(args..)
and therefore will have all the same problems.
In addition, these can result in confusing mistakes:
assert!(v.is_empty(), false); // runs panic!(false) if v is not empty ๐
๐ Problem 6: assert!(expr, expr)
should probably have been a assert_eq!
, but compiles fine and gives no useful panic message.
Because core::panic!
and std::panic!
are different, assert!
and related macros expand to panic!(..)
, not to $crate::panic!(..)
,
making these macros not work with #![no_implicit_prelude]
, as reported in #78333.
This also means that the panic of an assert can be accidentally โhijackedโ by a locally defined panic!
macro.
๐ Problem 7: assert!
and related macros need to choose between core::panic!
and std::panic!
, and canโt use $crate::panic!
for proper hygiene.
Implicit formatting arguments
RFC 2795 adds implicit formatting args, as follows:
let a = 4;
println!("a is {a}");
It modifies format_args!()
to automatically capture variables that are named in a formatting placeholder.
With the current implementations of panic!()
(both coreโs and stdโs), this would not work if there are no additional explicit arguments:
let a = 4;
println!("{}", a); // prints `4`
panic!("{}", a); // panics with `4`
println!("{a}"); // prints `4`
panic!("{a}"); // panics with `{a}` ๐
println!("{a} {}", 4); // prints `4 4`
panic!("{a} {}", 4); // panics with `4 4`
๐ Problem 8: panic!("error: {error}")
will silently not work as expected, after RFC 2795 is implemented.
Bloat
core::panic!("hello {")
produces the same fmt::Arguments
as format_args!("hello {{")
, not format_args!("{}", "hello {")
to avoid pulling in stringโs Display
code,
which can be quite big.
However, core::panic!(non_static_str)
does need to expand to format_args!("{}", non_static_str)
, because fmt::Arguments
requires a 'static
lifetime
for the non-formatted pieces. Because the panic!
macro_rules
macro canโt distinguish between non-'static
and 'static
values,
this optimization is only applied to what macro_rules consider a $_:literal
, which does not include concat!(..)
or CONST_STR
.
๐ Problem 9: const CONST_STR: &'static str = "hi"; core::panic!(CONST_STR)
works,
but will silently result in a lot more generated code than core::panic!("hi")
.
(And also needs special handling to make const_panic
work.)
Solution if we could go back in time
None of these these problems would have existed if
1) panic!()
did not handle the single-argument case differently, and
2) std::panic!
was no different than core::panic!
:
// core
macro_rules! panic {
() => (
$crate::panic!("explicit panic")
);
($($t:tt)*) => (
$crate::panicking::panic_fmt($crate::format_args!($($t)+))
);
}
// std
use core::panic;
The examples from problems 1, 2, 3, 4, 5, 6 and 9 would simply not compile, and problems 7 and 8 would not occur.
However, that would break too much existing code.
Proposed solution
Considering we should not break existing code, I propose we gate the breaking changes on the 2021 edition.
In addition, we add a lint that warns about the problems in Rust 2015/2018, while not giving errors or changing the behaviour.
Specifically:
-
Only for Rust 2021, we apply the breaking changes as in the previous section. So,
core::panic!
andstd::panic!
are the same, and always put their arguments throughformat_args!()
.Any optimization that needs special casing should be done after
format_args!()
. (E.g. usingfmt::Arguments::as_str()
, as is already done forcore::panic!("literal")
.)This means
std::panic!(x)
can no longer be used to panic with arbitrary (Any + Send
) payloads. -
We add
std::panic::panic_any(x)
, that still allows programs with std to panic with arbitrary (Any + Send
) payloads. -
We add a lint for Rust 2015/2018 that warns about problem 1, 2, and 8, similar to what Clippy already has.
Note that this lint isnโt just to warn about incompatibilities with Rust 2021, but also to warn about usages of
panic!()
that are likely mistakes.This lint suggests add an argument to
panic!("hello: {}")
, or to insert"{}",
to use it literally:panic!("{}", "hello: {}")
. (Screenshot here.) The second suggestion can be a pessimization for code size, but I believe that can be solved separately. -
After
panic_any
is stable, we add a lint for Rust 2015/2018 (or extend the one above) to warn about problem 3, 4, 5 and 9. It warns aboutpanic!(x)
for anything other than a string literal, and suggests to usepanic_any(x)
instead ofstd::panic!(x)
, andpanic!("{}", x)
instead ofcore::panic!(x)
.It will also detect problem 6 (e.g.
assert!(true, false)
) because that expands to such a panic invocation, but will suggestassert_eq!()
for this case instead. -
We modify the panic glue between core and std to use
Arguments::as_str()
to make sure bothstd::panic!("literal")
andcore::panic!("literal")
result in a&'static str
payload. This removes one of the differences between the two macros in Rust 2015/2018.This is already merged.
-
Now that
std::panic!("literal")
andcore::panic!("literal")
behave identically, we modifytodo!()
,unimplemented!()
,assert_eq!()
, etc. to use$crate::panic!()
instead ofpanic!()
. This solves problem 7 for all macros exceptassert!()
. -
We modify
assert!()
to use$crate::panic!()
instead ofpanic!()
for the single argument case in Rust 2015/2018, and for all cases in Rust 2021.This solves problem 7 for the common case of
assert!(expr)
in Rust 2015/2018, and for all cases ofassert!
in Rust 2021.
Together, these actions address all problems, without breaking any existing code.
Drawbacks
-
This results in subtle differences between Rust editions.
-
This requires
assert!
andpanic!
to behave differently depending on the Rust edition of the crate it is used in.panic!
is just amacro_rules
macro right now, which does not natively support that.
Alternatives
- Instead of the last step, we could also simply break
assert!(expr, non_string_literal)
in all editions. This usage is probably way less common thanpanic!(non_string_literal)
.