解决方案与macro_rules!
macro
要使用声明性宏来实现这一点(macro_rules!
)有点棘手但可能。然而,有必要使用一些技巧。
但首先,这是代码():
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
这是如何运作的?
在理解这个宏之前,需要了解两个主要技巧:下推积累 and 如何检查两个标识符是否相等.
此外,可以肯定的是:@foobar
宏模式开头的内容并不是一个特殊功能,而只是标记内部辅助宏的约定(另请参阅:《宏的小书》 https://danielkeep.github.io/tlborm/book/pat-internal-rules.html, StackOverflow问题 https://stackoverflow.com/questions/54406388/what-does-an-symbol-mean-in-a-declarative-macro).
下推积累中有很好的描述《Rust 宏小书》这一章 https://danielkeep.github.io/tlborm/book/pat-push-down-accumulation.html。重要的部分是:
Rust 中的所有宏must产生完整的、受支持的语法元素(例如表达式、项目等)。这意味着不可能将宏扩展为部分构造。
但通常需要获得部分结果,例如在使用某些输入处理令牌时。为了解决这个问题,基本上有一个“out”参数,它只是一个随着每次递归宏调用而增长的标记列表。这是可行的,因为宏输入可以是任意标记,并且不必是有效的 Rust 构造。
这种模式仅对充当“增量 TT 咀嚼者”的宏有意义,我的解决方案就是这样做的。还有TLBORM 中有关于此模式的一章 https://danielkeep.github.io/tlborm/book/pat-incremental-tt-munchers.html.
第二个关键点是检查两个标识符是否相等。这是通过一个有趣的技巧来完成的:宏定义一个新的宏,然后立即使用它。我们看一下代码:
(@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)
And the 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
.
我从一个板条箱中学到了这个技巧,该板条箱基本上只提供了这样的功能:检查两个标识符是否相等的宏。但不幸的是,我再也找不到这个箱子了。如果您知道那个箱子的名字,请告诉我!
然而,这种实现有一些限制:
作为增量 TT 咀嚼器,它会针对输入中的每个标记进行递归。所以很容易达到递归限制(可以增加,但不是最优的)。可以编写这个宏的非递归版本,但到目前为止我还没有找到一种方法来做到这一点。
macro_rules!
当涉及到标识符时,宏有点奇怪。上面提出的解决方案可能会表现得很奇怪self
作为标识符。看本章 https://danielkeep.github.io/tlborm/book/mbe-min-non-identifier-identifiers.html有关该主题的更多信息。
使用 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!"),
}
}
将此 proc 宏与main()
上面的例子的工作原理完全相同。
Note:为了使示例简短,此处忽略了错误处理。请参见这个问题 https://stackoverflow.com/questions/54392702/how-to-report-errors-in-a-procedural-macro-using-the-quote-macro关于如何在 proc 宏中进行错误报告。
除此之外,我认为该代码不需要太多解释。这个 proc 宏版本也不会遇到递归限制问题,因为macro_rules!
macro.