Skip to content

Add support for repetition to proc_macro::quote #141608

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 18, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion library/proc_macro/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ mod diagnostic;
mod escape;
mod to_tokens;

use core::ops::BitOr;
use std::ffi::CStr;
use std::ops::{Range, RangeBounds};
use std::path::PathBuf;
Expand Down Expand Up @@ -237,7 +238,7 @@ impl Default for TokenStream {
}

#[unstable(feature = "proc_macro_quote", issue = "54722")]
pub use quote::{quote, quote_span};
pub use quote::{HasIterator, RepInterp, ThereIsNoIteratorInRepetition, ext, quote, quote_span};

fn tree_to_bridge_tree(
tree: TokenTree,
Expand Down
328 changes: 325 additions & 3 deletions library/proc_macro/src/quote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,183 @@
//! items from `proc_macro`, to build a `proc_macro::TokenStream`.

use crate::{
Delimiter, Group, Ident, Literal, Punct, Spacing, Span, ToTokens, TokenStream, TokenTree,
BitOr, Delimiter, Group, Ident, Literal, Punct, Spacing, Span, ToTokens, TokenStream, TokenTree,
};

#[doc(hidden)]
pub struct HasIterator; // True
#[doc(hidden)]
pub struct ThereIsNoIteratorInRepetition; // False

impl BitOr<ThereIsNoIteratorInRepetition> for ThereIsNoIteratorInRepetition {
type Output = ThereIsNoIteratorInRepetition;
fn bitor(self, _rhs: ThereIsNoIteratorInRepetition) -> ThereIsNoIteratorInRepetition {
ThereIsNoIteratorInRepetition
}
}

impl BitOr<ThereIsNoIteratorInRepetition> for HasIterator {
type Output = HasIterator;
fn bitor(self, _rhs: ThereIsNoIteratorInRepetition) -> HasIterator {
HasIterator
}
}

impl BitOr<HasIterator> for ThereIsNoIteratorInRepetition {
type Output = HasIterator;
fn bitor(self, _rhs: HasIterator) -> HasIterator {
HasIterator
}
}

impl BitOr<HasIterator> for HasIterator {
type Output = HasIterator;
fn bitor(self, _rhs: HasIterator) -> HasIterator {
HasIterator
}
}

/// Extension traits used by the implementation of `quote!`. These are defined
/// in separate traits, rather than as a single trait due to ambiguity issues.
///
/// These traits expose a `quote_into_iter` method which should allow calling
/// whichever impl happens to be applicable. Calling that method repeatedly on
/// the returned value should be idempotent.
#[doc(hidden)]
pub mod ext {
use core::slice;
use std::collections::btree_set::{self, BTreeSet};

use super::{
HasIterator as HasIter, RepInterp, ThereIsNoIteratorInRepetition as DoesNotHaveIter,
};
use crate::ToTokens;

/// Extension trait providing the `quote_into_iter` method on iterators.
#[doc(hidden)]
pub trait RepIteratorExt: Iterator + Sized {
fn quote_into_iter(self) -> (Self, HasIter) {
(self, HasIter)
}
}

impl<T: Iterator> RepIteratorExt for T {}

/// Extension trait providing the `quote_into_iter` method for
/// non-iterable types. These types interpolate the same value in each
/// iteration of the repetition.
#[doc(hidden)]
pub trait RepToTokensExt {
/// Pretend to be an iterator for the purposes of `quote_into_iter`.
/// This allows repeated calls to `quote_into_iter` to continue
/// correctly returning DoesNotHaveIter.
fn next(&self) -> Option<&Self> {
Some(self)
}

fn quote_into_iter(&self) -> (&Self, DoesNotHaveIter) {
(self, DoesNotHaveIter)
}
}

impl<T: ToTokens + ?Sized> RepToTokensExt for T {}

/// Extension trait providing the `quote_into_iter` method for types that
/// can be referenced as an iterator.
#[doc(hidden)]
pub trait RepAsIteratorExt<'q> {
type Iter: Iterator;

fn quote_into_iter(&'q self) -> (Self::Iter, HasIter);
}

impl<'q, T: RepAsIteratorExt<'q> + ?Sized> RepAsIteratorExt<'q> for &T {
type Iter = T::Iter;

fn quote_into_iter(&'q self) -> (Self::Iter, HasIter) {
<T as RepAsIteratorExt>::quote_into_iter(*self)
}
}

impl<'q, T: RepAsIteratorExt<'q> + ?Sized> RepAsIteratorExt<'q> for &mut T {
type Iter = T::Iter;

fn quote_into_iter(&'q self) -> (Self::Iter, HasIter) {
<T as RepAsIteratorExt>::quote_into_iter(*self)
}
}

impl<'q, T: 'q> RepAsIteratorExt<'q> for [T] {
type Iter = slice::Iter<'q, T>;

