Skip to content
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 3 commits into from
Jan 13, 2021
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
187 changes: 187 additions & 0 deletions text/0000-panic-plan.md
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.
Copy link
Member

@Aaron1011 Aaron1011 Oct 26, 2020

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:

// crate2018 (2018 edition):

macro_rules! custom_panic {
    ($($arg:tt),*) => {
        panic!($($arg),*)
    }
}

fn foo() {
	custom_panic!("Weird format: {}");
}


// crate2021 (2021 edition)

fn bar() {
	crate2018::custom_panic!("Weird format: {}");
}

what should the result be?

Copy link
Member Author

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?

Copy link
Member

@Aaron1011 Aaron1011 Oct 26, 2020

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:

macro_rules! custom_invoke {
    ($name:ident) => {
        $name!("My panic: {}")
    }
}

fn main() {
    custom_invoke!(panic)
}

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.

Copy link
Member Author

@m-ou-se m-ou-se Oct 26, 2020

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.

Copy link
Member

@Aaron1011 Aaron1011 Oct 26, 2020

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. Using Span::resolved_at, you could get a panic ident that appears to come from a 2021 crate, even if the literal panic 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.

Copy link
Contributor

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.

Copy link
Member

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.

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
Copy link
Member Author

Choose a reason for hiding this comment

The 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 @bors try run. It caused a compilation error while compiling this part of clap:

https://github.com/clap-rs/clap/blob/0c7da9f5b32bcd6968a70258a4868d439fbc1fc3/src/app/parser.rs#L181-L184

Apparently this behaviour of assert!() is relied upon.


[RFC 2795]: https://rust-lang.github.io/rfcs/2795-format-args-implicit-identifiers.html