Repository: rust-lang/rust
Pull request: Fix whitespace after fragment specifiers in macro pretty printing

When a macro-generating-macro captures fragment specifier tokens (like `$x:ident`) as `tt` metavariables and replays them before a keyword (like `where`), the pretty printer concatenates them into an invalid fragment specifier (e.g. `$x:identwhere` instead of `$x:ident where`).

This happens because `tt` captures preserve the original token spacing. When the fragment specifier name (e.g. `ident`) was originally the last token before a closing delimiter, it retains `JointHidden` spacing. The `print_tts` function only checks `space_between` for `Spacing::Alone` tokens, so `JointHidden` tokens skip the space check entirely, causing adjacent identifier-like tokens to merge.

The fix adds a check in `print_tts` to insert a space between adjacent identifier-like tokens (identifiers and keywords) regardless of the original spacing, preventing them from being concatenated into invalid tokens.

This is similar to the existing `space_between` mechanism that prevents token merging for `Spacing::Alone` tokens, extended to also handle `Joint`/`JointHidden` cases where two identifier-like tokens would merge.

## Example

**before** (`rustc 1.96.0-nightly (3b1b0ef4d 2026-03-11)`):

```rust
#![feature(prelude_import)]
#![no_std]
extern crate std;
#[prelude_import]
use ::std::prelude::rust_2015::*;
//@ pretty-mode:expanded
//@ pp-exact:macro-fragment-specifier-whitespace.pp

// Test that fragment specifier names in macro definitions are properly
// separated from the following keyword/identifier token when pretty-printed.
// This is a regression test for a bug where `$x:ident` followed by `where`
// was pretty-printed as `$x:identwhere` (an invalid fragment specifier).

macro_rules! outer {
    ($d:tt $($params:tt)*) =>
    {
        #[macro_export] macro_rules! inner
        { ($($params)* where $d($rest:tt)*) => {}; }
    };
}
#[macro_export]
macro_rules! inner { ($x:identwhere $ ($rest : tt)*) => {}; }

fn main() {}
```

**after** (this branch):

```rust
#![feature(prelude_import)]
#![no_std]
extern crate std;
#[prelude_import]
use ::std::prelude::rust_2015::*;
//@ pretty-mode:expanded
//@ pp-exact:macro-fragment-specifier-whitespace.pp

// Test that fragment specifier names in macro definitions are properly
// separated from the following keyword/identifier token when pretty-printed.
// This is a regression test for a bug where `$x:ident` followed by `where`
// was pretty-printed as `$x:identwhere` (an invalid fragment specifier).

macro_rules! outer {
    ($d:tt $($params:tt)*) =>
    {
        #[macro_export] macro_rules! inner
        { ($($params)* where $d($rest:tt)*) => {}; }
    };
}
#[macro_export]
macro_rules! inner { ($x:ident where $ ($rest : tt)*) => {}; }

fn main() {}
```

Notice the `$x:identwhere` in the before — an invalid fragment specifier that causes a hard parse error. The after correctly separates it as `$x:ident where`.

<!-- 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: @@ -329,6 +329,19 @@ fn print_crate_inner<'a>(
 /// - #63896: `#[allow(unused,` must be printed rather than `#[allow(unused ,`
 /// - #73345: `#[allow(unused)]` must be printed rather than `# [allow(unused)]`
 ///
+/// Returns `true` if both token trees are identifier-like tokens that would
+/// merge into a single token if printed without a space between them.
+/// E.g. `ident` + `where` would merge into `identwhere`.
+fn idents_would_merge(tt1: &TokenTree, tt2: &TokenTree) -> bool {
+    fn is_ident_like(tt: &TokenTree) -> bool {
+        matches!(
+            tt,
+            TokenTree::Token(Token { kind: token::Ident(..) | token::NtIdent(..), .. }, _,)
+        )
+    }
+    is_ident_like(tt1) && is_ident_like(tt2)
+}
+
 fn space_between(tt1: &TokenTree, tt2: &TokenTree) -> bool {
     use Delimiter::*;
     use TokenTree::{Delimited as Del, Token as Tok};
@@ -811,6 +824,13 @@ pub trait PrintState<'a>: std::ops::Deref<Target = pp::Printer> + std::ops::Dere
             if let Some(next) = iter.peek() {
                 if spacing == Spacing::Alone && space_between(tt, next) {
                     self.space();
+                } else if spacing != Spacing::Alone && idents_would_merge(tt, next) {
+                    // When tokens from macro `tt` captures preserve their
+                    // original `Joint`/`JointHidden` spacing, adjacent
+                    // identifier-like tokens can be concatenated without a
+                    // space (e.g. `$x:identwhere`). Insert a space to
+                    // prevent this.
+                    self.space();
                 }
             }
         }

filename: tests/pretty/macro-fragment-specifier-whitespace.pp
patch: @@ -0,0 +1,24 @@
+#![feature(prelude_import)]
+#![no_std]
+extern crate std;
+#[prelude_import]
+use ::std::prelude::rust_2015::*;
+//@ pretty-mode:expanded
+//@ pp-exact:macro-fragment-specifier-whitespace.pp
+
+// Test that fragment specifier names in macro definitions are properly
+// separated from the following keyword/identifier token when pretty-printed.
+// This is a regression test for a bug where `$x:ident` followed by `where`
+// was pretty-printed as `$x:identwhere` (an invalid fragment specifier).
+
+macro_rules! outer {
+    ($d:tt $($params:tt)*) =>
+    {
+        #[macro_export] macro_rules! inner
+        { ($($params)* where $d($rest:tt)*) => {}; }
+    };
+}
+#[macro_export]
+macro_rules! inner { ($x:ident where $ ($rest : tt)*) => {}; }
+
+fn main() {}

filename: tests/pretty/macro-fragment-specifier-whitespace.rs
patch: @@ -0,0 +1,19 @@
+//@ pretty-mode:expanded
+//@ pp-exact:macro-fragment-specifier-whitespace.pp
+
+// Test that fragment specifier names in macro definitions are properly
+// separated from the following keyword/identifier token when pretty-printed.
+// This is a regression test for a bug where `$x:ident` followed by `where`
+// was pretty-printed as `$x:identwhere` (an invalid fragment specifier).
+
+macro_rules! outer {
+    ($d:tt $($params:tt)*) => {
+        #[macro_export]
+        macro_rules! inner {
+            ($($params)* where $d($rest:tt)*) => {};
+        }
+    };
+}
+outer!($ $x:ident);
+
+fn main() {}