fn quote_into_iter(&'q self) -> (Self::Iter, HasIter) {
(self.iter(), HasIter)
}
}

impl<'q, T: 'q, const N: usize> RepAsIteratorExt<'q> for [T; N] {
type Iter = slice::Iter<'q, T>;

fn quote_into_iter(&'q self) -> (Self::Iter, HasIter) {
(self.iter(), HasIter)
}
}

impl<'q, T: 'q> RepAsIteratorExt<'q> for Vec<T> {
type Iter = slice::Iter<'q, T>;

fn quote_into_iter(&'q self) -> (Self::Iter, HasIter) {
(self.iter(), HasIter)
}
}

impl<'q, T: 'q> RepAsIteratorExt<'q> for BTreeSet<T> {
type Iter = btree_set::Iter<'q, T>;

fn quote_into_iter(&'q self) -> (Self::Iter, HasIter) {
(self.iter(), HasIter)
}
}

impl<'q, T: RepAsIteratorExt<'q>> RepAsIteratorExt<'q> for RepInterp<T> {
type Iter = T::Iter;

fn quote_into_iter(&'q self) -> (Self::Iter, HasIter) {
self.0.quote_into_iter()
}
}
}

// Helper type used within interpolations to allow for repeated binding names.
// Implements the relevant traits, and exports a dummy `next()` method.
#[derive(Copy, Clone)]
#[doc(hidden)]
pub struct RepInterp<T>(pub T);

impl<T> RepInterp<T> {
// This method is intended to look like `Iterator::next`, and is called when
// a name is bound multiple times, as the previous binding will shadow the
// original `Iterator` object. This allows us to avoid advancing the
// iterator multiple times per iteration.
pub fn next(self) -> Option<T> {
Some(self.0)
}
}

impl<T: Iterator> Iterator for RepInterp<T> {
type Item = T::Item;

fn next(&mut self) -> Option<Self::Item> {
self.0.next()
}
}

impl<T: ToTokens> ToTokens for RepInterp<T> {
fn to_tokens(&self, tokens: &mut TokenStream) {
self.0.to_tokens(tokens);
}
}

