Skip to content

assert_eq/assert_matches vs debug_assert_eq/debug_assert_matches have different temporary scoping behavior #154406

@theemathas

Description

@theemathas

I tried this code:

use std::{assert_matches, debug_assert_matches};

#[derive(Debug)]
struct LoudDrop;
impl Drop for LoudDrop {
    fn drop(&mut self) {
        println!("drop");
    }
}

fn make() -> LoudDrop {
    LoudDrop
}

fn discard<T>(value: T, _: &LoudDrop) -> T {
    value
}

fn main() {
    println!("assert_eq");
    (assert_eq!((), discard((), &make())), println!("after"));
    println!();
    println!("debug_assert_eq");
    (debug_assert_eq!((), discard((), &make())), println!("after"));
    println!();
    println!("assert_matches");
    (assert_matches!(discard((), &make()), ()), println!("after"));
    println!();
    println!("debug_assert_matches");
    (debug_assert_matches!(discard((), &make()), ()), println!("after"));
    println!();
    println!("assert");
    (assert!(discard(true, &make())), println!("after"));
    println!();
    println!("debug_assert");
    (debug_assert!(discard(true, &make())), println!("after"));
}

I expected the temporary scopes to be the same regardless of whether I use the debug_ variant of the macros or not. Instead, the code outputs the following:

assert_eq
after
drop

debug_assert_eq
drop
after

assert_matches
after
drop

debug_assert_matches
drop
after

assert
drop
after

debug_assert
drop
after

That is, debug_assert_eq, debug_assert_matches, assert, and debug_assert each introduces a temporary scope, causing temporaries to be dropped as soon as execution of the macro call finishes. However, assert_eq and assert_matches each does not introduce a temporary scope, causing temporaries to only be dropped at the next surrounding temporary scope, potentially after other code has executed outside the macro call. This seems inconsistent, although I'm not sure if this could affect real code in practice.

(Note: assert_matches and debug_assert_matches are stable in 1.95.0 beta.)

The reason assert_eq does not introduce a temporary scope is because, assert_eq!(expr1, expr2) expands to match (&expr1, &expr2) { .... }. And match does not introduce a temporary scope for the scrutinee. Similarly, assert_matches!(expr, pat) expands to match expr { .... }.

In contrast, assert!(expr) expands to if !expr { .... }. And if introduces a temporary scope for the condition expression.

The reference documents this difference in behavior between match and if.

The reason debug_assert_eq introduces a temporary scope is because, debug_assert_eq!(expr1, expr2) expands to if cfg!(debug_assertions) { assert_eq!(expr1, expr2); }. And the semicolon introduces a temporary scope.

cc @dianne

Meta

Reproducible on the playground with version 1.96.0-nightly (2026-03-24 362211dc29abc4e8f8cf)

Metadata

Metadata

Assignees

No one assigned

    Labels

    A-destructorsArea: Destructors (`Drop`, …)A-macrosArea: All kinds of macros (custom derive, macro_rules!, proc macros, ..)A-temporary-lifetime-extensionArea: temporary lifetime extensionC-bugCategory: This is a bug.T-libs-apiRelevant to the library API team, which will review and decide on the PR/issue.

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions