为什么 Rust 找不到使用 proc_macro_attribute 生成的枚举的方法?
Why can't Rust find method for enum generated using proc_macro_attribute?
我正在尝试编写将接受 Rust 枚举的过程宏,例如
#[repr(u8)]
enum Ty {
A,
B
}
并为枚举生成一个方法,让我将 u8 转换为这样的允许变体
fn from_byte(byte: u8) -> Ty {
match {
0 => Ty::A,
1 => Ty::B,
_ => unreachable!()
}
}
这是我使用 proc_macro
库实现的。 (没有外部库)
#![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
}
这就是我的使用方式。
use helper_macros::procmac;
#[procmac]
#[derive(Debug)]
#[repr(u8)]
enum Ty {
A,
B
}
fn main() {
println!("TEST - {:?}", Ty::from_byte(0))
}
问题是这导致编译器出错。确切的错误是
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`
运行 cargo expand
虽然生成了正确的代码。 运行 该代码直接按预期工作。所以我很难过。可能是我遗漏了一些有关如何使用 proc_macros 的内容,因为这是我第一次使用它们,而且我没有看到任何会导致此错误的内容。我正在关注 proc_macro_workshop0 的 sorted
部分。唯一的变化是,我直接使用 TokenStream 而不是使用 syn 和 quote crates。此外,如果我输入错误的方法名称,Rust 编译器会提示存在具有相似名称的方法。
这是一个 Playground 重现:https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=02c1ee77bcd80c68967834a53c011e41
所以,确实你说的是真的:扩展的代码可以复制粘贴,它会起作用。当发生这种情况时(宏扩展和“手动复制粘贴扩展”的行为不同),有两种可能性:
macro_rules!
元变量
当使用 macro_rules!
特殊捕获发出代码时,其中一些捕获用特殊的不可见括号包裹,这些括号已经告诉解析器应该如何解析里面的东西,这使得在其他地方使用是非法的 (例如,一个人可能会捕获一个 $Trait:ty
,然后执行 impl $Trait for ...
将失败(它将 $Trait
解析为一种类型,从而导致它被解释为 一个特征object(旧语法));另请参阅 https://github.com/danielhenrymantilla/rust-defile 了解其他示例。
这不是你的情况,但最好记住(例如我最初的预感是 $tt::$rhs
如果 $tt
是一个类似于 :path
的捕获,那么它可能会失败)。
宏hygiene/transparency和Span
s
考虑,例如:
macro_rules! let_x_42 {() => (
let x = 42;
)}
let_x_42!();
let y = x;
这个expands to code that, if copy-pasted, does not fail to compile.
基本上,宏使用的名称 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
中的代码.
此时的解决方案很简单:使用显式非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!()
}
}
}))
我正在尝试编写将接受 Rust 枚举的过程宏,例如
#[repr(u8)]
enum Ty {
A,
B
}
并为枚举生成一个方法,让我将 u8 转换为这样的允许变体
fn from_byte(byte: u8) -> Ty {
match {
0 => Ty::A,
1 => Ty::B,
_ => unreachable!()
}
}
这是我使用 proc_macro
库实现的。 (没有外部库)
#![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
}
这就是我的使用方式。
use helper_macros::procmac;
#[procmac]
#[derive(Debug)]
#[repr(u8)]
enum Ty {
A,
B
}
fn main() {
println!("TEST - {:?}", Ty::from_byte(0))
}
问题是这导致编译器出错。确切的错误是
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`
运行 cargo expand
虽然生成了正确的代码。 运行 该代码直接按预期工作。所以我很难过。可能是我遗漏了一些有关如何使用 proc_macros 的内容,因为这是我第一次使用它们,而且我没有看到任何会导致此错误的内容。我正在关注 proc_macro_workshop0 的 sorted
部分。唯一的变化是,我直接使用 TokenStream 而不是使用 syn 和 quote crates。此外,如果我输入错误的方法名称,Rust 编译器会提示存在具有相似名称的方法。
这是一个 Playground 重现:https://play.rust-lang.org/?version=nightly&mode=debug&edition=2018&gist=02c1ee77bcd80c68967834a53c011e41
所以,确实你说的是真的:扩展的代码可以复制粘贴,它会起作用。当发生这种情况时(宏扩展和“手动复制粘贴扩展”的行为不同),有两种可能性:
macro_rules!
元变量当使用
macro_rules!
特殊捕获发出代码时,其中一些捕获用特殊的不可见括号包裹,这些括号已经告诉解析器应该如何解析里面的东西,这使得在其他地方使用是非法的 (例如,一个人可能会捕获一个$Trait:ty
,然后执行impl $Trait for ...
将失败(它将$Trait
解析为一种类型,从而导致它被解释为 一个特征object(旧语法));另请参阅 https://github.com/danielhenrymantilla/rust-defile 了解其他示例。这不是你的情况,但最好记住(例如我最初的预感是
$tt::$rhs
如果$tt
是一个类似于:path
的捕获,那么它可能会失败)。宏hygiene/transparency和
Span
s考虑,例如:
macro_rules! let_x_42 {() => ( let x = 42; )} let_x_42!(); let y = x;
这个expands to code that, if copy-pasted, does not fail to compile.
基本上,宏使用的名称
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
中的代码.
此时的解决方案很简单:使用显式非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!()
}
}
}))