macro_rules! minimal_quote_tt {
(($($t:tt)*)) => { Group::new(Delimiter::Parenthesis, minimal_quote!($($t)*)) };
([$($t:tt)*]) => { Group::new(Delimiter::Bracket, minimal_quote!($($t)*)) };
Expand All @@ -20,7 +194,13 @@ macro_rules! minimal_quote_tt {
(>) => { Punct::new('>', Spacing::Alone) };
(&) => { Punct::new('&', Spacing::Alone) };
(=) => { Punct::new('=', Spacing::Alone) };
(#) => { Punct::new('#', Spacing::Alone) };
(|) => { Punct::new('|', Spacing::Alone) };
(:) => { Punct::new(':', Spacing::Alone) };
(*) => { Punct::new('*', Spacing::Alone) };
(_) => { Ident::new("_", Span::def_site()) };
($i:ident) => { Ident::new(stringify!($i), Span::def_site()) };
($lit:literal) => { stringify!($lit).parse::<Literal>().unwrap() };
}

macro_rules! minimal_quote_ts {
Expand All @@ -36,6 +216,39 @@ macro_rules! minimal_quote_ts {
[c.0, c.1].into_iter().collect::<TokenStream>()
}
};
(=>) => {
{
let mut c = (
TokenTree::from(Punct::new('=', Spacing::Joint)),
TokenTree::from(Punct::new('>', Spacing::Alone))
);
c.0.set_span(Span::def_site());
c.1.set_span(Span::def_site());
[c.0, c.1].into_iter().collect::<TokenStream>()
}
};
(+=) => {
{
let mut c = (
TokenTree::from(Punct::new('+', Spacing::Joint)),
TokenTree::from(Punct::new('=', Spacing::Alone))
);
c.0.set_span(Span::def_site());
c.1.set_span(Span::def_site());
[c.0, c.1].into_iter().collect::<TokenStream>()
}
};
(!=) => {
{
let mut c = (
TokenTree::from(Punct::new('!', Spacing::Joint)),
TokenTree::from(Punct::new('=', Spacing::Alone))
);
c.0.set_span(Span::def_site());
c.1.set_span(Span::def_site());
[c.0, c.1].into_iter().collect::<TokenStream>()
}
};
($t:tt) => { TokenTree::from(minimal_quote_tt!($t)) };
}

Expand Down Expand Up @@ -71,17 +284,99 @@ pub fn quote(stream: TokenStream) -> TokenStream {
let mut after_dollar = false;

let mut tokens = crate::TokenStream::new();
for tree in stream {
let mut iter = stream.into_iter().peekable();
while let Some(tree) = iter.next() {
if after_dollar {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

after_dollar can be combined with LookaheadIter, if you prefer.

after_dollar = false;
match tree {
TokenTree::Group(tt) => {
// Handles repetition by expanding `$( CONTENTS ) SEP_OPT *` to `{ REP_EXPANDED }`.
let contents = tt.stream();

// The `*` token is also consumed here.
let sep_opt: Option<Punct> = match (iter.next(), iter.peek()) {
(Some(TokenTree::Punct(sep)), Some(TokenTree::Punct(star)))
if sep.spacing() == Spacing::Joint && star.as_char() == '*' =>
{
iter.next();
Some(sep)
}
(Some(TokenTree::Punct(star)), _) if star.as_char() == '*' => None,
_ => panic!("`$(...)` must be followed by `*` in `quote!`"),
};

let mut rep_expanded = TokenStream::new();

// Append setup code for a `while`, where recursively quoted `CONTENTS`
// and `SEP_OPT` are repeatedly processed, to `REP_EXPANDED`.
let meta_vars = collect_meta_vars(contents.clone());
minimal_quote!(
use crate::ext::*;
(@ if sep_opt.is_some() {
minimal_quote!(let mut _i = 0usize;)
} else {
minimal_quote!(();)
})
let has_iter = crate::ThereIsNoIteratorInRepetition;
)
.to_tokens(&mut rep_expanded);
for meta_var in &meta_vars {
minimal_quote!(
#[allow(unused_mut)]
let (mut (@ meta_var), i) = (@ meta_var).quote_into_iter();
let has_iter = has_iter | i;
)
.to_tokens(&mut rep_expanded);
}
minimal_quote!(let _: crate::HasIterator = has_iter;)
.to_tokens(&mut rep_expanded);

// Append the `while` to `REP_EXPANDED`.
let mut while_body = TokenStream::new();
for meta_var in &meta_vars {
minimal_quote!(
let (@ meta_var) = match (@ meta_var).next() {
Some(_x) => crate::RepInterp(_x),
None => break,
};
)
.to_tokens(&mut while_body);
}
minimal_quote!(
(@ if let Some(sep) = sep_opt {
minimal_quote!(
if _i > 0 {
(@ minimal_quote!(crate::ToTokens::to_tokens(&crate::TokenTree::Punct(crate::Punct::new(
(@ TokenTree::from(Literal::character(sep.as_char()))),
(@ minimal_quote!(crate::Spacing::Alone)),
)), &mut ts);))
}
_i += 1;
)
} else {
minimal_quote!(();)
})
(@ quote(contents.clone())).to_tokens(&mut ts);
)
.to_tokens(&mut while_body);
rep_expanded.extend(vec![
TokenTree::Ident(Ident::new("while", Span::call_site())),
TokenTree::Ident(Ident::new("true", Span::call_site())),
TokenTree::Group(Group::new(Delimiter::Brace, while_body)),
]);

minimal_quote!((@ TokenTree::Group(Group::new(Delimiter::Brace, rep_expanded)))).to_tokens(&mut tokens);
continue;
}
TokenTree::Ident(_) => {
minimal_quote!(crate::ToTokens::to_tokens(&(@ tree), &mut ts);)
.to_tokens(&mut tokens);
continue;
}
TokenTree::Punct(ref tt) if tt.as_char() == '$' => {}
_ => panic!("`$` must be followed by an ident or `$` in `quote!`"),
_ => panic!(
"`$` must be followed by an ident or `$` or a repetition group in `quote!`"
),
}
} else if let TokenTree::Punct(ref tt) = tree {
if tt.as_char() == '$' {
Expand Down Expand Up @@ -155,6 +450,33 @@ pub fn quote(stream: TokenStream) -> TokenStream {
}
}

/// Helper function to support macro repetitions like `$( CONTENTS ) SEP_OPT *` in `quote!`.
/// Recursively collects all `Ident`s (meta-variables) that follow a `$`
/// from the given `CONTENTS` stream, preserving their order of appearance.
fn collect_meta_vars(content_stream: TokenStream) -> Vec<Ident> {
fn helper(stream: TokenStream, out: &mut Vec<Ident>) {
let mut iter = stream.into_iter().peekable();
while let Some(tree) = iter.next() {
match &tree {
TokenTree::Punct(tt) if tt.as_char() == '$' => {
if let Some(TokenTree::Ident(id)) = iter.peek() {
out.push(id.clone());
iter.next();
}
}
TokenTree::Group(tt) => {
helper(tt.stream(), out);
}
_ => {}
}
}
}

let mut vars = Vec::new();
helper(content_stream, &mut vars);
vars
}

/// Quote a `Span` into a `TokenStream`.
/// This is needed to implement a custom quoter.
#[unstable(feature = "proc_macro_quote", issue = "54722")]
Expand Down
Loading
Loading