-
Notifications
You must be signed in to change notification settings - Fork 1.6k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
RFC: Plan to make core and std's panic identical. #3007
Merged
Merged
Changes from all commits
Commits
Show all changes
3 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,187 @@ | ||
- Start Date: 2020-10-25 | ||
- RFC PR: [rust-lang/rfcs#3007](https://github.com/rust-lang/rfcs/pull/3007) | ||
- Rust Issue: [#80162](https://github.com/rust-lang/rust/issues/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: | ||
|
||
```rust | ||
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](https://github.com/rust-lang/rust/issues/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: | ||
|
||
```rust | ||
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: | ||
|
||
```rust | ||
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](https://github.com/rust-lang/rust/pull/78069) 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!`: | ||
|
||
```rust | ||
// 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!` and `std::panic!` are the same, and *always* put their arguments through `format_args!()`. | ||
|
||
Any optimization that needs special casing should be done *after* `format_args!()`. | ||
(E.g. using [`fmt::Arguments::as_str()`](https://github.com/rust-lang/rust/pull/74056), | ||
as is [already done](https://github.com/rust-lang/rust/pull/78119) for `core::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)`](https://github.com/rust-lang/rust/pull/74622), | ||
that still allows programs with std to panic with arbitrary (`Any + Send`) payloads. | ||
|
||
- We [add a lint](https://github.com/rust-lang/rust/pull/78088) for Rust 2015/2018 that warns about problem 1, 2, and 8, | ||
similar to [what Clippy already has](https://rust-lang.github.io/rust-clippy/master/index.html#panic_params). | ||
|
||
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.](https://user-images.githubusercontent.com/783247/96643867-79eb1080-1328-11eb-8d4e-a5586837c70a.png)) | ||
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). | ||
|
||
- 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 about `panic!(x)` for anything other than a string literal, and suggests to use | ||
`panic_any(x)` instead of `std::panic!(x)`, and | ||
`panic!("{}", x)` instead of `core::panic!(x)`. | ||
|
||
It will also detect problem 6 (e.g. `assert!(true, false)`) because that expands to such a panic invocation, | ||
but will suggest `assert_eq!()` for this case instead. | ||
|
||
- We [modify the panic glue between core and std](https://github.com/rust-lang/rust/pull/78119) | ||
to use `Arguments::as_str()` to make sure both `std::panic!("literal")` and `core::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")` and `core::panic!("literal")` behave identically, | ||
[we modify `todo!()`, `unimplemented!()`, `assert_eq!()`, etc.](https://github.com/rust-lang/rust/pull/78343) | ||
to use `$crate::panic!()` instead of `panic!()`. This solves problem 7 for all macros except `assert!()`. | ||
|
||
- We modify `assert!()` to use `$crate::panic!()` instead of `panic!()` 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 of `assert!` 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!` and `panic!` to behave differently depending on the Rust edition of the crate it is used in. | ||
`panic!` is just a `macro_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 than `panic!(non_string_literal)`. | ||
Comment on lines
+184
to
+185
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Trying to do a crater run for this failed before it even started, because that change didn't even get past the Apparently this behaviour of |
||
|
||
[RFC 2795]: https://rust-lang.github.io/rfcs/2795-format-args-implicit-identifiers.html |
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How will this interact with macro expansion? For example, if I have the following code:
what should the result be?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's a good question. We could either look at the edition of the span of
panic
or at the edition of the span of"Weird format: {}"
.I think the former would make most sense. Then the macro doesn't change its behaviour until it is updated to Rust 2021 itself.
What do you think?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That sounds reasonable to me. Note that this will be a 'best effort' approach to preventing breakage. With a somewhat contrived macro, this approach can be made to fail:
That is, a higher-order macro could use the ident
panic
from a 2021 edition crate, causing the new behavior to be used.While this particular macro seems very unrealistic, there are some places in rustc that actually use higher-order macros. Unfortunately, I think we need to accept risk of breakage if this RFC is accepted. However, this would require a crate to be bumped to the 2021 edition, so we will never break a 2018-edition-only crate graph.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That'd still make sense right? You're now passing the macro the 2021 panic, not the 2018 panic. With this style of macro, it's the responsibility of the caller to provide it with the name of a macro that works. Just like
custom_invoke!(println)
also wouldn't compile. So (like you said) the breakage in this example is in the 2021 crate that calls the macro, not in the 2018 one that defines it.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With proc macros, it may not be as clear that
crate2021
is to blame. UsingSpan::resolved_at
, you could get apanic
ident that appears to come from a 2021 crate, even if the literalpanic
never appears in a 2021 crate.To be clear, I woud be shocked if this ever came up in practice. However, I think it would be worth adding an note to the RFC that this is idended to mitigate cross-edition breakage, not prevent it entirely.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@Aaron1011 isn't this true for all edition changes though? If I write a proc-macro that attaches the wrong spans, then it may stop working when a crate using that macro upgrades to a new edition.
Perhaps there should be an exception to the stability rules for bugs that cause code to be tagged with the wrong edition.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's true. However, there have been a bunch of proc-macro API stabilizations since the 2018 edition, so I don't think it's really come up before.