Repository: rust-lang/rust
Pull request: Parenthesize or-patterns in prefix pattern positions in pretty printer

The AST pretty printer was dropping parentheses around or-patterns when they appeared inside `@` bindings, `&` references, or `box` patterns. For example:

- `v @ (1 | 2 | 3)` was printed as `v @ 1 | 2 | 3`
- `&(1 | 2 | 3)` was printed as `&1 | 2 | 3`
- `box (1 | 2 | 3)` was printed as `box 1 | 2 | 3`

Since `|` has the lowest precedence among pattern operators, all of these are parsed incorrectly without parentheses — e.g. `v @ 1 | 2 | 3` becomes `(v @ 1) | 2 | 3`, binding `v` only to the first alternative.

This caused E0408 ("variable not bound in all patterns") when the expanded output was fed back to the compiler, affecting crates like html5ever and wgpu-core that use macros expanding to or-patterns after `@`.

The fix adds a `print_pat_paren_if_or` helper that wraps `PatKind::Or` subpatterns in parentheses, and uses it in the `@`, `&`, and `box` printing arms. This is similar in spirit to the existing `FixupContext` parenthesization approach used for expression printing.

## Example

**before** (`rustc 1.96.0-nightly (3b1b0ef4d 2026-03-11)`):

```rust
#![feature(prelude_import)]
#![no_std]
#![feature(box_patterns)]
extern crate std;
#[prelude_import]
use ::std::prelude::rust_2015::*;

//@ pretty-compare-only
//@ pretty-mode:expanded
//@ pp-exact:or-pattern-paren.pp

macro_rules! or_pat { ($($name:pat),+) => { $($name)|+ } }

fn check_at(x: Option<i32>) {
    match x {
        Some(v @ 1 | 2 | 3) =>


            {
            ::std::io::_print(format_args!("{0}\n", v));
        }
        _ => {}
    }
}
fn check_ref(x: &i32) { match x { &1 | 2 | 3 => {} _ => {} } }
fn check_box(x: Box<i32>) { match x { box 1 | 2 | 3 => {} _ => {} } }
fn main() { check_at(Some(2)); check_ref(&1); check_box(Box::new(1)); }
```

**after** (this branch):

```rust
#![feature(prelude_import)]
#![no_std]
#![feature(box_patterns)]
extern crate std;
#[prelude_import]
use ::std::prelude::rust_2015::*;

//@ pretty-compare-only
//@ pretty-mode:expanded
//@ pp-exact:or-pattern-paren.pp

macro_rules! or_pat { ($($name:pat),+) => { $($name)|+ } }

fn check_at(x: Option<i32>) {
    match x {
        Some(v @ (1 | 2 | 3)) =>


            {
            ::std::io::_print(format_args!("{0}\n", v));
        }
        _ => {}
    }
}
fn check_ref(x: &i32) { match x { &(1 | 2 | 3) => {} _ => {} } }
fn check_box(x: Box<i32>) { match x { box (1 | 2 | 3) => {} _ => {} } }
fn main() { check_at(Some(2)); check_ref(&1); check_box(Box::new(1)); }
```

Notice `v @ 1 | 2 | 3` becomes `v @ (1 | 2 | 3)`, `&1 | 2 | 3` becomes `&(1 | 2 | 3)`, and `box 1 | 2 | 3` becomes `box (1 | 2 | 3)`. Without parens, the or-pattern binds incorrectly — only the first alternative gets the `@`/`&`/`box`, causing E0408.

<!-- homu-ignore:start -->
<!--
If this PR is related to an unstable feature or an otherwise tracked effort,
please link to the relevant tracking issue here. If you don't know of a related
tracking issue or there are none, feel free to ignore this.

This PR will get automatically assigned to a reviewer. In case you would like
a specific user to review your work, you can assign it to them by using

    r? <reviewer name>
-->
<!-- homu-ignore:end -->


Changed files:
filename: compiler/rustc_ast_pretty/src/pprust/state.rs
patch: @@ -1786,6 +1786,23 @@ impl<'a> State<'a> {
         }
     }
 
