我正在try 构建一个宏,它可以进行一些代码转换,并且应该能够解析自己的语法.

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

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

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

推荐答案

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 accumulationhow to check two identifiers for equality.

此外,可以肯定的是:宏模式开头的@foobar个东西不是一个特殊功能,只是一个标记内部辅助宏的约定(另请参见:"The little book of Macros"StackOverflow question).


Push down accumulationthis 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 中学到了这个技巧:一个宏判断两个标识符是否相等.但不幸的是,我再也找不到这个箱子了.如果你知道那箱子的名字,请告诉我!


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

  • 作为一个增量TT-muncher,它对输入中的每个令牌进行递归.所以很容易达到递归极限(可以增加,但不是最优的).可以编写这个宏的非递归版本,但到目前为止,我还没有找到一种方法.

  • macro_rules!宏在标识符方面有点奇怪.上面给出的解决方案可能会表现出奇怪的行为,使用self作为标识符.有关该主题的更多信息,请参见this chapter.


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!宏那样存在递归限制问题.

Rust相关问答推荐

关于Rust 中回归的逻辑

rust 蚀生命周期 行为

通过解引用将值移出Box(以及它被脱糖到什么地方)?

在Rust中是否可以使用Rc自动化约束传播

装箱特性如何影响传递给它的参数的生命周期 ?(举一个非常具体的例子)

处理带有panic 的 Err 时,匹配臂具有不兼容的类型

.在 Rust 模块标识符中

如何正确使用git2::Remote::push?

一旦令牌作为文字使用,声明宏不匹配硬编码值?

max(ctz(x), ctz(y)) 有更快的算法吗?

如何在 Rust 中将枚举变体转换为 u8?

Rust:`sort_by` 多个条件,冗长的模式匹配

如何将 Rust 中的树状 struct 展平为 Vec<&mut ...>?

如果我不想运行析构函数,如何移出具有析构函数的 struct ?

意外的正则表达式模式匹配

当 `T` 没有实现 `Debug` 时替代 `unwrap()`

使用 `.` 将 T 转换为 &mut T?

Iterator::collect如何进行转换?

如何在 Rust 中使用特征标志来捕获多行代码?

在 `Cow` 上实现 `AsRef` 和 `Borrow`