RFC 3007: Align std::panic and core::panic macros

lang | libs (panic)

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:

Together, these actions address all problems, without breaking any existing code.

Drawbacks

Alternatives