Repository navigation
assert_eq/assert_matches vs debug_assert_eq/debug_assert_matches have different temporary scoping behavior #154406
Description
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
dianne commentedon Mar 26, 2026
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
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...? ↩
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 ↩
Mark-Simulacrum commentedon Apr 8, 2026
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
Auto merge of #154999 - cuviper:beta-next, r=cuviper
Auto merge of #154999 - cuviper:beta-next, r=cuviper
Amanieu commentedon Apr 14, 2026
This was discussed by the @rust-lang/libs-api team and we agree that both
assert_eqandassert_matchesshould introduce a temporary scope. We've already de-stabilizedassert_matchesfor 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.refactor: fix `collapsable_if` lint, rust 1.95 `assert_matches` desta…
Add temporary scope to assert_matches
Rollup merge of rust-lang#155431 - Voultapher:add-tmp-scope-to-assert…
Rollup merge of rust-lang#155431 - Voultapher:add-tmp-scope-to-assert…
Rollup merge of rust-lang#155431 - Voultapher:add-tmp-scope-to-assert…
Rollup merge of rust-lang#155431 - Voultapher:add-tmp-scope-to-assert…
Unrolled build for #155431
Rollup merge of rust-lang#155943 - romancitodev:fix/version-mismatch,…
Add temporary scope to assert_matches
refactor: fix `collapsable_if` lint, rust 1.95 `assert_matches` desta…
theemathas commentedon Jul 29, 2026
Fixed by #155431 and #155739.
Upgrade/blueos 1.96 (#17)