Skip to content

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

Closed

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)

Activity

  1. added
    A-destructorsArea: Destructors (`Drop`, …)
    A-macrosArea: All kinds of macros (custom derive, macro_rules!, proc macros, ..)
    T-libs-api[DEPRECATED; DO NOT USE]
    C-bugCategory: This is a bug.
    on Mar 26, 2026
  2. added
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on Mar 26, 2026
  3. removed
    needs-triageThis issue may need triage. Remove when done. See docs forge.rust-lang.org/release/issue-triaging
    on Mar 26, 2026
  4. dianne commented on Mar 26, 2026

    @dianne
    Member

    I think it'd be most reasonable for all of those macros to introduce temporary scopes. I can't speak for T-libs-api, but my impression is that standard library macros generally want to drop arguments' temporaries when they can't be borrowed in the result value of the macro; I think the same is true of other printing macros (other than write! currently; cf #99684).

    I'd also be pretty surprised if this affected real code. I think that'd require using the assert within some larger expression that either uses a temporary from the assert's arguments1 or is affected by whether a temporary from the assert's arguments has been dropped yet2.

    Footnotes

    1. e.g. by assigning it to a variable within the assert, then using the variable outside of the assert; this could theoretically break from introducing temporary scopes, but would anyone ever do that...? ↩

    2. but my impression is that these drop-relative-to-execution-order things are more likely to break from larger temporary scopes, not when making them smaller ↩

  5. Mark-Simulacrum commented on Apr 8, 2026

    @Mark-Simulacrum
    Member

    Nominating in case we want to hold off on stabilizing assert_matches (or, really, revert that in beta so we have at least 6 weeks to discuss) while we figure this out.

    cc Zulip thread since we're relatively late in the process, https://rust-lang.zulipchat.com/#narrow/channel/219381-t-libs/topic/temporary.20scoping.20in.20assert_matches

  6. added 2 commits that reference this issue on Apr 9, 2026
  7. added this to the 1.96.0 milestone on Apr 12, 2026
  8. Amanieu commented on Apr 14, 2026

    @Amanieu
    Member

    This was discussed by the @rust-lang/libs-api team and we agree that both assert_eq and assert_matches should introduce a temporary scope. We've already de-stabilized assert_matches for 1.95 and we're just waiting for a PR to change the behavior that we can then backport into 1.96 (beta).

    For assert_eq, we would like to see a separate PR on which we can do a crater run to ensure this doesn't break anything. This may need to be a test run to catch case where a lock is acquired in an assert, which can impact behavior.

  9. added 4 commits that reference this issue on Apr 19, 2026
  10. added a commit that references this issue on Apr 19, 2026
  11. added a commit that references this issue on Apr 19, 2026
  12. added a commit that references this issue on Apr 29, 2026
  13. added a commit that references this issue on Apr 30, 2026
  14. theemathas commented on Jul 29, 2026

    @theemathas
    ContributorAuthor

    Fixed by #155431 and #155739.

  15. added a commit that references this issue on Oct 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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-api[DEPRECATED; DO NOT USE]

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions