Rollup merge of #122661 - estebank:assert-macro-span, r=petrochenkov

Change the desugaring of `assert!` for better error output

In the desugaring of `assert!`, we now expand to a `match` expression instead of `if !cond {..}`.

The span of incorrect conditions will point only at the expression, and not the whole `assert!` invocation.

```
error[E0308]: mismatched types
  --> $DIR/issue-14091.rs:2:13
   |
LL |     assert!(1,1);
   |             ^ expected `bool`, found integer
```

We no longer mention the expression needing to implement the `Not` trait.

```
error[E0308]: mismatched types
  --> $DIR/issue-14091-2.rs:15:13
   |
LL |     assert!(x, x);
   |             ^ expected `bool`, found `BytePos`
```

Now `assert!(val)` desugars to:

```rust
match val {
    true => {},
    _ => $crate::panic::panic_2021!(),
}
```

Fix #122159.
This commit is contained in:
Stuart Cook
2025-08-15 16:16:29 +10:00
committed by GitHub
15 changed files with 135 additions and 51 deletions

View File

@@ -1,8 +1,8 @@
mod context; mod context;
use rustc_ast::token::Delimiter; use rustc_ast::token::{self, Delimiter};
use rustc_ast::tokenstream::{DelimSpan, TokenStream}; use rustc_ast::tokenstream::{DelimSpan, TokenStream};
use rustc_ast::{DelimArgs, Expr, ExprKind, MacCall, Path, PathSegment, UnOp, token}; use rustc_ast::{DelimArgs, Expr, ExprKind, MacCall, Path, PathSegment};
use rustc_ast_pretty::pprust; use rustc_ast_pretty::pprust;
use rustc_errors::PResult; use rustc_errors::PResult;
use rustc_expand::base::{DummyResult, ExpandResult, ExtCtxt, MacEager, MacroExpanderResult}; use rustc_expand::base::{DummyResult, ExpandResult, ExtCtxt, MacEager, MacroExpanderResult};
@@ -29,7 +29,7 @@ pub(crate) fn expand_assert<'cx>(
// `core::panic` and `std::panic` are different macros, so we use call-site // `core::panic` and `std::panic` are different macros, so we use call-site
// context to pick up whichever is currently in scope. // context to pick up whichever is currently in scope.
let call_site_span = cx.with_call_site_ctxt(span); let call_site_span = cx.with_call_site_ctxt(cond_expr.span);
let panic_path = || { let panic_path = || {
if use_panic_2021(span) { if use_panic_2021(span) {
@@ -63,7 +63,7 @@ pub(crate) fn expand_assert<'cx>(
}), }),
})), })),
); );
expr_if_not(cx, call_site_span, cond_expr, then, None) assert_cond_check(cx, call_site_span, cond_expr, then)
} }
// If `generic_assert` is enabled, generates rich captured outputs // If `generic_assert` is enabled, generates rich captured outputs
// //
@@ -88,26 +88,33 @@ pub(crate) fn expand_assert<'cx>(
)), )),
)], )],
); );
expr_if_not(cx, call_site_span, cond_expr, then, None) assert_cond_check(cx, call_site_span, cond_expr, then)
}; };
ExpandResult::Ready(MacEager::expr(expr)) ExpandResult::Ready(MacEager::expr(expr))
} }
/// `assert!($cond_expr, $custom_message)`
struct Assert { struct Assert {
cond_expr: Box<Expr>, cond_expr: Box<Expr>,
custom_message: Option<TokenStream>, custom_message: Option<TokenStream>,
} }
// if !{ ... } { ... } else { ... } /// `match <cond> { true => {} _ => <then> }`
fn expr_if_not( fn assert_cond_check(cx: &ExtCtxt<'_>, span: Span, cond: Box<Expr>, then: Box<Expr>) -> Box<Expr> {
cx: &ExtCtxt<'_>, // Instead of expanding to `if !<cond> { <then> }`, we expand to
span: Span, // `match <cond> { true => {} _ => <then> }`.
cond: Box<Expr>, // This allows us to always complain about mismatched types instead of "cannot apply unary
then: Box<Expr>, // operator `!` to type `X`" when passing an invalid `<cond>`, while also allowing `<cond>` to
els: Option<Box<Expr>>, // be `&true`.
) -> Box<Expr> { let els = cx.expr_block(cx.block(span, thin_vec![]));
cx.expr_if(span, cx.expr(span, ExprKind::Unary(UnOp::Not, cond)), then, els) let mut arms = thin_vec![];
arms.push(cx.arm(span, cx.pat_lit(span, cx.expr_bool(span, true)), els));
arms.push(cx.arm(span, cx.pat_wild(span), then));
// We wrap the `match` in a statement to limit the length of any borrows introduced in the
// condition.
cx.expr_block(cx.block(span, [cx.stmt_expr(cx.expr_match(span, cond, arms))].into()))
} }
fn parse_assert<'a>(cx: &ExtCtxt<'a>, sp: Span, stream: TokenStream) -> PResult<'a, Assert> { fn parse_assert<'a>(cx: &ExtCtxt<'a>, sp: Span, stream: TokenStream) -> PResult<'a, Assert> {

View File

@@ -1619,8 +1619,18 @@ impl<'a, 'tcx> TypeErrCtxt<'a, 'tcx> {
{ {
let e = self.tcx.erase_regions(e); let e = self.tcx.erase_regions(e);
let f = self.tcx.erase_regions(f); let f = self.tcx.erase_regions(f);
let expected = with_forced_trimmed_paths!(e.sort_string(self.tcx)); let mut expected = with_forced_trimmed_paths!(e.sort_string(self.tcx));
let found = with_forced_trimmed_paths!(f.sort_string(self.tcx)); let mut found = with_forced_trimmed_paths!(f.sort_string(self.tcx));
if let ObligationCauseCode::Pattern { span, .. } = cause.code()
&& let Some(span) = span
&& !span.from_expansion()
&& cause.span.from_expansion()
{
// When the type error comes from a macro like `assert!()`, and we are pointing at
// code the user wrote the cause and effect are reversed as the expected value is
// what the macro expanded to.
(found, expected) = (expected, found);
}
if expected == found { if expected == found {
label_or_note(span, terr.to_string(self.tcx)); label_or_note(span, terr.to_string(self.tcx));
} else { } else {
@@ -2143,7 +2153,9 @@ impl<'a, 'tcx> TypeErrCtxt<'a, 'tcx> {
) -> Option<(DiagStyledString, DiagStyledString)> { ) -> Option<(DiagStyledString, DiagStyledString)> {
match values { match values {
ValuePairs::Regions(exp_found) => self.expected_found_str(exp_found), ValuePairs::Regions(exp_found) => self.expected_found_str(exp_found),
ValuePairs::Terms(exp_found) => self.expected_found_str_term(exp_found, long_ty_path), ValuePairs::Terms(exp_found) => {
self.expected_found_str_term(cause, exp_found, long_ty_path)
}
ValuePairs::Aliases(exp_found) => self.expected_found_str(exp_found), ValuePairs::Aliases(exp_found) => self.expected_found_str(exp_found),
ValuePairs::ExistentialTraitRef(exp_found) => self.expected_found_str(exp_found), ValuePairs::ExistentialTraitRef(exp_found) => self.expected_found_str(exp_found),
ValuePairs::ExistentialProjection(exp_found) => self.expected_found_str(exp_found), ValuePairs::ExistentialProjection(exp_found) => self.expected_found_str(exp_found),
@@ -2182,6 +2194,7 @@ impl<'a, 'tcx> TypeErrCtxt<'a, 'tcx> {
fn expected_found_str_term( fn expected_found_str_term(
&self, &self,
cause: &ObligationCause<'tcx>,
exp_found: ty::error::ExpectedFound<ty::Term<'tcx>>, exp_found: ty::error::ExpectedFound<ty::Term<'tcx>>,
long_ty_path: &mut Option<PathBuf>, long_ty_path: &mut Option<PathBuf>,
) -> Option<(DiagStyledString, DiagStyledString)> { ) -> Option<(DiagStyledString, DiagStyledString)> {
@@ -2189,8 +2202,27 @@ impl<'a, 'tcx> TypeErrCtxt<'a, 'tcx> {
if exp_found.references_error() { if exp_found.references_error() {
return None; return None;
} }
let (mut expected, mut found) = (exp_found.expected, exp_found.found);
Some(match (exp_found.expected.kind(), exp_found.found.kind()) { if let ObligationCauseCode::Pattern { span, .. } = cause.code()
&& let Some(span) = span
&& !span.from_expansion()
&& cause.span.from_expansion()
{
// When the type error comes from a macro like `assert!()`, and we are pointing at
// code the user wrote, the cause and effect are reversed as the expected value is
// what the macro expanded to. So if the user provided a `Type` when the macro is
// written in such a way that a `bool` was expected, we want to print:
// = note: expected `bool`
// found `Type`"
// but as far as the compiler is concerned, after expansion what was expected was `Type`
// = note: expected `Type`
// found `bool`"
// so we reverse them here to match user expectation.
(expected, found) = (found, expected);
}
Some(match (expected.kind(), found.kind()) {
(ty::TermKind::Ty(expected), ty::TermKind::Ty(found)) => { (ty::TermKind::Ty(expected), ty::TermKind::Ty(found)) => {
let (mut exp, mut fnd) = self.cmp(expected, found); let (mut exp, mut fnd) = self.cmp(expected, found);
// Use the terminal width as the basis to determine when to compress the printed // Use the terminal width as the basis to determine when to compress the printed

View File

@@ -11,7 +11,7 @@ use rustc_ast::{BinOpKind, LitKind, RangeLimits};
use rustc_data_structures::packed::Pu128; use rustc_data_structures::packed::Pu128;
use rustc_data_structures::unhash::UnindexMap; use rustc_data_structures::unhash::UnindexMap;
use rustc_errors::{Applicability, Diag}; use rustc_errors::{Applicability, Diag};
use rustc_hir::{Block, Body, Expr, ExprKind, UnOp}; use rustc_hir::{Body, Expr, ExprKind};
use rustc_lint::{LateContext, LateLintPass}; use rustc_lint::{LateContext, LateLintPass};
use rustc_session::declare_lint_pass; use rustc_session::declare_lint_pass;
use rustc_span::source_map::Spanned; use rustc_span::source_map::Spanned;
@@ -135,12 +135,12 @@ fn assert_len_expr<'hir>(
cx: &LateContext<'_>, cx: &LateContext<'_>,
expr: &'hir Expr<'hir>, expr: &'hir Expr<'hir>,
) -> Option<(LengthComparison, usize, &'hir Expr<'hir>)> { ) -> Option<(LengthComparison, usize, &'hir Expr<'hir>)> {
let (cmp, asserted_len, slice_len) = if let Some(higher::If { cond, then, .. }) = higher::If::hir(expr) let (cmp, asserted_len, slice_len) = if let Some(
&& let ExprKind::Unary(UnOp::Not, condition) = &cond.kind higher::IfLetOrMatch::Match(cond, [_, then], _)
&& let ExprKind::Binary(bin_op, left, right) = &condition.kind ) = higher::IfLetOrMatch::parse(cx, expr)
&& let ExprKind::Binary(bin_op, left, right) = &cond.kind
// check if `then` block has a never type expression // check if `then` block has a never type expression
&& let ExprKind::Block(Block { expr: Some(then_expr), .. }, _) = then.kind && cx.typeck_results().expr_ty(then.body).is_never()
&& cx.typeck_results().expr_ty(then_expr).is_never()
{ {
len_comparison(bin_op.node, left, right)? len_comparison(bin_op.node, left, right)?
} else if let Some((macro_call, bin_op)) = first_node_macro_backtrace(cx, expr).find_map(|macro_call| { } else if let Some((macro_call, bin_op)) = first_node_macro_backtrace(cx, expr).find_map(|macro_call| {

View File

@@ -196,6 +196,7 @@ fn issue_13106() {
const { const {
assert!(EMPTY_STR.is_empty()); assert!(EMPTY_STR.is_empty());
//~^ const_is_empty
} }
const { const {

View File

@@ -158,10 +158,16 @@ LL | let _ = val.is_empty();
| ^^^^^^^^^^^^^^ | ^^^^^^^^^^^^^^
error: this expression always evaluates to true error: this expression always evaluates to true
--> tests/ui/const_is_empty.rs:202:9 --> tests/ui/const_is_empty.rs:198:17
|
LL | assert!(EMPTY_STR.is_empty());
| ^^^^^^^^^^^^^^^^^^^^
error: this expression always evaluates to true
--> tests/ui/const_is_empty.rs:203:9
| |
LL | EMPTY_STR.is_empty(); LL | EMPTY_STR.is_empty();
| ^^^^^^^^^^^^^^^^^^^^ | ^^^^^^^^^^^^^^^^^^^^
error: aborting due to 27 previous errors error: aborting due to 28 previous errors

View File

@@ -1,6 +1,6 @@
#![warn(clippy::incompatible_msrv)] #![warn(clippy::incompatible_msrv)]
#![feature(custom_inner_attributes)] #![feature(custom_inner_attributes)]
#![allow(stable_features)] #![allow(stable_features, clippy::diverging_sub_expression)]
#![feature(strict_provenance)] // For use in test #![feature(strict_provenance)] // For use in test
#![clippy::msrv = "1.3.0"] #![clippy::msrv = "1.3.0"]

View File

@@ -1,4 +1,16 @@
fn main() { fn main() {
assert!("foo"); assert!("foo"); //~ ERROR mismatched types
//~^ ERROR cannot apply unary operator `!` //~^ NOTE expected `bool`, found `str`
//~| NOTE in this expansion of assert!
let x = Some(&1);
assert!(x); //~ ERROR mismatched types
//~^ NOTE expected `bool`, found `Option<&{integer}>`
//~| NOTE expected enum `bool`
//~| NOTE in this expansion of assert!
//~| NOTE in this expansion of assert!
assert!(x, ""); //~ ERROR mismatched types
//~^ NOTE expected `bool`, found `Option<&{integer}>`
//~| NOTE expected enum `bool`
//~| NOTE in this expansion of assert!
//~| NOTE in this expansion of assert!
} }

View File

@@ -1,9 +1,27 @@
error[E0600]: cannot apply unary operator `!` to type `&'static str` error[E0308]: mismatched types
--> $DIR/issue-28308.rs:2:5 --> $DIR/issue-28308.rs:2:13
| |
LL | assert!("foo"); LL | assert!("foo");
| ^^^^^^^^^^^^^^ cannot apply unary operator `!` | ^^^^^ expected `bool`, found `str`
error: aborting due to 1 previous error error[E0308]: mismatched types
--> $DIR/issue-28308.rs:6:13
|
LL | assert!(x);
| ^ expected `bool`, found `Option<&{integer}>`
|
= note: expected enum `bool`
found type `Option<&{integer}>`
For more information about this error, try `rustc --explain E0600`. error[E0308]: mismatched types
--> $DIR/issue-28308.rs:11:13
|
LL | assert!(x, "");
| ^ expected `bool`, found `Option<&{integer}>`
|
= note: expected enum `bool`
found type `Option<&{integer}>`
error: aborting due to 3 previous errors
For more information about this error, try `rustc --explain E0308`.

View File

@@ -1,8 +1,8 @@
error[E0080]: evaluation panicked: assertion failed: false error[E0080]: evaluation panicked: assertion failed: false
--> $DIR/assert.rs:5:15 --> $DIR/assert.rs:5:23
| |
LL | const _: () = assert!(false); LL | const _: () = assert!(false);
| ^^^^^^^^^^^^^^ evaluation of `_` failed here | ^^^^^ evaluation of `_` failed here
error: aborting due to 1 previous error error: aborting due to 1 previous error

View File

@@ -1,8 +1,8 @@
error[E0080]: evaluation panicked: assertion failed: std::mem::size_of::<T>() == 0 error[E0080]: evaluation panicked: assertion failed: std::mem::size_of::<T>() == 0
--> $DIR/post_monomorphization_error_backtrace.rs:6:23 --> $DIR/post_monomorphization_error_backtrace.rs:6:31
| |
LL | const V: () = assert!(std::mem::size_of::<T>() == 0); LL | const V: () = assert!(std::mem::size_of::<T>() == 0);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ evaluation of `assert_zst::F::<u32>::V` failed here | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ evaluation of `assert_zst::F::<u32>::V` failed here
note: erroneous constant encountered note: erroneous constant encountered
--> $DIR/post_monomorphization_error_backtrace.rs:14:5 --> $DIR/post_monomorphization_error_backtrace.rs:14:5
@@ -17,10 +17,10 @@ LL | assert_zst::<U>()
| ^^^^^^^^^^^^^^^^^ | ^^^^^^^^^^^^^^^^^
error[E0080]: evaluation panicked: assertion failed: std::mem::size_of::<T>() == 0 error[E0080]: evaluation panicked: assertion failed: std::mem::size_of::<T>() == 0
--> $DIR/post_monomorphization_error_backtrace.rs:6:23 --> $DIR/post_monomorphization_error_backtrace.rs:6:31
| |
LL | const V: () = assert!(std::mem::size_of::<T>() == 0); LL | const V: () = assert!(std::mem::size_of::<T>() == 0);
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ evaluation of `assert_zst::F::<i32>::V` failed here | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ evaluation of `assert_zst::F::<i32>::V` failed here
note: erroneous constant encountered note: erroneous constant encountered
--> $DIR/post_monomorphization_error_backtrace.rs:14:5 --> $DIR/post_monomorphization_error_backtrace.rs:14:5

View File

@@ -1,8 +1,8 @@
error[E0080]: evaluation panicked: assertion failed: std::mem::size_of::<T>() == 0 error[E0080]: evaluation panicked: assertion failed: std::mem::size_of::<T>() == 0
--> $DIR/const-expr-generic-err.rs:4:13 --> $DIR/const-expr-generic-err.rs:4:21
| |
LL | const { assert!(std::mem::size_of::<T>() == 0); } LL | const { assert!(std::mem::size_of::<T>() == 0); }
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ evaluation of `foo::<i32>::{constant#0}` failed here | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ evaluation of `foo::<i32>::{constant#0}` failed here
note: erroneous constant encountered note: erroneous constant encountered
--> $DIR/const-expr-generic-err.rs:4:5 --> $DIR/const-expr-generic-err.rs:4:5

View File

@@ -0,0 +1,8 @@
//@ check-pass
#[derive(PartialEq, Eq, Hash)]
struct S;
fn main() {
let foo = std::rc::Rc::new(std::cell::RefCell::new(std::collections::HashMap::<S, S>::new()));
// Ensure that the lifetimes of the borrow do not leak past the end of `main`.
assert!(matches!(foo.borrow().get(&S).unwrap(), S))
}

View File

@@ -12,7 +12,7 @@ fn main() {
// Test that we can use addr_of! to get the address of a packed member which according to its // Test that we can use addr_of! to get the address of a packed member which according to its
// type is not aligned, but because it is a projection from a packed type is a valid place. // type is not aligned, but because it is a projection from a packed type is a valid place.
let ptr0 = std::ptr::addr_of!(memory[0].tail); let ptr0 = std::ptr::addr_of!(memory[0].tail);
let ptr1 = std::ptr::addr_of!(memory[0].tail); let ptr1 = std::ptr::addr_of!(memory[1].tail);
// Even if ptr0 happens to be aligned by chance, ptr1 is not. // Even if ptr0 happens to be aligned by chance, ptr1 is not.
assert!(!ptr0.is_aligned() || !ptr1.is_aligned()); assert!(!ptr0.is_aligned() || !ptr1.is_aligned());

View File

@@ -1,8 +1,8 @@
error[E0080]: evaluation panicked: assertion failed: LANE < 4 error[E0080]: evaluation panicked: assertion failed: LANE < 4
--> $DIR/const-err-trumps-simd-err.rs:17:13 --> $DIR/const-err-trumps-simd-err.rs:17:21
| |
LL | const { assert!(LANE < 4); } // the error should be here... LL | const { assert!(LANE < 4); } // the error should be here...
| ^^^^^^^^^^^^^^^^^ evaluation of `get_elem::<4>::{constant#0}` failed here | ^^^^^^^^ evaluation of `get_elem::<4>::{constant#0}` failed here
note: erroneous constant encountered note: erroneous constant encountered
--> $DIR/const-err-trumps-simd-err.rs:17:5 --> $DIR/const-err-trumps-simd-err.rs:17:5

View File

@@ -41,10 +41,10 @@ LL | | }>
| |__________^ required by this bound in `is_maybe_transmutable` | |__________^ required by this bound in `is_maybe_transmutable`
error[E0080]: evaluation panicked: assertion failed: false error[E0080]: evaluation panicked: assertion failed: false
--> $DIR/uninhabited.rs:41:9 --> $DIR/uninhabited.rs:41:17
| |
LL | assert!(false); LL | assert!(false);
| ^^^^^^^^^^^^^^ evaluation of `yawning_void_struct::_` failed here | ^^^^^ evaluation of `yawning_void_struct::_` failed here
error[E0277]: `()` cannot be safely transmuted into `yawning_void_enum::Void` error[E0277]: `()` cannot be safely transmuted into `yawning_void_enum::Void`
--> $DIR/uninhabited.rs:71:41 --> $DIR/uninhabited.rs:71:41
@@ -68,10 +68,10 @@ LL | | }>
| |__________^ required by this bound in `is_maybe_transmutable` | |__________^ required by this bound in `is_maybe_transmutable`
error[E0080]: evaluation panicked: assertion failed: false error[E0080]: evaluation panicked: assertion failed: false
--> $DIR/uninhabited.rs:63:9 --> $DIR/uninhabited.rs:63:17
| |
LL | assert!(false); LL | assert!(false);
| ^^^^^^^^^^^^^^ evaluation of `yawning_void_enum::_` failed here | ^^^^^ evaluation of `yawning_void_enum::_` failed here
error[E0277]: `u128` cannot be safely transmuted into `DistantVoid` error[E0277]: `u128` cannot be safely transmuted into `DistantVoid`
--> $DIR/uninhabited.rs:92:43 --> $DIR/uninhabited.rs:92:43
@@ -95,10 +95,10 @@ LL | | }>
| |__________^ required by this bound in `is_maybe_transmutable` | |__________^ required by this bound in `is_maybe_transmutable`
error[E0080]: evaluation panicked: assertion failed: false error[E0080]: evaluation panicked: assertion failed: false
--> $DIR/uninhabited.rs:87:9 --> $DIR/uninhabited.rs:87:17
| |
LL | assert!(false); LL | assert!(false);
| ^^^^^^^^^^^^^^ evaluation of `distant_void::_` failed here | ^^^^^ evaluation of `distant_void::_` failed here
error[E0277]: `Src` cannot be safely transmuted into `issue_126267::Error` error[E0277]: `Src` cannot be safely transmuted into `issue_126267::Error`
--> $DIR/uninhabited.rs:108:42 --> $DIR/uninhabited.rs:108:42