|
| 1 | +- Start Date: 2020-10-25 |
| 2 | +- RFC PR: [rust-lang/rfcs#3007](https://github.com/rust-lang/rfcs/pull/3007) |
| 3 | +- Rust Issue: [#80162](https://github.com/rust-lang/rust/issues/80162) |
| 4 | + |
| 5 | +# Summary |
| 6 | + |
| 7 | +This RFC proposes to make `core::panic!` and `std::panic!` identical and consistent in Rust 2021, |
| 8 | +and proposes a way to deal with the differences in earlier editions without breaking code. |
| 9 | + |
| 10 | +# Problems |
| 11 | + |
| 12 | +`core::panic!` and `std::panic!` behave mostly the same, but have their own incompatible quirks for the single-argument case. |
| 13 | + |
| 14 | +This leads to several different problems, which would all be solved if they didn't special-case `panic!(one_argument)`. |
| 15 | + |
| 16 | +For multiple-arguments (e.g. `panic!("error: {}", e)`), both already behave identical. |
| 17 | + |
| 18 | +## Panic |
| 19 | + |
| 20 | +Both do not use `format_args!("..")` for `panic!("..")` like they do for multiple arguments, but use the string literally. |
| 21 | + |
| 22 | +*💔 **Problem 1:** `panic!("error: {}")` is probably a mistake, but compiles fine.* |
| 23 | + |
| 24 | +*💔 **Problem 2:** `panic!("Here's a brace: {{")` outputs two braces (`{{`), not one (`{`).* |
| 25 | + |
| 26 | +In the case of `std::panic!(x)`, `x` does not have to be a string literal, but can be of any (`Any + Send`) type. |
| 27 | +This means that `std::panic!("{}")` and even `std::panic!(&"hi")` compile without errors or warnings, even though these are most likely mistakes. |
| 28 | + |
| 29 | +*💔 **Problem 3:** `panic!(123)`, `panic!(&"..")`, `panic!(b"..")`, etc. are probably mistakes, but compile fine with `std`.* |
| 30 | + |
| 31 | +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`. |
| 32 | +This means that `core::panic!("{}")` and `core::panic!(string.as_str())` compile fine. |
| 33 | + |
| 34 | +*💔 **Problem 4:** `let error = String::from("error"); panic!(&error);` works fine in `no_std` code, but no longer compiles when switching `no_std` off.* |
| 35 | + |
| 36 | +*💔 **Problem 5:** `panic!(CustomError::Error);` works with std, but no longer compiles when switching `no_std` on.* |
| 37 | + |
| 38 | +## Assert |
| 39 | + |
| 40 | +`assert!(expr, args..)` and `assert_debug(expr, args..)` expand to `panic!(args..)` and therefore will have all the same problems. |
| 41 | +In addition, these can result in confusing mistakes: |
| 42 | + |
| 43 | +```rust |
| 44 | +assert!(v.is_empty(), false); // runs panic!(false) if v is not empty 😕 |
| 45 | +``` |
| 46 | + |
| 47 | +*💔 **Problem 6:** `assert!(expr, expr)` should probably have been a `assert_eq!`, but compiles fine and gives no useful panic message.* |
| 48 | + |
| 49 | +Because `core::panic!` and `std::panic!` are different, `assert!` and related macros expand to `panic!(..)`, not to `$crate::panic!(..)`, |
| 50 | +making these macros not work with `#![no_implicit_prelude]`, as reported in [#78333](https://github.com/rust-lang/rust/issues/78333). |
| 51 | +This also means that the panic of an assert can be accidentally 'hijacked' by a locally defined `panic!` macro. |
| 52 | + |
| 53 | +*💔 **Problem 7:** `assert!` and related macros need to choose between `core::panic!` and `std::panic!`, and can't use `$crate::panic!` for proper hygiene.* |
| 54 | + |
| 55 | +## Implicit formatting arguments |
| 56 | + |
| 57 | +[RFC 2795] adds implicit formatting args, as follows: |
| 58 | + |
| 59 | +```rust |
| 60 | +let a = 4; |
| 61 | +println!("a is {a}"); |
| 62 | +``` |
| 63 | + |
| 64 | +It modifies `format_args!()` to automatically capture variables that are named in a formatting placeholder. |
| 65 | + |
| 66 | +With the current implementations of `panic!()` (both core's and std's), this would not work if there are no additional explicit arguments: |
| 67 | + |
| 68 | +```rust |
| 69 | +let a = 4; |
| 70 | + |
| 71 | +println!("{}", a); // prints `4` |
| 72 | +panic!("{}", a); // panics with `4` |
| 73 | + |
| 74 | +println!("{a}"); // prints `4` |
| 75 | +panic!("{a}"); // panics with `{a}` 😕 |
| 76 | + |
| 77 | +println!("{a} {}", 4); // prints `4 4` |
| 78 | +panic!("{a} {}", 4); // panics with `4 4` |
| 79 | +``` |
| 80 | + |
| 81 | +*💔 **Problem 8:** `panic!("error: {error}")` will silently not work as expected, after [RFC 2795] is implemented.* |
| 82 | + |
| 83 | +## Bloat |
| 84 | + |
| 85 | +`core::panic!("hello {")` produces the same `fmt::Arguments` as `format_args!("hello {{")`, not `format_args!("{}", "hello {")` to avoid pulling in string's `Display` code, |
| 86 | +which can be quite big. |
| 87 | + |
| 88 | +However, `core::panic!(non_static_str)` does need to expand to `format_args!("{}", non_static_str)`, because `fmt::Arguments` requires a `'static` lifetime |
| 89 | +for the non-formatted pieces. Because the `panic!` `macro_rules` macro can't distinguish between non-`'static` and `'static` values, |
| 90 | +this optimization is only applied to what macro_rules consider a `$_:literal`, which does not include `concat!(..)` or `CONST_STR`. |
| 91 | + |
| 92 | +*💔 **Problem 9:** `const CONST_STR: &'static str = "hi"; core::panic!(CONST_STR)` works, |
| 93 | +but will silently result in a lot more generated code than `core::panic!("hi")`. |
| 94 | +(And also needs [special handling](https://github.com/rust-lang/rust/pull/78069) to make `const_panic` work.)* |
| 95 | + |
| 96 | +# Solution if we could go back in time |
| 97 | + |
| 98 | +None of these these problems would have existed if |
| 99 | +1\) `panic!()` did not handle the single-argument case differently, and |
| 100 | +2\) `std::panic!` was no different than `core::panic!`: |
| 101 | + |
| 102 | +```rust |
| 103 | +// core |
| 104 | +macro_rules! panic { |
| 105 | + () => ( |
| 106 | + $crate::panic!("explicit panic") |
| 107 | + ); |
| 108 | + ($($t:tt)*) => ( |
| 109 | + $crate::panicking::panic_fmt($crate::format_args!($($t)+)) |
| 110 | + ); |
| 111 | +} |
| 112 | + |
| 113 | +// std |
| 114 | +use core::panic; |
| 115 | +``` |
| 116 | + |
| 117 | +The examples from problems 1, 2, 3, 4, 5, 6 and 9 would simply not compile, and problems 7 and 8 would not occur. |
| 118 | + |
| 119 | +However, that would break too much existing code. |
| 120 | + |
| 121 | +# Proposed solution |
| 122 | + |
| 123 | +Considering we should not break existing code, I propose we gate the breaking changes on the 2021 edition. |
| 124 | + |
| 125 | +In addition, we add a lint that *warns* about the problems in Rust 2015/2018, while not giving errors or changing the behaviour. |
| 126 | + |
| 127 | +Specifically: |
| 128 | + |
| 129 | +- Only for Rust 2021, we apply the breaking changes as in the previous section. |
| 130 | + So, `core::panic!` and `std::panic!` are the same, and *always* put their arguments through `format_args!()`. |
| 131 | + |
| 132 | + Any optimization that needs special casing should be done *after* `format_args!()`. |
| 133 | + (E.g. using [`fmt::Arguments::as_str()`](https://github.com/rust-lang/rust/pull/74056), |
| 134 | + as is [already done](https://github.com/rust-lang/rust/pull/78119) for `core::panic!("literal")`.) |
| 135 | + |
| 136 | + This means `std::panic!(x)` can no longer be used to panic with arbitrary (`Any + Send`) payloads. |
| 137 | + |
| 138 | +- We [add `std::panic::panic_any(x)`](https://github.com/rust-lang/rust/pull/74622), |
| 139 | + that still allows programs with std to panic with arbitrary (`Any + Send`) payloads. |
| 140 | + |
| 141 | +- We [add a lint](https://github.com/rust-lang/rust/pull/78088) for Rust 2015/2018 that warns about problem 1, 2, and 8, |
| 142 | + similar to [what Clippy already has](https://rust-lang.github.io/rust-clippy/master/index.html#panic_params). |
| 143 | + |
| 144 | + 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. |
| 145 | + |
| 146 | + This lint suggests add an argument to `panic!("hello: {}")`, or to insert `"{}", ` to use it literally: `panic!("{}", "hello: {}")`. |
| 147 | + ([Screenshot here.](https://user-images.githubusercontent.com/783247/96643867-79eb1080-1328-11eb-8d4e-a5586837c70a.png)) |
| 148 | + The second suggestion can be a pessimization for code size, but I believe that [can be solved separately](https://github.com/rust-lang/rust/issues/78356). |
| 149 | + |
| 150 | +- 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. |
| 151 | + It warns about `panic!(x)` for anything other than a string literal, and suggests to use |
| 152 | + `panic_any(x)` instead of `std::panic!(x)`, and |
| 153 | + `panic!("{}", x)` instead of `core::panic!(x)`. |
| 154 | + |
| 155 | + It will also detect problem 6 (e.g. `assert!(true, false)`) because that expands to such a panic invocation, |
| 156 | + but will suggest `assert_eq!()` for this case instead. |
| 157 | + |
| 158 | +- We [modify the panic glue between core and std](https://github.com/rust-lang/rust/pull/78119) |
| 159 | + to use `Arguments::as_str()` to make sure both `std::panic!("literal")` and `core::panic!("literal")` |
| 160 | + result in a `&'static str` payload. This removes one of the differences between the two macros in Rust 2015/2018. |
| 161 | + |
| 162 | + This is already merged. |
| 163 | + |
| 164 | +- Now that `std::panic!("literal")` and `core::panic!("literal")` behave identically, |
| 165 | + [we modify `todo!()`, `unimplemented!()`, `assert_eq!()`, etc.](https://github.com/rust-lang/rust/pull/78343) |
| 166 | + to use `$crate::panic!()` instead of `panic!()`. This solves problem 7 for all macros except `assert!()`. |
| 167 | + |
| 168 | +- We modify `assert!()` to use `$crate::panic!()` instead of `panic!()` for the single argument case in Rust 2015/2018, |
| 169 | + and for all cases in Rust 2021. |
| 170 | + |
| 171 | + This solves problem 7 for the common case of `assert!(expr)` in Rust 2015/2018, and for all cases of `assert!` in Rust 2021. |
| 172 | + |
| 173 | +Together, these actions address all problems, without breaking any existing code. |
| 174 | + |
| 175 | +# Drawbacks |
| 176 | + |
| 177 | +- This results in subtle differences between Rust editions. |
| 178 | + |
| 179 | +- This requires `assert!` and `panic!` to behave differently depending on the Rust edition of the crate it is used in. |
| 180 | + `panic!` is just a `macro_rules` macro right now, which does not natively support that. |
| 181 | + |
| 182 | +# Alternatives |
| 183 | + |
| 184 | +- Instead of the last step, we could also simply break `assert!(expr, non_string_literal)` in all editions. |
| 185 | + This usage is probably way less common than `panic!(non_string_literal)`. |
| 186 | + |
| 187 | +[RFC 2795]: https://rust-lang.github.io/rfcs/2795-format-args-implicit-identifiers.html |
0 commit comments