如何通过 Rust 宏将表达式中的一个标识符替换为另一个标识符?

Posted

技术标签:

【中文标题】如何通过 Rust 宏将表达式中的一个标识符替换为另一个标识符?【英文标题】:How to replace one identifier in an expression with another one via Rust macro? 【发布时间】:2019-09-20 22:01:55 【问题描述】:

我正在尝试构建一个宏来进行一些代码转换,并且应该能够解析它自己的语法。 这是我能想到的最简单的例子:

replace!(x, y, x * 100 + z) ~> y * 100 + z

这个宏应该能够用作为第三个参数提供的表达式中的第二个标识符替换第一个标识符。宏应该对第三个参数的语言有一定的了解(在我的特殊情况下,与示例相反,不会在 Rust 中解析)并递归地应用它。

在 Rust 中构建这样一个宏的最有效方法是什么?我知道proc_macro 方法和macro_rules! 方法。但是我不确定macro_rules! 是否足够强大来处理这个问题,而且我找不到太多关于如何使用proc_macro 构建自己的转换的文档。谁能指出我正确的方向?

【问题讨论】:

“谁能指出我正确的方向?” 这是在 SO 问题中做出的危险陈述。最好自己做一些尝试,将问题缩小到更具体的问题。 谢谢!但是,我自己尝试使用macro_rules! 的解决方案,这是记录最多的解决方案。在那一点上我完全被卡住了,无法找到一种方法来进行这种匹配。我应该分享我的尝试吗? 【参考方案1】:

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 并不是一个特殊功能,而只是标记内部辅助宏的约定(另请参阅:"The little book of Macros"、*** question)。


下推积累在this chapter of "The little book of Rust macros"中有很好的描述。重要的部分是:

Rust 中的所有宏必须产生完整的、受支持的语法元素(例如表达式、项目等)。这意味着不可能将宏扩展为部分构造。

但通常需要有部分结果,例如在处理带有某些输入的令牌时。为了解决这个问题,基本上有一个“out”参数,它只是一个随着每个递归宏调用而增长的令牌列表。这是可行的,因为宏输入可以是任意标记,并且不必是有效的 Rust 构造。

这种模式只对作为“增量 TT 咀嚼器”工作的宏有意义,我的解决方案就是这样做的。还有a chapter about this pattern in 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 中学到了这个技巧,它基本上只提供了一个:一个宏检查两个标识符是否相等。但不幸的是,我再也找不到这个箱子了。如果你知道那个箱子的名字,请告诉我!


但是,此实现有一些限制:

作为增量 TT muncher,它对输入中的每个标记进行递归。所以很容易达到递归限制(可以增加,但不是最优的)。可以编写此宏的非递归版本,但到目前为止我还没有找到方法。

macro_rules! 宏在标识符方面有点奇怪。上面提出的解决方案在使用 self 作为标识符时可能表现得很奇怪。有关该主题的更多信息,请参阅 this chapter。

使用 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() 示例一起使用完全一样。

注意:这里忽略了错误处理以保持示例简短。请参阅this question 了解如何在 proc 宏中进行错误报告。

除此之外,我认为该代码不需要太多解释。这个 proc 宏版本也不会像 macro_rules! 宏那样遇到递归限制问题。

【讨论】:

好技巧!谢谢,这很有启发性。很高兴我可以继续使用macro_rules! 构建我的解决方案,我想将其实现为处理TokenStreams 的函数还有很多工作要做。 @hoheinzollern 我为 proc 宏添加了一个实现。我不会说这是“更多的工作”。它实际上更容易理解 IMO,因为它不需要那么多的 hack。但可以肯定的是,遗憾的是,设置 proc-macro 仍然需要一个单独的 crate,并且进行适当的错误处理会添加样板代码。 请注意,您的macro_rules! 解决方案不处理括号(例如replace!(foo, bar, (foo))),需要为此添加特殊规则。 @Jmb 发现得很好!我根本没想到。我现在在答案中修复了它(我认为)。 @Lukas 我需要对您的代码进行哪些更改才能解析块而不是表达式?例如我想打这个电话:dbg!(replace!(abc, foo, let x = 100; foo * x + z ));

以上是关于如何通过 Rust 宏将表达式中的一个标识符替换为另一个标识符?的主要内容,如果未能解决你的问题,请参考以下文章

如何评估 Rust 宏系统中的表达式?

打开多个 Excel 实例时如何通过 Excel 宏将数据从 Excel 导出到 Access

如何将 C 变长数组代码转换为 Rust?

通过组替换引导,但为重新采样的单元创建新标识符

Excel 宏将字段中的公式保留为文本

c语言中的“宏”是指啥?