From 0109ed292f8f5b23a067f94df215e62996958b1b Mon Sep 17 00:00:00 2001 From: Jake Drew <25826535+jakedrew@users.noreply.github.com> Date: Wed, 23 Sep 2026 01:14:29 +0200 Subject: [PATCH 01/10] BTree: make dropping an empty BTreeMap free --- library/alloc/src/collections/btree/map.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/library/alloc/src/collections/btree/map.rs b/library/alloc/src/collections/btree/map.rs index e8ee6fa628bfa..9876e1c131122 100644 --- a/library/alloc/src/collections/btree/map.rs +++ b/library/alloc/src/collections/btree/map.rs @@ -205,8 +205,11 @@ pub struct BTreeMap< #[stable(feature = "btree_drop", since = "1.7.0")] unsafe impl<#[may_dangle] K, #[may_dangle] V, A: AllocatorClone> Drop for BTreeMap { fn drop(&mut self) { - // ignore-tidy-undocumented-unsafe - drop(unsafe { ptr::read(self) }.into_iter()) + if self.root.is_some() { + drop(unsafe { ptr::read(self) }.into_iter()) + } else { + unsafe { ManuallyDrop::drop(&mut self.alloc) } + } } } From 756abe8393782352f5badfbee2dcc88334c5f1e2 Mon Sep 17 00:00:00 2001 From: Jake Drew <25826535+jakedrew@users.noreply.github.com> Date: Wed, 23 Sep 2026 01:14:29 +0200 Subject: [PATCH 02/10] Adding codegen for the empty BTree drop --- tests/codegen-llvm/btree-empty-drop.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 tests/codegen-llvm/btree-empty-drop.rs diff --git a/tests/codegen-llvm/btree-empty-drop.rs b/tests/codegen-llvm/btree-empty-drop.rs new file mode 100644 index 0000000000000..4e08920f55dbf --- /dev/null +++ b/tests/codegen-llvm/btree-empty-drop.rs @@ -0,0 +1,15 @@ +// Regression test for https://github.com/rust-lang/rust/issues/161375 +// Checking that an empty BTree's drop is optimized away. +//@ compile-flags: -Copt-level=3 + +#![crate_type = "lib"] + +use std::collections::BTreeMap; + +// CHECK-LABEL: @drop_btree +// CHECK-NOT: dying_next +// CHECK: ret void +#[no_mangle] +pub fn drop_btree() { + let _ = BTreeMap::<(), ()>::new(); +} From 15831b0a64907b45e98f1b55eaa060cb1c368fce Mon Sep 17 00:00:00 2001 From: Jake Drew <25826535+jakedrew@users.noreply.github.com> Date: Wed, 23 Sep 2026 01:14:29 +0200 Subject: [PATCH 03/10] BTree: documenting the unsafe in the empty map drop --- library/alloc/src/collections/btree/map.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/library/alloc/src/collections/btree/map.rs b/library/alloc/src/collections/btree/map.rs index 9876e1c131122..1f479569705f8 100644 --- a/library/alloc/src/collections/btree/map.rs +++ b/library/alloc/src/collections/btree/map.rs @@ -206,8 +206,11 @@ pub struct BTreeMap< unsafe impl<#[may_dangle] K, #[may_dangle] V, A: AllocatorClone> Drop for BTreeMap { fn drop(&mut self) { if self.root.is_some() { + // ignore-tidy-undocumented-unsafe drop(unsafe { ptr::read(self) }.into_iter()) } else { + // SAFETY: With no root there are no nodes to free, so only the allocator needs + // dropping. `self` is not used after this, and `alloc` is dropped only here. unsafe { ManuallyDrop::drop(&mut self.alloc) } } } From 70ff620dc79853f245a9010de8773847e0512bcf Mon Sep 17 00:00:00 2001 From: Jake Drew <25826535+jakedrew@users.noreply.github.com> Date: Wed, 23 Sep 2026 01:14:29 +0200 Subject: [PATCH 04/10] Check for dealloc instead of dying_next --- tests/codegen-llvm/btree-empty-drop.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/codegen-llvm/btree-empty-drop.rs b/tests/codegen-llvm/btree-empty-drop.rs index 4e08920f55dbf..60e3c3b0c9a70 100644 --- a/tests/codegen-llvm/btree-empty-drop.rs +++ b/tests/codegen-llvm/btree-empty-drop.rs @@ -7,7 +7,7 @@ use std::collections::BTreeMap; // CHECK-LABEL: @drop_btree -// CHECK-NOT: dying_next +// CHECK-NOT: dealloc // CHECK: ret void #[no_mangle] pub fn drop_btree() { From 4cf035f6814d2c1f482487e9d671b4fd56a2c38f Mon Sep 17 00:00:00 2001 From: Jake Drew <25826535+jakedrew@users.noreply.github.com> Date: Wed, 23 Sep 2026 10:00:52 +0200 Subject: [PATCH 05/10] Checking that an empty BTreeMap drop compiles down to only a `ret void`, and that function only match drop_btree exactly. As the previous dealloc also passed on old code so didn't catch the regression --- tests/codegen-llvm/btree-empty-drop.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/codegen-llvm/btree-empty-drop.rs b/tests/codegen-llvm/btree-empty-drop.rs index 60e3c3b0c9a70..f1aaf52e95729 100644 --- a/tests/codegen-llvm/btree-empty-drop.rs +++ b/tests/codegen-llvm/btree-empty-drop.rs @@ -6,9 +6,9 @@ use std::collections::BTreeMap; -// CHECK-LABEL: @drop_btree -// CHECK-NOT: dealloc -// CHECK: ret void +// CHECK-LABEL: @drop_btree( +// CHECK-NEXT: {{.*}}: +// CHECK-NEXT: ret void #[no_mangle] pub fn drop_btree() { let _ = BTreeMap::<(), ()>::new(); From 9c77e1eaf7e342ab850b786c9ac61ea3e75aeba1 Mon Sep 17 00:00:00 2001 From: Jake Drew <25826535+jakedrew@users.noreply.github.com> Date: Thu, 24 Sep 2026 01:58:09 +0200 Subject: [PATCH 06/10] Adding safety comment on the unsafe in BTreeMap drop --- library/alloc/src/collections/btree/map.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/library/alloc/src/collections/btree/map.rs b/library/alloc/src/collections/btree/map.rs index 1f479569705f8..fc194639c5468 100644 --- a/library/alloc/src/collections/btree/map.rs +++ b/library/alloc/src/collections/btree/map.rs @@ -206,7 +206,8 @@ pub struct BTreeMap< unsafe impl<#[may_dangle] K, #[may_dangle] V, A: AllocatorClone> Drop for BTreeMap { fn drop(&mut self) { if self.root.is_some() { - // ignore-tidy-undocumented-unsafe + // SAFETY: `self` is not used after this and none of its fields are dropped again: + // `alloc` is `ManuallyDrop` and `root` has no drop glue. drop(unsafe { ptr::read(self) }.into_iter()) } else { // SAFETY: With no root there are no nodes to free, so only the allocator needs From d432494d3808d6002c1afe0130d2fe89b2a447fb Mon Sep 17 00:00:00 2001 From: Jake Drew <25826535+jakedrew@users.noreply.github.com> Date: Fri, 25 Sep 2026 21:32:57 +0200 Subject: [PATCH 07/10] Comment that `dying_next` is too costly to inline --- library/alloc/src/collections/btree/map.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/library/alloc/src/collections/btree/map.rs b/library/alloc/src/collections/btree/map.rs index fc194639c5468..a05345fe45508 100644 --- a/library/alloc/src/collections/btree/map.rs +++ b/library/alloc/src/collections/btree/map.rs @@ -205,6 +205,8 @@ pub struct BTreeMap< #[stable(feature = "btree_drop", since = "1.7.0")] unsafe impl<#[may_dangle] K, #[may_dangle] V, A: AllocatorClone> Drop for BTreeMap { fn drop(&mut self) { + // Skip `into_iter` for an empty map: `dying_next` is too costly to inline, so the + // empty drop isn't optimised away (see #161375). if self.root.is_some() { // SAFETY: `self` is not used after this and none of its fields are dropped again: // `alloc` is `ManuallyDrop` and `root` has no drop glue. From 8d8674117b8f1c3531c0860f3955ee6e816e7c18 Mon Sep 17 00:00:00 2001 From: tye-exe Date: Sat, 26 Sep 2026 08:26:50 +0000 Subject: [PATCH 08/10] Update expect messages in rust_const_eval * Update expect messages In rustc_const_eval/src/interpret/eval_context.rs * Simplify exception messages --- compiler/rustc_const_eval/src/interpret/eval_context.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compiler/rustc_const_eval/src/interpret/eval_context.rs b/compiler/rustc_const_eval/src/interpret/eval_context.rs index 99218a30e16b1..a74b323c395e6 100644 --- a/compiler/rustc_const_eval/src/interpret/eval_context.rs +++ b/compiler/rustc_const_eval/src/interpret/eval_context.rs @@ -285,12 +285,12 @@ impl<'tcx, M: Machine<'tcx>> InterpCx<'tcx, M> { #[inline(always)] pub fn frame(&self) -> &Frame<'tcx, M::Provenance, M::FrameExtra> { - self.stack().last().expect("no call frames exist") + self.stack().last().expect("a call frame should exist") } #[inline(always)] pub fn frame_mut(&mut self) -> &mut Frame<'tcx, M::Provenance, M::FrameExtra> { - self.stack_mut().last_mut().expect("no call frames exist") + self.stack_mut().last_mut().expect("a call frame should exist") } #[inline(always)] From a0de97917cf55252e6bd8506324b809386e439e9 Mon Sep 17 00:00:00 2001 From: albab-hasan Date: Tue, 25 Aug 2026 14:36:20 +0600 Subject: [PATCH 09/10] dont lint unused parens on a pattern that came from a macro argument parentheses around a pattern passed to a `macro_rules!` metavariable can be required by the matcher even though they are redundant once expanded, so suggesting their removal breaks the macro call. a `$p:pat_param` fragment never accepts a top-level `|` and neither does `$p:pat` before edition 2021. a metavariable keeps the call site syntax context while the code around it keeps the macro definition context, so compare the two and reuse the existing `avoid_or` flag when they differ. parens written inside a macro body share one context and keep linting as before. --- compiler/rustc_lint/src/unused.rs | 22 ++++++-- .../unused-parens-macro-pat-issue-86959.rs | 54 +++++++++++++++++++ 2 files changed, 71 insertions(+), 5 deletions(-) create mode 100644 tests/ui/lint/unused-parens-macro-pat-issue-86959.rs diff --git a/compiler/rustc_lint/src/unused.rs b/compiler/rustc_lint/src/unused.rs index 2651ce55281fe..e2dff59a92990 100644 --- a/compiler/rustc_lint/src/unused.rs +++ b/compiler/rustc_lint/src/unused.rs @@ -783,6 +783,14 @@ impl UnusedParens { } } + /// A pattern that a `macro_rules!` metavariable brings in from the call site keeps the call + /// site's syntax context, while the pattern's surroundings keep the macro's. Parentheses in + /// such a pattern can be required by the matcher even when they are redundant in the + /// expansion, so removing them would break the macro call. (Issue #86959) + fn pat_is_macro_arg(pat_span: Span, parent_span: Span) -> bool { + !pat_span.eq_ctxt(parent_span) + } + fn cast_followed_by_lt(&self, expr: &ast::Expr) -> Option { if let ExprKind::Binary(op, lhs, _rhs) = &expr.kind && (op.node == ast::BinOpKind::Lt || op.node == ast::BinOpKind::Shl) @@ -811,7 +819,8 @@ impl EarlyLintPass for UnusedParens { match e.kind { ExprKind::Let(ref pat, _, _, _) | ExprKind::ForLoop(ForLoop { ref pat, .. }) => { - self.check_unused_parens_pat(cx, pat, false, false, (true, true)); + let avoid_or = Self::pat_is_macro_arg(pat.span, e.span); + self.check_unused_parens_pat(cx, pat, avoid_or, false, (true, true)); } // We ignore parens in cases like `if (((let Some(0) = Some(1))))` because we already // handle a hard error for them during AST lowering in `lower_expr_mut`, but we still @@ -890,13 +899,15 @@ impl EarlyLintPass for UnusedParens { | Err(_) => {} // These are list-like patterns; parens can always be removed. TupleStruct(_, _, ps) | Tuple(ps) | Slice(ps) | Or(ps) => { - for p in ps { - self.check_unused_parens_pat(cx, p, false, false, keep_space); + for sub in ps { + let avoid_or = Self::pat_is_macro_arg(sub.span, p.span); + self.check_unused_parens_pat(cx, sub, avoid_or, false, keep_space); } } Struct(_, _, fps, _) => { for f in fps { - self.check_unused_parens_pat(cx, &f.pat, false, false, keep_space); + let avoid_or = Self::pat_is_macro_arg(f.pat.span, p.span); + self.check_unused_parens_pat(cx, &f.pat, avoid_or, false, keep_space); } } // Avoid linting on `i @ (p0 | .. | pn)`, #64106. @@ -938,7 +949,8 @@ impl EarlyLintPass for UnusedParens { } fn check_arm(&mut self, cx: &EarlyContext<'_>, arm: &ast::Arm) { - self.check_unused_parens_pat(cx, &arm.pat, false, false, (false, false)); + let avoid_or = Self::pat_is_macro_arg(arm.pat.span, arm.span); + self.check_unused_parens_pat(cx, &arm.pat, avoid_or, false, (false, false)); } fn check_ty(&mut self, cx: &EarlyContext<'_>, ty: &ast::Ty) { diff --git a/tests/ui/lint/unused-parens-macro-pat-issue-86959.rs b/tests/ui/lint/unused-parens-macro-pat-issue-86959.rs new file mode 100644 index 0000000000000..3e41cb22c5aad --- /dev/null +++ b/tests/ui/lint/unused-parens-macro-pat-issue-86959.rs @@ -0,0 +1,54 @@ +//@ check-pass +//@ revisions: e2018 e2021 +//@[e2018] edition: 2018 +//@[e2021] edition: 2021 + +// Parentheses around a pattern handed to a `macro_rules!` metavariable can be required by the +// matcher even though they are redundant in the expanded pattern, so removing them breaks the +// macro call. rust-lang/rust#86959 + +#![deny(unused_parens)] + +macro_rules! match_pat { + ($p:pat) => { + match 'a' { + $p => {} + _ => {} + } + }; +} + +macro_rules! match_pat_param { + ($p:pat_param) => { + match 'a' { + $p => {} + _ => {} + } + }; +} + +macro_rules! if_let_pat { + ($p:pat) => { + if let $p = 'a' {} + }; +} + +macro_rules! nested_pat { + ($p:pat) => { + match Some(1) { + Some($p) => {} + _ => {} + } + }; +} + +fn main() { + // `$p:pat` only accepts a top-level `|` from edition 2021 onwards. + match_pat!(('a' | 'A')); + // `$p:pat_param` never accepts a top-level `|`, in any edition. + match_pat_param!(('a' | 'A')); + if_let_pat!(('a' | 'A')); + nested_pat!((1 | 2)); + // A leading `..=` is rejected by both fragment specifiers. rust-lang/rust#120737 + match_pat!((..='a' | 'z')); +} From ff6341671f2ae918cd819c14d7d981a38c56911f Mon Sep 17 00:00:00 2001 From: albab-hasan Date: Sat, 19 Sep 2026 12:15:24 +0600 Subject: [PATCH 10/10] add tests that the lint still fires outside the macro argument case --- ...-parens-macro-pat-issue-86959.e2018.stderr | 42 +++++++++++++++++++ ...-parens-macro-pat-issue-86959.e2021.stderr | 42 +++++++++++++++++++ .../unused-parens-macro-pat-issue-86959.rs | 29 ++++++++++++- 3 files changed, 112 insertions(+), 1 deletion(-) create mode 100644 tests/ui/lint/unused-parens-macro-pat-issue-86959.e2018.stderr create mode 100644 tests/ui/lint/unused-parens-macro-pat-issue-86959.e2021.stderr diff --git a/tests/ui/lint/unused-parens-macro-pat-issue-86959.e2018.stderr b/tests/ui/lint/unused-parens-macro-pat-issue-86959.e2018.stderr new file mode 100644 index 0000000000000..4a04b5b3d1fe2 --- /dev/null +++ b/tests/ui/lint/unused-parens-macro-pat-issue-86959.e2018.stderr @@ -0,0 +1,42 @@ +error: unnecessary parentheses around pattern + --> $DIR/unused-parens-macro-pat-issue-86959.rs:76:16 + | +LL | match_pat!(('a')); + | ^ ^ + | +note: the lint level is defined here + --> $DIR/unused-parens-macro-pat-issue-86959.rs:9:9 + | +LL | #![deny(unused_parens)] + | ^^^^^^^^^^^^^ +help: remove these parentheses + | +LL - match_pat!(('a')); +LL + match_pat!('a'); + | + +error: unnecessary parentheses around pattern + --> $DIR/unused-parens-macro-pat-issue-86959.rs:54:13 + | +LL | macro_rules! body_or_pat { +... +LL | ('a' | 'A') => {} + | ^^^^^^^^^^^ +... +LL | body_or_pat!(); + | -------------- in this macro invocation + +error: unnecessary parentheses around pattern + --> $DIR/unused-parens-macro-pat-issue-86959.rs:80:9 + | +LL | let (_x) = 1; + | ^ ^ + | +help: remove these parentheses + | +LL - let (_x) = 1; +LL + let _x = 1; + | + +error: aborting due to 3 previous errors + diff --git a/tests/ui/lint/unused-parens-macro-pat-issue-86959.e2021.stderr b/tests/ui/lint/unused-parens-macro-pat-issue-86959.e2021.stderr new file mode 100644 index 0000000000000..4a04b5b3d1fe2 --- /dev/null +++ b/tests/ui/lint/unused-parens-macro-pat-issue-86959.e2021.stderr @@ -0,0 +1,42 @@ +error: unnecessary parentheses around pattern + --> $DIR/unused-parens-macro-pat-issue-86959.rs:76:16 + | +LL | match_pat!(('a')); + | ^ ^ + | +note: the lint level is defined here + --> $DIR/unused-parens-macro-pat-issue-86959.rs:9:9 + | +LL | #![deny(unused_parens)] + | ^^^^^^^^^^^^^ +help: remove these parentheses + | +LL - match_pat!(('a')); +LL + match_pat!('a'); + | + +error: unnecessary parentheses around pattern + --> $DIR/unused-parens-macro-pat-issue-86959.rs:54:13 + | +LL | macro_rules! body_or_pat { +... +LL | ('a' | 'A') => {} + | ^^^^^^^^^^^ +... +LL | body_or_pat!(); + | -------------- in this macro invocation + +error: unnecessary parentheses around pattern + --> $DIR/unused-parens-macro-pat-issue-86959.rs:80:9 + | +LL | let (_x) = 1; + | ^ ^ + | +help: remove these parentheses + | +LL - let (_x) = 1; +LL + let _x = 1; + | + +error: aborting due to 3 previous errors + diff --git a/tests/ui/lint/unused-parens-macro-pat-issue-86959.rs b/tests/ui/lint/unused-parens-macro-pat-issue-86959.rs index 3e41cb22c5aad..9db969f4635cd 100644 --- a/tests/ui/lint/unused-parens-macro-pat-issue-86959.rs +++ b/tests/ui/lint/unused-parens-macro-pat-issue-86959.rs @@ -1,4 +1,3 @@ -//@ check-pass //@ revisions: e2018 e2021 //@[e2018] edition: 2018 //@[e2021] edition: 2021 @@ -42,6 +41,22 @@ macro_rules! nested_pat { }; } +// `body_or_pat!()` breaks the context leg. its a `PatKind::Or`, but the parens and the arm +// both come from the macro body so they share one `SyntaxContext`, `eq_ctxt` is true and +// `avoid_or` is false. these are the macro authors own parens, no metavariable involved, so +// removing them is safe. this is the case that matters most: `from_expansion()` is true here +// and `!eq_ctxt` is false, so if the gate were ever keyed on "are we inside a macro" instead +// of "did this cross a context boundary" it would be wrongly silenced. nothing else in the +// file detects that. +macro_rules! body_or_pat { + () => { + match 'a' { + ('a' | 'A') => {} //~ ERROR unnecessary parentheses around pattern + _ => {} + } + }; +} + fn main() { // `$p:pat` only accepts a top-level `|` from edition 2021 onwards. match_pat!(('a' | 'A')); @@ -51,4 +66,16 @@ fn main() { nested_pat!((1 | 2)); // A leading `..=` is rejected by both fragment specifiers. rust-lang/rust#120737 match_pat!((..='a' | 'z')); + + // `match_pat!(('a'))` breaks the or leg. `avoid_or` is true, since the pattern arrives + // through `$p:pat` and its `SyntaxContext` differs from the arms. but the inner pattern is + // `PatKind::Expr`, not `PatKind::Or`, so `unused.rs:676` never matches and the lint fires. + // the suggestion rewrites the call site to `match_pat!('a')`, which is right — `$p:pat` + // takes a bare literal. this guards against widening the gate to all patterns, or moving + // the `avoid_or` check above the `match inner.kind`. + match_pat!(('a')); //~ ERROR unnecessary parentheses around pattern + body_or_pat!(); + // `let (_x) = 1;` is the no-macro baseline for the `check_expr` call site, one of the four + // that went from a hardcoded `false` to a computed value. + let (_x) = 1; //~ ERROR unnecessary parentheses around pattern }