Solution with macro_rules!
macro
用声明性宏(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
这是怎么回事?
在理解这个宏之前,需要了解两个主要技巧:push down accumulation和how to check two identifiers for equality.
此外,可以肯定的是:宏模式开头的@foobar
个东西不是一个特殊功能,只是一个标记内部辅助宏的约定(另请参见:"The little book of Macros"StackOverflow question).
Push down accumulation在this chapter of "The little book of Rust macros"中有很好的描述.重要的部分是:
Rust must中的所有宏都会生成一个完整的、受支持的语法元素(例如表达式、项等).这意味着不可能将宏展开为部分构造.
但通常需要有部分结果,例如,当使用一些输入逐个处理令牌时.为了解决这个问题,基本上有一个"out"参数,它只是一个令牌列表,随着每次递归宏调用而增长.这是可行的,因为宏输入可以是任意标记,而不必是有效的构造.
这种模式只适用于作为"增量TT munchers"工作的宏,我的解决方案就是这样做的.还有a chapter about this pattern in TLBORM个.
第二个关键点是check two identifiers for equality.这是通过一个有趣的技巧完成的:宏定义一个新的宏,然后立即使用它.让我们看一下代码:
(@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 中学到了这个技巧:一个宏判断两个标识符是否相等.但不幸的是,我再也找不到这个箱子了.如果你知道那箱子的名字,请告诉我!
然而,这种实现有一些局限性:
Solution with proc-macro
当然,这也可以通过proc宏来完成.它还涉及一些不那么奇怪的技巧.我的解决方案如下所示:
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!"),
}
}
在上面的main()
个例子中使用这个proc宏的效果完全相同.
Note:这里忽略了错误处理,以保持示例简短.有关如何在proc宏中执行错误报告,请参见this question.
除此之外,我认为代码不需要太多解释.这个proc宏版本也不像macro_rules!
宏那样存在递归限制问题.