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

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
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
200 changes: 200 additions & 0 deletions library/proc_macro/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,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 @@ -1613,3 +1614,202 @@ pub mod tracked_path {
crate::bridge::client::FreeFunctions::track_path(path);
}
}

#[doc(hidden)]
#[unstable(feature = "proc_macro_quote", issue = "54722")]
Copy link
Author

@moatom moatom May 31, 2025

Choose a reason for hiding this comment

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

I am not sure whether these annotations are appropriate. (I added them just based on my speculation.)

In addition,

It's probably easiest to just copy quote's logic here, which uses an extension trait to facilitate this.

do we need to take care of its license?

Copy link
Contributor

@tgross35 tgross35 Jun 13, 2025

Choose a reason for hiding this comment

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

I am not sure whether these annotations are appropriate. (I added them just based on my speculation.)

You can always try removing them and see if it complains - but in general, all crate-public items need a stability gate. Try to make as little as possible actually pub of course, but that's difficult because this needs a lot of helpers to be public.

This all doesn't need to be in lib.rs though, could you move the additions here to the quote module? And then reexport only what is needed.

It's probably easiest to just copy quote's logic here, which uses an extension trait to facilitate this.

do we need to take care of its license?

quote is MIT AND Apache-2.0, which is the same as rust-lang/rust so there is no problem here.

#[derive(Debug)]
pub struct HasIterator; // True
#[doc(hidden)]
#[unstable(feature = "proc_macro_quote", issue = "54722")]
#[derive(Debug)]
pub struct ThereIsNoIteratorInRepetition; // False

#[unstable(feature = "proc_macro_quote", issue = "54722")]
impl BitOr<ThereIsNoIteratorInRepetition> for ThereIsNoIteratorInRepetition {
type Output = ThereIsNoIteratorInRepetition;
fn bitor(self, _rhs: ThereIsNoIteratorInRepetition) -> ThereIsNoIteratorInRepetition {
ThereIsNoIteratorInRepetition
}
}

#[unstable(feature = "proc_macro_quote", issue = "54722")]
impl BitOr<ThereIsNoIteratorInRepetition> for HasIterator {
type Output = HasIterator;
fn bitor(self, _rhs: ThereIsNoIteratorInRepetition) -> HasIterator {
HasIterator
}
}

#[unstable(feature = "proc_macro_quote", issue = "54722")]
impl BitOr<HasIterator> for ThereIsNoIteratorInRepetition {
type Output = HasIterator;
fn bitor(self, _rhs: HasIterator) -> HasIterator {
HasIterator
}
}

#[unstable(feature = "proc_macro_quote", issue = "54722")]
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)]
#[unstable(feature = "proc_macro_quote", issue = "54722")]
pub mod ext {
use core::slice;
use std::collections::btree_set::{self, BTreeSet};
Copy link
Author

Choose a reason for hiding this comment

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

an alternative for alloc::collections


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

/// Extension trait providing the `quote_into_iter` method on iterators.
#[unstable(feature = "proc_macro_quote", issue = "54722")]
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.
#[unstable(feature = "proc_macro_quote", issue = "54722")]
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.
#[unstable(feature = "proc_macro_quote", issue = "54722")]
fn next(&self) -> Option<&Self> {
Some(self)
}

#[unstable(feature = "proc_macro_quote", issue = "54722")]
fn quote_into_iter(&self) -> (&Self, DoesNotHaveIter) {
(self, DoesNotHaveIter)
}
}

#[unstable(feature = "proc_macro_quote", issue = "54722")]
impl<T: ToTokens + ?Sized> RepToTokensExt for T {}

/// Extension trait providing the `quote_into_iter` method for types that
/// can be referenced as an iterator.
#[unstable(feature = "proc_macro_quote", issue = "54722")]
pub trait RepAsIteratorExt<'q> {
#[unstable(feature = "proc_macro_quote", issue = "54722")]
type Iter: Iterator;

#[unstable(feature = "proc_macro_quote", issue = "54722")]
fn quote_into_iter(&'q self) -> (Self::Iter, HasIter);
}

#[unstable(feature = "proc_macro_quote", issue = "54722")]
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)
}
}

#[unstable(feature = "proc_macro_quote", issue = "54722")]
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)
}
}

#[unstable(feature = "proc_macro_quote", issue = "54722")]
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)
}
}

#[unstable(feature = "proc_macro_quote", issue = "54722")]
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)
}
}

#[unstable(feature = "proc_macro_quote", issue = "54722")]
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)
}
}

#[unstable(feature = "proc_macro_quote", issue = "54722")]
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)]
#[unstable(feature = "proc_macro_quote", issue = "54722")]
pub struct RepInterp<T>(pub T);

#[unstable(feature = "proc_macro_quote", issue = "54722")]
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.
#[unstable(feature = "proc_macro_quote", issue = "54722")]
pub fn next(self) -> Option<T> {
Some(self.0)
}
}

#[unstable(feature = "proc_macro_quote", issue = "54722")]
impl<T: Iterator> Iterator for RepInterp<T> {
type Item = T::Item;

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

#[unstable(feature = "proc_macro_quote", issue = "54722")]
impl<T: ToTokens> ToTokens for RepInterp<T> {
fn to_tokens(&self, tokens: &mut TokenStream) {
self.0.to_tokens(tokens);
}
}
147 changes: 146 additions & 1 deletion library/proc_macro/src/quote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,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 +42,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,10 +110,92 @@ 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
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(inner) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you add comments in this section about what is happening?

let content = inner.stream();

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(sep)), _) if sep.as_char() == '*' => None,
_ => panic!("`$(...)` must be followed by `*` in `quote!`"),
};

let meta_vars = collect_meta_vars(content.clone());

let mut content_tokens = TokenStream::new();
minimal_quote!(
use crate::ext::*;
)
.to_tokens(&mut content_tokens);
if sep_opt.is_some() {
minimal_quote!(
let mut _i = 0usize;
)
.to_tokens(&mut content_tokens);
}
minimal_quote!(
let has_iter = crate::ThereIsNoIteratorInRepetition;
)
.to_tokens(&mut content_tokens);
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 content_tokens);
}
minimal_quote!(
let _: crate::HasIterator = has_iter;
)
.to_tokens(&mut content_tokens);

let while_ident = TokenTree::Ident(Ident::new("while", Span::call_site()));
let true_literal = TokenTree::Ident(Ident::new("true", Span::call_site()));

let mut inner_tokens = 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 inner_tokens);
}
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;
)
.to_tokens(&mut inner_tokens);
};
minimal_quote!(
(@ quote(content.clone())).to_tokens(&mut ts);
)
.to_tokens(&mut inner_tokens);
let while_block = TokenTree::Group(Group::new(Delimiter::Brace, inner_tokens));

content_tokens.extend(vec![while_ident, true_literal, while_block]);
let block = TokenTree::Group(Group::new(Delimiter::Brace, content_tokens));
minimal_quote!((@ block)).to_tokens(&mut tokens);

continue;
}
TokenTree::Ident(_) => {
minimal_quote!(crate::ToTokens::to_tokens(&(@ tree), &mut ts);)
.to_tokens(&mut tokens);
Expand Down Expand Up @@ -155,6 +276,30 @@ pub fn quote(stream: TokenStream) -> TokenStream {
}
}

fn collect_meta_vars(stream: TokenStream) -> Vec<Ident> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Could you add a doc comment?

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(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