用macro_rules!
宏解决
使用声明性宏 ( macro_rules!
) 来实现这一点有点棘手,但可能。但是,有必要使用一些技巧。
但首先,这是代码(Playground):
macro_rules! replace {
// This is the "public interface". The only thing we do here is to delegate
// to the actual implementation. The implementation is more complicated to
// call, because it has an "out" parameter which accumulates the token we
// will generate.
($x:ident, $y:ident, $($e:tt)*) => {
replace!(@impl $x, $y, [], $($e)*)
};
// Recursion stop: if there are no tokens to check anymore, we just emit
// what we accumulated in the out parameter so far.
(@impl $x:ident, $y:ident, [$($out:tt)*], ) => {
$($out)*
};
// This is the arm that's used when the first token in the stream is an
// identifier. We potentially replace the identifier and push it to the
// out tokens.
(@impl $x:ident, $y:ident, [$($out:tt)*], $head:ident $($tail:tt)*) => {{
replace!(
@impl $x, $y,
[$($out)* replace!(@replace $x $y $head)],
$($tail)*
)
}};
// These arms are here to recurse into "groups" (tokens inside of a
// (), [] or {} pair)
(@impl $x:ident, $y:ident, [$($out:tt)*], ( $($head:tt)* ) $($tail:tt)*) => {{
replace!(
@impl $x, $y,
[$($out)* ( replace!($x, $y, $($head)*) ) ],
$($tail)*
)
}};
(@impl $x:ident, $y:ident, [$($out:tt)*], [ $($head:tt)* ] $($tail:tt)*) => {{
replace!(
@impl $x, $y,
[$($out)* [ replace!($x, $y, $($head)*) ] ],
$($tail)*
)
}};
(@impl $x:ident, $y:ident, [$($out:tt)*], { $($head:tt)* } $($tail:tt)*) => {{
replace!(
@impl $x, $y,
[$($out)* { replace!($x, $y, $($head)*) } ],
$($tail)*
)
}};
// This is the standard recusion case: we have a non-identifier token as
// head, so we just put it into the out parameter.
(@impl $x:ident, $y:ident, [$($out:tt)*], $head:tt $($tail:tt)*) => {{
replace!(@impl $x, $y, [$($out)* $head], $($tail)*)
}};
// Helper to replace the identifier if its the needle.
(@replace $needle:ident $replacement:ident $i:ident) => {{
// This is a trick to check two identifiers for equality. Note that
// the patterns in this macro don't contain any meta variables (the
// out meta variables $needle and $i are interpolated).
macro_rules! __inner_helper {
// Identifiers equal, emit $replacement
($needle $needle) => { $replacement };
// Identifiers not equal, emit original
($needle $i) => { $i };
}
__inner_helper!($needle $i)
}}
}
fn main() {
let foo = 3;
let bar = 7;
let z = 5;
dbg!(replace!(abc, foo, bar * 100 + z)); // no replacement
dbg!(replace!(bar, foo, bar * 100 + z)); // replace `bar` with `foo`
}
它输出:
[src/main.rs:56] replace!(abc , foo , bar * 100 + z) = 705
[src/main.rs:57] replace!(bar , foo , bar * 100 + z) = 305
这是如何运作的?
在理解这个宏之前需要理解两个主要技巧:下推累加以及如何检查两个标识符是否相等。
此外,请确定:@foobar
宏模式开头的东西不是特殊功能,而只是标记内部辅助宏的约定(另请参阅:“宏的小书”,StackOverflow 问题)。
下推累积在《Rust 宏小书》这一章中有很好的描述。重要的部分是:
Rust 中的所有宏都必须产生完整的、受支持的语法元素(例如表达式、项等)。这意味着不可能将宏扩展为部分构造。
但通常需要有部分结果,例如在使用某些输入处理令牌时。为了解决这个问题,基本上有一个“out”参数,它只是一个随着每个递归宏调用而增长的令牌列表。这是可行的,因为宏输入可以是任意标记,并且不必是有效的 Rust 构造。
这种模式只对作为“增量 TT munchers”工作的宏有意义,我的解决方案就是这样做的。在 TLBORM中也有一章关于这种模式。
第二个关键点是检查两个标识符是否相等。这是通过一个有趣的技巧完成的:宏定义了一个新的宏,然后立即使用该宏。让我们看一下代码:
(@replace $needle:ident $replacement:ident $i:ident) => {{
macro_rules! __inner_helper {
($needle $needle) => { $replacement };
($needle $i) => { $i };
}
__inner_helper!($needle $i)
}}
让我们来看看两个不同的调用:
replace!(@replace foo bar baz)
:这扩展为:
macro_rules! __inner_helper {
(foo foo) => { bar };
(foo baz) => { baz };
}
__inner_helper!(foo baz)
现在inner_helper!
调用显然采用了第二种模式,导致baz
.
replace!(@replace foo bar foo)
另一方面扩展为:
macro_rules! __inner_helper {
(foo foo) => { bar };
(foo foo) => { foo };
}
__inner_helper!(foo foo)
这一次,inner_helper!
调用采用第一个模式,导致bar
.
我从一个 crate 中学到了这个技巧,该 crate 基本上只提供了这个:一个宏检查两个标识符是否相等。但不幸的是,我再也找不到这个箱子了。如果你知道那个箱子的名字,请告诉我!
但是,此实现有一些限制:
使用 proc-macro 的解决方案
当然,这也可以通过 proc-macro 来完成。它还涉及不那么奇怪的技巧。我的解决方案如下所示:
extern crate proc_macro;
use proc_macro::{
Ident, TokenStream, TokenTree,
token_stream,
};
#[proc_macro]
pub fn replace(input: TokenStream) -> TokenStream {
let mut it = input.into_iter();
// Get first parameters
let needle = get_ident(&mut it);
let _comma = it.next().unwrap();
let replacement = get_ident(&mut it);
let _comma = it.next().unwrap();
// Return the remaining tokens, but replace identifiers.
it.map(|tt| {
match tt {
// Comparing `Ident`s can only be done via string comparison right
// now. Note that this ignores syntax contexts which can be a
// problem in some situation.
TokenTree::Ident(ref i) if i.to_string() == needle.to_string() => {
TokenTree::Ident(replacement.clone())
}
// All other tokens are just forwarded
other => other,
}
}).collect()
}
/// Extract an identifier from the iterator.
fn get_ident(it: &mut token_stream::IntoIter) -> Ident {
match it.next() {
Some(TokenTree::Ident(i)) => i,
_ => panic!("oh noes!"),
}
}
将此 proc 宏与main()
上面的示例一起使用完全相同。
注意:这里忽略了错误处理以保持示例简短。请参阅此问题,了解如何在 proc 宏中进行错误报告。
除此之外,我认为该代码不需要太多解释。这个 proc 宏版本也没有宏的递归限制问题macro_rules!
。