为什么 Rust 找不到使用 proc_macro_attribute 生成的枚举的方法?

Akr*_*ime 5 enums rust rust-proc-macros

我正在尝试编写将接受 Rust 枚举的程序宏,例如

#[repr(u8)]
enum Ty {
    A,
    B
}
Run Code Online (Sandbox Code Playgroud)

并为枚举生成一个方法,让我将 u8 转换为允许的变体,如下所示

fn from_byte(byte: u8) -> Ty {
    match {
        0 => Ty::A,
        1 => Ty::B,
        _ => unreachable!()
    }
}
Run Code Online (Sandbox Code Playgroud)

这是我使用proc_macrolib实现的。(没有外部库)

#![feature(proc_macro_diagnostic)]
#![feature(proc_macro_quote)]
extern crate proc_macro;

use proc_macro::{TokenStream, Diagnostic, Level, TokenTree, Ident, Group, Literal};
use proc_macro::quote;

fn report_error(tt: TokenTree, msg: &str) {
    Diagnostic::spanned(tt.span(), Level::Error, msg).emit();
}

fn variants_from_group(group: Group) -> Vec<Ident> {
    let mut iter = group.stream().into_iter();
    let mut res = vec![];
    while let Some(TokenTree::Ident(id)) = iter.next() {
        match iter.next() {
            Some(TokenTree::Punct(_)) | None => res.push(id),
            Some(tt) => {
                report_error(tt, "unexpected variant. Only unit variants accepted.");
                return res
            }
        }
    }
    res
}

#[proc_macro_attribute]
pub fn procmac(args: TokenStream, input: TokenStream) -> TokenStream {
    let _ = args;
    let mut res = TokenStream::new();
    res.extend(input.clone());
    let mut iter = input.into_iter()
        .skip_while(|tt| if let TokenTree::Punct(_) | TokenTree::Group(_) = tt {true} else {false})
        .skip_while(|tt| tt.to_string() == "pub");
    match iter.next() {
        Some(tt @ TokenTree::Ident(_)) if tt.to_string() == "enum" => (),
        Some(tt) => {
            report_error(tt, "unexpected token. this should be only used with enums");
            return res
        },
        None => return res
    }

    match iter.next() {
        Some(tt) => {
            let variants = match iter.next() {
                Some(TokenTree::Group(g)) => {
                    variants_from_group(g)
                }
                _ => return res
            };
            let mut match_arms = TokenStream::new();
            for (i, v) in variants.into_iter().enumerate() {
                let lhs = TokenTree::Literal(Literal::u8_suffixed(i as u8));
                if i >= u8::MAX as usize {
                    report_error(lhs, "enum can have only u8::MAX variants");
                    return res
                }
                let rhs = TokenTree::Ident(v);
                match_arms.extend(quote! {
                    $lhs => $tt::$rhs,
                })
            }
            res.extend(quote!(impl $tt {
                pub fn from_byte(byte: u8) -> $tt {
                    match byte {
                        $match_arms
                        _ => unreachable!()
                    }
                }
            }))
        }
        _ => ()
    }
    
    res
}

Run Code Online (Sandbox Code Playgroud)

这就是我使用它的方式。

use helper_macros::procmac;

#[procmac]
#[derive(Debug)]
#[repr(u8)]
enum Ty {
    A,
    B
}

fn main() {
    println!("TEST - {:?}", Ty::from_byte(0))
}
Run Code Online (Sandbox Code Playgroud)

问题是这会导致编译器出错。确切的错误是

error[E0599]: no variant or associated item named `from_byte` found for enum `Ty` in the current scope
  --> main/src/main.rs:91:32
   |
85 | enum Ty {
   | ------- variant or associated item `from_byte` not found here
...
91 |     println!("TEST - {:?}", Ty::from_byte(0))
   |                                ^^^^^^^^^ variant or associated item not found in `Ty`

Run Code Online (Sandbox Code Playgroud)

运行cargo expand虽然生成正确的代码。并按预期直接运行该代码。所以我很难过。可能是我遗漏了一些关于如何使用 proc_macros 的信息,因为这是我第一次使用它们,我没有看到任何会导致此错误的内容。我正在关注sortedproc_macro_workshop 0的部分。唯一的变化是,我直接使用 TokenStream 而不是使用 syn 和 quote crates。此外,如果我输入错误的方法名称,rust 编译器会建议存在具有相似名称的方法。

Dan*_*H-M 5

这是一个 Playground repro:https ://play.rust-lang.org/ ? version = nightly & mode = debug & edition = 2018 & gist = 02c1ee77bcd80c68967834a53c011e41

所以,确实你提到的是真的:扩展的代码可以复制粘贴,它会起作用。发生这种情况时(宏扩展和“手动复制粘贴扩展”的行为不同),有两种可能性:

  • macro_rules! 元变量

    当使用macro_rules!特殊捕获发出代码时,其中一些捕获用特殊的不可见括号包裹,这些括号已经告诉解析器应该如何解析里面的东西,这使得在其他地方使用是非法的(例如,一个人可能捕获 a $Trait:ty,然后这样做impl $Trait for ...会失败(它将解析$Trait为一种类型,从而导致它被解释为特征对象(旧语法));另请参阅https://github.com/danielhenrymantilla/rust-defile以获取其他示例。

    这不是您的情况,但最好记住(例如,我最初的预感是,如果执行$tt::$rhsif$tt是一个类似:path捕获的操作,则可能会失败)。

  • 宏观卫生/透明度和Spans

    例如,考虑:

    macro_rules! let_x_42 {() => (
        let x = 42;
    )}
    
    let_x_42!();
    let y = x;
    
    Run Code Online (Sandbox Code Playgroud)

    扩展为代码,如果复制粘贴,则不会编译失败

    基本上x,宏使用的名称被“污染”以x与宏主体外使用的任何名称不同,正是为了避免在宏需要定义诸如变量之类的辅助内容时发生错误交互。

    事实证明,这与您的from_byte标识符发生的事情是一样的:您的代码发出了一个from_byte私人卫生/def_site()跨度,这对于使用经典宏或经典 proc-macros 时的方法名称通常不会发生(,当不使用不稳定::proc_macro::quote!宏时)。看到这个评论:https : //github.com/rust-lang/rust/issues/54722#issuecomment-696510769

    因此,from_byte标识符以某种方式被“污染”,允许 Rust 使其对不属于同一宏扩展的代码不可见,例如fn main.

该解决方案,在这一点上,是很容易:打造一个from_bytes Ident具有明确的非ifier def_site() Span例如Span::call_site()或者甚至更好:Span::mixed_site()要模仿的规则macro_rules!宏),以防止其获得该默认def_site() Span::proc_macro::quote!使用:

use ::proc_macro::Span;
// ...
let from_byte = TokenTree::from(Ident::new("from_byte", Span::mixed_site()));
res.extend(quote!(impl $tt {
//         use an interpolated ident rather than a "hardcoded one"
//         vvvvvvvvvv
    pub fn $from_byte(byte: u8) -> $tt {
        match byte {
            $match_arms
            _ => unreachable!()
        }
    }
}))
Run Code Online (Sandbox Code Playgroud)