+    /// Print a pattern, parenthesizing it if it is an or-pattern (`A | B`).
+    ///
+    /// Or-patterns have the lowest precedence among patterns, so they need
+    /// parentheses when nested inside `@` bindings, `&` references, or `box`
+    /// patterns — otherwise `x @ A | B` parses as `(x @ A) | B`, `&A | B`
+    /// parses as `(&A) | B`, etc.
+    fn print_pat_paren_if_or(&mut self, pat: &ast::Pat) {
+        let needs_paren = matches!(pat.kind, PatKind::Or(..));
+        if needs_paren {
+            self.popen();
+        }
+        self.print_pat(pat);
+        if needs_paren {
+            self.pclose();
+        }
+    }
+
     fn print_pat(&mut self, pat: &ast::Pat) {
         self.maybe_print_comment(pat.span.lo());
         self.ann.pre(self, AnnNode::Pat(pat));
@@ -1813,7 +1830,7 @@ impl<'a> State<'a> {
                 if let Some(p) = sub {
                     self.space();
                     self.word_space("@");
-                    self.print_pat(p);
+                    self.print_pat_paren_if_or(p);
                 }
             }
             PatKind::TupleStruct(qself, path, elts) => {
@@ -1885,7 +1902,7 @@ impl<'a> State<'a> {
             }
             PatKind::Box(inner) => {
                 self.word("box ");
-                self.print_pat(inner);
+                self.print_pat_paren_if_or(inner);
             }
             PatKind::Deref(inner) => {
                 self.word("deref!");
@@ -1909,7 +1926,7 @@ impl<'a> State<'a> {
                     self.print_pat(inner);
                     self.pclose();
                 } else {
-                    self.print_pat(inner);
+                    self.print_pat_paren_if_or(inner);
                 }
             }
             PatKind::Expr(e) => self.print_expr(e, FixupContext::default()),

filename: src/tools/clippy/tests/ui/unnested_or_patterns2.fixed
patch: @@ -23,6 +23,6 @@ fn main() {
     //~^ unnested_or_patterns
     if let box (0 | 1 | 2 | 3 | 4) = Box::new(0) {}
     //~^ unnested_or_patterns
-    if let box box (0 | 2 | 4) = Box::new(Box::new(0)) {}
+    if let box (box (0 | 2 | 4)) = Box::new(Box::new(0)) {}
     //~^ unnested_or_patterns
 }

filename: src/tools/clippy/tests/ui/unnested_or_patterns2.stderr
patch: @@ -93,7 +93,7 @@ LL |     if let box box 0 | box (box 2 | box 4) = Box::new(Box::new(0)) {}
 help: nest the patterns
    |
 LL -     if let box box 0 | box (box 2 | box 4) = Box::new(Box::new(0)) {}
-LL +     if let box box (0 | 2 | 4) = Box::new(Box::new(0)) {}
+LL +     if let box (box (0 | 2 | 4)) = Box::new(Box::new(0)) {}
    |
 
 error: aborting due to 8 previous errors

filename: tests/pretty/or-pattern-paren.pp
patch: @@ -0,0 +1,27 @@
+#![feature(prelude_import)]
+#![no_std]
+#![feature(box_patterns)]
+extern crate std;
+#[prelude_import]
+use ::std::prelude::rust_2015::*;
+
+//@ pretty-compare-only
+//@ pretty-mode:expanded
+//@ pp-exact:or-pattern-paren.pp
+
+macro_rules! or_pat { ($($name:pat),+) => { $($name)|+ } }
+
+fn check_at(x: Option<i32>) {
+    match x {
+        Some(v @ (1 | 2 | 3)) =>
+
+
+            {
+            ::std::io::_print(format_args!("{0}\n", v));
+        }
+        _ => {}
+    }
+}
+fn check_ref(x: &i32) { match x { &(1 | 2 | 3) => {} _ => {} } }
+fn check_box(x: Box<i32>) { match x { box (1 | 2 | 3) => {} _ => {} } }
+fn main() { check_at(Some(2)); check_ref(&1); check_box(Box::new(1)); }

filename: tests/pretty/or-pattern-paren.rs
patch: @@ -0,0 +1,36 @@
+#![feature(box_patterns)]
+
+//@ pretty-compare-only
+//@ pretty-mode:expanded
+//@ pp-exact:or-pattern-paren.pp
+
+macro_rules! or_pat {
+    ($($name:pat),+) => { $($name)|+ }
+}
+
+fn check_at(x: Option<i32>) {
+    match x {
+        Some(v @ or_pat!(1, 2, 3)) => println!("{v}"),
+        _ => {}
+    }
+}
+
+fn check_ref(x: &i32) {
+    match x {
+        &or_pat!(1, 2, 3) => {}
+        _ => {}
+    }
+}
+
+fn check_box(x: Box<i32>) {
+    match x {
+        box or_pat!(1, 2, 3) => {}
+        _ => {}
+    }
+}
+
+fn main() {
+    check_at(Some(2));
+    check_ref(&1);
+    check_box(Box::new(1));
+}