< 返回版块

TsuITOAR 发表于 2022-02-14 18:10

Tags:quote,macro

尝试实现proc-macro-workshop的seq宏,但是在第三步要求中的输出指定错误信息卡住了
lib.rs

use proc_macro::TokenStream;
use proc_macro2::{Group, Literal, TokenStream as TokenStream2, TokenTree};
use quote::{quote, quote_spanned, ToTokens};
use syn::{
    parse::{Parse, ParseStream},
    parse_macro_input,
    spanned::Spanned,
    Block, Ident, LitInt, Result, Token,
};

#[derive(Debug, Clone)]
struct Seq {
    ident: Ident,
    lower_bound: i32,
    higher_bound: i32,
    body: TokenStream2,
}

impl Parse for Seq {
    fn parse(input: ParseStream) -> Result<Self> {
        let ident: Ident = input.parse()?;
        input.parse::<Token![in]>()?;
        let lower_bound = input.parse::<LitInt>()?.base10_parse()?;
        input.parse::<Token![..]>()?;
        let higher_bound = input.parse::<LitInt>()?.base10_parse()?;
        let b = input.parse::<Block>()?;
        let body = if let Some(TokenTree::Group(g)) = b.to_token_stream().into_iter().next() {
            g.stream()
        } else {
            unreachable!()
        };
        Ok(Self {
            ident,
            lower_bound,
            higher_bound,
            body,
        })
    }
}

fn substitute_ident(input: TokenStream2, ident: Ident, target: TokenTree) -> TokenStream2 {
    let mut out = TokenStream2::new();
    for t in input {
        match t {
            TokenTree::Group(g) => TokenTree::Group(Group::new(
                g.delimiter(),
                substitute_ident(g.stream(), ident.clone(), target.clone()),
            )),
            TokenTree::Ident(i) => {
                if i == ident {
                    target.clone()
                } else {
                    TokenTree::Ident(i)
                }
            }
            x => x,
        }
        .to_tokens(&mut out)
    }
    out
}

#[proc_macro]
pub fn seq(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as Seq);
    dbg!(&input);
    let s = (input.lower_bound..input.higher_bound).map(|i| {
        substitute_ident(
            input.body.clone(),
            input.ident.clone(),
            TokenTree::Literal(Literal::i32_unsuffixed(i)),
        )
    });
    quote_spanned! {input.body.span()=> #(#s)*}.into()
}

test文件

use seq::seq;

seq!(N in 0..4 {
    compile_error!(concat!("error number ", stringify!(N)));
});

fn main() {}

虽然使用了quote_spanned!宏,但是输出错误信息是

error: error number 0
 --> main.rs:2:1
  |
2 | / seq!(N in 0..4 {
3 | |     compile_error!(concat!("error number ", stringify!(N)));
4 | | });
  | |__^
  |
  = note: this error originates in the macro `seq` (in Nightly builds, run with -Z macro-backtrace for more info)

但是我的span确实是正确的,如果宏输出以下内容

#[proc_macro]
pub fn seq(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as Seq);
    dbg!(&input);
    let s = (input.lower_bound..input.higher_bound).map(|i| {
        substitute_ident(
            input.body.clone(),
            input.ident.clone(),
            TokenTree::Literal(Literal::i32_unsuffixed(i)),
        )
    });
    let e = syn::Error::new(input.body.span(), "test").to_compile_error();
    //quote_spanned! {input.body.span()=> #(#s)*}.into()
    quote!(#e).into()
}

就可以在正确的地方给出错误

error: test
 --> main.rs:3:5
  |
3 |     compile_error!(concat!("error number ", stringify!(N)));
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

这里的quote_spanned应该如何使用?

评论区

写评论
苦瓜小仔 2022-02-15 00:21

你使用 group.set_span(g.span()); 之后可以通过测试,但这与 quote_spanned! 无关,你把它换成 quote!,也一样通过。在这里它们几乎没有差别,因为标记的 Span 都原封不动地保留下来了。

你使用 quote_spanned! 是个好习惯,因为你记得把生成的标记对应到原文该存在的位置上去。

但实际操作中,我们无需考虑那些从原文拿过来的标记的 Span(它们已经存在,而且你无法修改),而应该考虑新生成的标记的 Span(因为这种情况下,syn/quote 默认给新标记过程宏调用处的 Span)。

比如你在新生成的 Group 没有给它原有的标记,导致测试不能通过。

此外,因为 Seq! 在这里只对某个标识符进行替换或者重组,你还可以考虑因为替换或者重组导致的新标记,是否需要给它定位到原文的某处,比如 a..b 中的 a 或者 b

说得再具体一点:

Seq! 把某个标识符进行替换成 Literal 类型时,你可以给它设置 Span。但由于这里产生了额外的数据,a..b 中的 a+1 你可能要考虑怎么赋予 Span(这个地方没有测试涉及到,所以这个问题你不太需要考虑)。

Seq! 把某些标识符重组时,你如果直接使用 Ident 类型(别忘了,syn 中的 Ident 是 proc_macro2 的重导出) + set_span 方法,也没问题。但很多时候 quote::format_ident! 帮你处理了一种常见情况下的卫生性问题。

是的,考虑 Span 就是考虑卫生性问题


你可以从我写的这个案例中看到:

// 过程宏错误信息:`quote!` 不指定 Span,默认指向宏调用的位置
error[E0277]: `Rc<u8>` cannot be shared between threads safely
 --> src/main.rs:16:5
  |
16|     assert_sync_proc!(std::rc::Rc<u8>);
  |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ `Rc<u8>` cannot be shared between threads safely
  |

// 过程宏错误信息:`quote_spanned!` 指定 Span 可以更清楚地知道错误区域
error[E0277]: `Rc<u8>` cannot be shared between threads safely
 --> src/main.rs:20:31
  |
20|     assert_sync_proc_spanned!(std::rc::Rc<u8>);
  |                               ^^^^^^^^^^^^^^^ `Rc<u8>` cannot be shared between threads safely
  |

这就是 quote!quote_spanned! 的区别。

在 proc-macro-workshop 中,大部分情况下使用 quote! 就足够了,而使用 quote_spanned! 优于使用 quote! 的地方,dtolnay 已经用测试来帮助你学习编写了。

作者 TsuITOAR 2022-02-14 22:47

感谢,是我把旧Group替换Ident后构造的新Group不包含原有的Span信息,所以默认Span在宏调用处了。能请教一下quote_spanned的使用限制吗,为什么我这里使用就没有效果呢

--
👇
苦瓜小仔: 这里无需使用 quote_spanned,如果只通过前三个测试,下面的代码就足够了:

playground

要通过完整的测试,可参考我的解答

提示:解析自原文的 TokenStream 自带 Span。

苦瓜小仔 2022-02-14 20:33

这里无需使用 quote_spanned,如果只通过前三个测试,下面的代码就足够了:

playground

要通过完整的测试,可参考我的解答

提示:解析自原文的 TokenStream 自带 Span。

1 共 3 条评论, 1 页