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

17

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

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

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

在 Rust 中,构建这样一个宏最有效的方法是什么?我知道有 proc_macro 方法和 macro_rules! 方法。然而,我不确定 macro_rules! 是否足够强大来处理这个问题,并且我找不到如何使用 proc_macro 构建自己的转换的文档。有人可以指引我一下吗?


有人可以指点我正确的方向吗? 在 SO 问题中这是一个危险的声明。最好自己尝试一些并将问题缩小到更具体的关注点。 - E net4
谢谢!不过我自己尝试使用macro_rules!来解决这个问题,因为它是最有文档记录的。但是我在那一点上卡住了,找不到一种匹配的方法。我应该分享我的尝试吗? - hoheinzollern
1个回答

25

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不是特殊功能,而只是一种约定,用于标记内部帮助宏(还请参见:"Macros小书"StackOverflow问题)。 向下累积"Rust宏小书"的这一章节中有很好的描述。重要部分如下:

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

但是,在处理逐个令牌时通常需要部分结果。为了解决这个问题,基本上有一个“输出”参数,它只是一个随着每次递归宏调用而增长的令牌列表。这是可行的,因为宏输入可以是任意令牌,不必是有效的Rust结构。
这种模式只对作为“增量TT咀嚼器”的宏有意义,而我的解决方案就是如此。还有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): this expands to:

    macro_rules! __inner_helper {
        (foo foo) => { bar };
        (foo baz) => { baz };
    }
    
    __inner_helper!(foo baz)
    

    And the inner_helper! invocation now clearly takes the second pattern, resulting in baz.

  • replace!(@replace foo bar foo) on the other hand expands to:

    macro_rules! __inner_helper {
        (foo foo) => { bar };
        (foo foo) => { foo };
    }
    
    __inner_helper!(foo foo)
    

    This time, the inner_helper! invocation takes the first pattern, resulting in bar.

我从一个仅提供检查两个标识符是否相等的宏中学到了这个技巧。但不幸的是,我找不到这个宏包了。如果您知道那个宏包的名称,请告诉我!


然而,这种实现有一些限制:

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

  • macro_rules!宏在处理标识符时有些奇怪。上面提出的解决方案可能会在标识符为self时表现奇怪。有关该主题的更多信息,请参见本章


使用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!"),
    }
}

使用这个过程宏与上面的 main() 示例完全相同。 注意:错误处理在此处被忽略以使示例简短。请参见此问题关于如何在过程宏中报告错误。
除此之外,那段代码不需要太多解释,我认为。这个过程宏版本也不会像 macro_rules! 宏一样受到递归限制的问题的困扰。

不错的技巧!谢谢,这很有启发性。很高兴我可以继续使用 macro_rules! 来构建我的解决方案,我猜实现一个处理 TokenStream 的函数会更费力一些。 - hoheinzollern
@hoheinzollern,我为过程宏添加了一个实现。我不会说这是“更多的工作”;在我看来,它实际上更容易理解,因为它不需要太多的技巧。但是,确实需要单独创建一个箱子来设置过程宏,并且进行适当的错误处理需要添加样板代码。 - Lukas Kalbertodt
请注意,您的 macro_rules! 解决方案无法处理括号(例如 replace!(foo, bar, (foo))),需要为此添加特殊规则。 - Jmb
@Jmb 发现得真好!我完全没想到。我现在已经在答案中修复了它(我想)。 - Lukas Kalbertodt
啊,真希望在我尝试将它适应我的问题之前读了那个评论,说它无法处理语句!(我还发现它对匹配表达式也不太友好。)看起来 proc-macro 是解决任何严重问题的方法。仍然非常酷! - Chrispher
显示剩余2条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接