1. 1. Introduction
  2. 2. 宏,彻底剖析
    1. 2.1. 语法扩展
      1. 2.1.1. 源码解析过程
      2. 2.1.2. AST中的宏
      3. 2.1.3. 展开
    2. 2.2. macro_rules!
    3. 2.3. 细枝末节
      1. 2.3.1. 再探捕获与展开
      2. 2.3.2. 卫生性
      3. 2.3.3. 不是标识符的标识符
      4. 2.3.4. 调试
      5. 2.3.5. 作用域
      6. 2.3.6. 导入/导出
  3. 3. 宏,实践介绍
  4. 4. 常用模式
    1. 4.1. 回调
    2. 4.2. 标记树撕咬机
    3. 4.3. 内用规则
    4. 4.4. 下推累积
    5. 4.5. 重复替代
    6. 4.6. 尾部分隔符
    7. 4.7. 标记树聚束
    8. 4.8. 可见性
    9. 4.9. 临时措施
  5. 5. 轮子
    1. 5.1. AST强转
    2. 5.2. 计数
    3. 5.3. 枚举解析
  6. 6. 实例注解
    1. 6.1. Ook!

宏,实践介绍

本章节将通过一个相对简单、可行的例子来介绍Rust的“示例宏”系统。我们将不会试图解释整个宏系统错综复杂的构造;而是试图让读者能够舒适地了解宏的书写方式,以及为何如斯。

Rust官方教程中也有一章讲解宏(中文版),同样提供了高层面的讲解。同时,本书也有一章更富条理的介绍,旨在详细阐释宏系统。

一点背景知识

注意:别慌!我们通篇只会涉及到下面这一点点数学。如果想直接看重点,本小节可被安全跳过。

如果你不了解,所谓“递推(recurrence)关系”是指这样一个序列,其中的每个值都由先前的一个或多个值决定,并最终由一个或多个初始值完全决定。举例来说,Fibonacci数列可被定义为如下关系:

Fn=0,1,,Fn1+Fn-2

即,序列的前两个数分别为0和1,而第3个则为F0 + F1 = 0 + 1 = 1,第4个为F1 + F2 = 1 + 1 = 2,依此类推。

由于这列值可以永远持续下去,定义一个fibonacci的求值函数略显困难。显然,返回一整列值并不实际。我们真正需要的,应是某种具有怠惰求值性质的东西——只在必要的时候才进行运算求值。

在Rust中,这样的需求表明,是Iterator派上用场的时候了。实现迭代器并不十分困难,但比较繁琐:你得自定义一个类型,弄明白该在其中存储什么,然后为它实现Iterator trait。

其实,递推关系足够简单;几乎所有的递推关系都可被抽象出来,变成一小段由宏驱动的代码生成机制。

好了,说得已经足够多了,让我们开始干活吧。

构建过程

通常来说,在构建新宏时,我所做的第一件事,是决定宏调用的形式。在我们当前所讨论的情况下,我的初次尝试是这样:

let fib = recurrence![a[n] = 0, 1, ..., a[n-1] + a[n-2]];

for e in fib.take(10) { println!("{}", e) }Run

以此为基点,我们可以向宏的定义迈出第一步,即便在此时我们尚不了解该宏的展开部分究竟是什么样子。此步骤的用处在于,如果在此处无法明确如何解析输入语法,那就可能意味着,整个宏的构思需要改变。

macro_rules! recurrence {
    ( a[n] = $($inits:expr),+ , ... , $recur:expr ) => { /* ... */ };
}Run

假装你并不熟悉相应的语法,让我来解释。上述代码块使用macro_rules!系统定义了一个宏,称为recurrence!。此宏仅包含一条解析规则,它规定,此宏必须依次匹配下列项目:

最后,规则声明,如果输入被成功匹配,则对该宏的调用将被标记序列/* ... */替换。

值得注意的是,inits,如它命名采用的复数形式所暗示的,实际上包含所有成功匹配进此重复的表达式,而不仅是第一或最后一个。不仅如此,它们将被捕获成一个序列,而不是——举例说——把它们不可逆地粘贴在一起。还注意到,可用*替换+来表示允许“0或多个”重复。宏系统并不支持“0或1个”或任何其它更加具体的重复形式。

作为练习,我们将采用上面提及的输入,并研究它被处理的过程。“位置”列将揭示下一个需要被匹配的句法模式,由“⌂”标出。注意在某些情况下下一个可用元素可能存在多个。“输入”将包括所有尚未被消耗的标记。initsrecur将分别包含其对应绑定的内容。

位置 输入 inits recur
a[n] = $($inits:expr),+ , ... , $recur:expr a[n] = 0, 1, ..., a[n-1] + a[n-2]
a[n] = $($inits:expr),+ , ... , $recur:expr [n] = 0, 1, ..., a[n-1] + a[n-2]
a[n] = $($inits:expr),+ , ... , $recur:expr n] = 0, 1, ..., a[n-1] + a[n-2]
a[n] = $($inits:expr),+ , ... , $recur:expr ] = 0, 1, ..., a[n-1] + a[n-2]
a[n] = $($inits:expr),+ , ... , $recur:expr = 0, 1, ..., a[n-1] + a[n-2]
a[n] = $($inits:expr),+ , ... , $recur:expr 0, 1, ..., a[n-1] + a[n-2]
a[n] = $($inits:expr),+ , ... , $recur:expr 0, 1, ..., a[n-1] + a[n-2]
a[n] = $($inits:expr),+ , ... , $recur:expr ⌂ ⌂ , 1, ..., a[n-1] + a[n-2] 0
注意:这有两个 ⌂,因为下个输入标记既能匹配 重复元素间的分隔符逗号,也能匹配 标志重复结束的逗号。宏系统将同时追踪这两种可能,直到决定具体选择为止。
a[n] = $($inits:expr),+ , ... , $recur:expr ⌂ ⌂ 1, ..., a[n-1] + a[n-2] 0
a[n] = $($inits:expr),+ , ... , $recur:expr ⌂ ⌂ , ..., a[n-1] + a[n-2] 0, 1
注意:第三个被划掉的记号表明,基于上个被消耗的标记,宏系统排除了一项先前存在的可能。
a[n] = $($inits:expr),+ , ... , $recur:expr ⌂ ⌂ ..., a[n-1] + a[n-2] 0, 1
a[n] = $($inits:expr),+ , ... , $recur:expr , a[n-1] + a[n-2] 0, 1
a[n] = $($inits:expr),+ , ... , $recur:expr a[n-1] + a[n-2] 0, 1
a[n] = $($inits:expr),+ , ... , $recur:expr 0, 1 a[n-1] + a[n-2]
注意:这一步表明,类似$recur:expr的绑定将消耗一个完整的表达式。此处,究竟什么算是一个完整的表达式,将由编译器决定。稍后我们会谈到语言其它部分的类似行为。

从此表中得到的最关键收获在于,宏系统会依次尝试将提供给它的每个标记当作输入,与提供给它的每条规则进行匹配。我们稍后还将谈回到这一“尝试”。

接下来我们首先将写出宏调用完全展开后的形态。我们想要的结构类似:

let fib = {
    struct Recurrence {
        mem: [u64; 2],
        pos: usize,
    }Run

它就是我们实际使用的迭代器类型。其中,mem负责存储最近算得的两个斐波那契值,保证递推计算能够顺利进行;pos则负责记录当前的n值。

附注:此处选用u64是因为,对斐波那契数列来说,它已经“足够”了。先不必担心它是否适用于其它数列,我们会提到这一点的。

    impl Iterator for Recurrence {
        type Item = u64;

        #[inline]
        fn next(&mut self) -> Option<u64> {
            if self.pos < 2 {
                let next_val = self.mem[self.pos];
                self.pos += 1;
                Some(next_val)Run

我们需要这个if分支来返回序列的初始值,没什么花哨。

            } else {
                let a = /* something */;
                let n = self.pos;
                let next_val = (a[n-1] + a[n-2]);

                self.mem.TODO_shuffle_down_and_append(next_val);

                self.pos += 1;
                Some(next_val)
            }
        }
    }Run

这段稍微难办一点。对于具体如何定义a,我们稍后再提。TODO_shuffle_down_and_append的真面目也将留到稍后揭晓;我们想让它做到:将next_val放至数组末尾,并将数组中剩下的元素依次前移一格,最后丢掉原先的首元素。


    Recurrence { mem: [0, 1], pos: 0 }
};

for e in fib.take(10) { println!("{}", e) }Run

最后,我们返回一个该结构的实例。在随后的代码中,我们将用它来进行迭代。综上所述,完整的展开应该如下:

let fib = {
    struct Recurrence {
        mem: [u64; 2],
        pos: usize,
    }

    impl Iterator for Recurrence {
        type Item = u64;

        #[inline]
        fn next(&mut self) -> Option<u64> {
            if self.pos < 2 {
                let next_val = self.mem[self.pos];
                self.pos += 1;
                Some(next_val)
            } else {
                let a = /* something */;
                let n = self.pos;
                let next_val = (a[n-1] + a[n-2]);

                self.mem.TODO_shuffle_down_and_append(next_val.clone());

                self.pos += 1;
                Some(next_val)
            }
        }
    }

    Recurrence { mem: [0, 1], pos: 0 }
};

for e in fib.take(10) { println!("{}", e) }Run

附注:是的,这样做的确意味着每次调用该宏时,我们都会重新定义并实现一个Recurrence结构。如果#[inline]属性应用得当,在最终编译出的二进制文件中,大部分冗余都将被优化掉。

在写展开部分的过程中时常检查,也是一个有效的技巧。如果在过程中发现,展开中的某些内容需要根据调用的不同发生改变,但这些内容并未被我们的宏语法定义囊括;那就要去考虑,应该怎样去引入它们。在此示例中,我们先前用过一次u64,但调用端想要的类型不一定是它;然而我们的宏语法并没有提供其它选择。因此,我们可以做一些修改。

macro_rules! recurrence {
    ( a[n]: $sty:ty = $($inits:expr),+ , ... , $recur:expr ) => { /* ... */ };
}

/*
let fib = recurrence![a[n]: u64 = 0, 1, ..., a[n-1] + a[n-2]];

for e in fib.take(10) { println!("{}", e) }
*/Run

我们加入了一个新的捕获sty,它应是一个类型(type)。

附注:如果你不清楚的话,在捕获冒号之后的部分,可是几种语法匹配候选项之一。最常用的包括itemexprty。完整的解释可在宏,彻底解析-macro_rules!-捕获部分找到。

还有一点值得注意:为方便语言的未来发展,对于跟在某些特定的匹配之后的标记,编译器施加了一些限制。这种情况常在试图匹配至表达式(expression)或语句(statement)时出现:它们后面仅允许跟进=>,;这些标记之一。

完整清单可在宏,彻底解析-细枝末节-再探捕获与展开找到。

索引与移位

在此节中我们将略去一些实际上与宏的联系不甚紧密的内容。本节我们的目标是,让用户可以通过索引a来访问数列中先前的值。a应该如同一个切口,让我们得以持续访问数列中最近几个(在本例中,两个)值。

通过采用封装类,我们可以相对简单地做到这点:

struct IndexOffset<'a> {
    slice: &'a [u64; 2],
    offset: usize,
}

impl<'a> Index<usize> for IndexOffset<'a> {
    type Output = u64;

    #[inline(always)]
    fn index<'b>(&'b self, index: usize) -> &'b u64 {
        use std::num::Wrapping;

        let index = Wrapping(index);
        let offset = Wrapping(self.offset);
        let window = Wrapping(2);

        let real_index = index - offset + window;
        &self.slice[real_index.0]
    }
}Run

附注:对于新接触Rust的人来说,生命周期的概念经常需要一番思考。我们给出一些简单的解释:'a'b是生命周期参数,它们被用于记录引用(即一个指向某些数据的借用指针)的有效期。在此例中,IndexOffset借用了一个指向我们迭代器数据的引用,因此,它需要记录该引用的有效期,记录者正是'a

我们用到'b,是因为Index::index函数(下标句法正是通过此函数实现的)的一个参数也需要生命周期。'a'b不一定在所有情况下都相同。我们并没有显式地声明'a'b之间有任何联系,但借用检查器(borrow checker)总会确保内存安全性不被意外破坏。

a地定义将随之变为:

let a = IndexOffset { slice: &self.mem, offset: n };Run

如何处理TODO_shuffle_down_and_append是我们现在剩下的唯一问题了。我没能在标准库中寻得可以直接使用的方法,但自己造一个出来并不难。

{
    use std::mem::swap;

    let mut swap_tmp = next_val;
    for i in (0..2).rev() {
        swap(&mut swap_tmp, &mut self.mem[i]);
    }
}Run

它把新值替换至数组末尾,并把其他值向前移动一位。

附注:采用这种做法,将使得我们的代码可同时被用于不可拷贝(non-copyable)的类型。

至此,最终起作用的代码将是:

macro_rules! recurrence {
    ( a[n]: $sty:ty = $($inits:expr),+ , ... , $recur:expr ) => { /* ... */ };
}

fn main() {
    /*
    let fib = recurrence![a[n]: u64 = 0, 1, ..., a[n-1] + a[n-2]];

    for e in fib.take(10) { println!("{}", e) }
    */
    let fib = {
        use std::ops::Index;

        struct Recurrence {
            mem: [u64; 2],
            pos: usize,
        }

        struct IndexOffset<'a> {
            slice: &'a [u64; 2],
            offset: usize,
        }

        impl<'a> Index<usize> for IndexOffset<'a> {
            type Output = u64;

            #[inline(always)]
            fn index<'b>(&'b self, index: usize) -> &'b u64 {
                use std::num::Wrapping;

                let index = Wrapping(index);
                let offset = Wrapping(self.offset);
                let window = Wrapping(2);

                let real_index = index - offset + window;
                &self.slice[real_index.0]
            }
        }

        impl Iterator for Recurrence {
            type Item = u64;

            #[inline]
            fn next(&mut self) -> Option<u64> {
                if self.pos < 2 {
                    let next_val = self.mem[self.pos];
                    self.pos += 1;
                    Some(next_val)
                } else {
                    let next_val = {
                        let n = self.pos;
                        let a = IndexOffset { slice: &self.mem, offset: n };
                        (a[n-1] + a[n-2])
                    };

                    {
                        use std::mem::swap;

                        let mut swap_tmp = next_val;
                        for i in (0..2).rev() {
                            swap(&mut swap_tmp, &mut self.mem[i]);
                        }
                    }

                    self.pos += 1;
                    Some(next_val)
                }
            }
        }

        Recurrence { mem: [0, 1], pos: 0 }
    };

    for e in fib.take(10) { println!("{}", e) }
}Run

注意我们改变了na的声明顺序,同时将它们(与递推表达式一同)用一个新区块包裹了起来。改变声明顺序的理由很明显(n得在a前被定义才能被a使用)。而包裹的理由则是:如果不,借用引用&self.mem将会阻止随后的swap操作(在某物仍存在其它别名时,无法对其进行改变)。包裹区块将确保&self.mem产生的借用在彼时过期。

顺带一提,将交换mem的代码包进区块里的唯一原因,正是为了缩减std::mem::swap的可用范畴,以保持代码整洁。

如果我们直接拿上段代码来跑,将会得到:

0
1
1
2
3
5
8
13
21
34

成功了!现在,让我们把这段代码复制粘贴进宏的展开部分,并把它们原本所在的位置换成一次宏调用。这样我们得到:

macro_rules! recurrence {
    ( a[n]: $sty:ty = $($inits:expr),+ , ... , $recur:expr ) => {
        {
            /*
                What follows here is *literally* the code from before,
                cut and pasted into a new position.  No other changes
                have been made.
            */

            use std::ops::Index;

            struct Recurrence {
                mem: [u64; 2],
                pos: usize,
            }

            struct IndexOffset<'a> {
                slice: &'a [u64; 2],
                offset: usize,
            }

            impl<'a> Index<usize> for IndexOffset<'a> {
                type Output = u64;

                #[inline(always)]
                fn index<'b>(&'b self, index: usize) -> &'b u64 {
                    use std::num::Wrapping;

                    let index = Wrapping(index);
                    let offset = Wrapping(self.offset);
                    let window = Wrapping(2);

                    let real_index = index - offset + window;
                    &self.slice[real_index.0]
                }
            }

            impl Iterator for Recurrence {
                type Item = u64;

                #[inline]
                fn next(&mut self) -> Option<u64> {
                    if self.pos < 2 {
                        let next_val = self.mem[self.pos];
                        self.pos += 1;
                        Some(next_val)
                    } else {
                        let next_val = {
                            let n = self.pos;
                            let a = IndexOffset { slice: &self.mem, offset: n };
                            (a[n-1] + a[n-2])
                        };

                        {
                            use std::mem::swap;

                            let mut swap_tmp = next_val;
                            for i in (0..2).rev() {
                                swap(&mut swap_tmp, &mut self.mem[i]);
                            }
                        }

                        self.pos += 1;
                        Some(next_val)
                    }
                }
            }

            Recurrence { mem: [0, 1], pos: 0 }
        }
    };
}

fn main() {
    let fib = recurrence![a[n]: u64 = 0, 1, ..., a[n-1] + a[n-2]];

    for e in fib.take(10) { println!("{}", e) }
}Run

显然,宏的捕获尚未被用到,但这点很好改。不过,如果尝试编译上述代码,rustc会中止,并显示:

recurrence.rs:69:45: 69:48 error: local ambiguity: multiple parsing options: built-in NTs expr ('inits') or 1 other options.
recurrence.rs:69     let fib = recurrence![a[n]: u64 = 0, 1, ..., a[n-1] + a[n-2]];
                                                             ^~~

这里我们撞上了macro_rules的一处限制。问题出在那第二个逗号上。当在展开过程中遇见它时,编译器无法决定是该将它解析成inits中的又一个表达式,还是解析成...。很遗憾,它不够聪明,没办法意识到...不是一个有效的表达式,所以它选择了放弃。理论上来说,上述代码应该能奏效,但当前它并不能。

附注:有关宏系统如何解读我们的规则,我之前的确撒了点小谎。通常来说,宏系统确实应当如我前述的那般运作,但在这里它没有。macro_rules的机制,由此看来,是存在一些小毛病的;我们得记得偶尔去做一些调控,好让它我们期许的那般运作。

在本例中,问题有两个。其一,宏系统不清楚各式各样的语法元素(如表达式)可由什么样的东西构成,或不能由什么样的东西构成;那是语法解析器的工作。其二,在试图捕获复合语法元素(如表达式)的过程中,它无法不100%地首先陷入该捕获中去。

换句话说,宏系统可以向语法解析器发出请求,让后者试图把某段输入当作表达式来进行解析;但此间无论语法解析器遇见任何问题,都将中止整个进程以示回应。目前,宏系统处理这种窘境的唯一方式,就是对任何可能产生此类问题的情境加以禁止。

好的一面在于,对于这摊子情况,没有任何人感到高兴。关键词macro早已被预留,以备未来更加严密的宏系统使用。直到那天来临之前,我们还是只得该怎么做就怎么做。

还好,修正方案也很简单:从宏句法中去掉逗号即可。出于平衡考量,我们将移除...双边的逗号:1

macro_rules! recurrence {
    ( a[n]: $sty:ty = $($inits:expr),+ ... $recur:expr ) => {
//                                     ^~~ changed
        /* ... */
    };
}

fn main() {
    let fib = recurrence![a[n]: u64 = 0, 1 ... a[n-1] + a[n-2]];
//                                         ^~~ changed

    for e in fib.take(10) { println!("{}", e) }
}Run

成功!现在,我们该将捕获部分捕获到的内容替代进展开部分中了。

替换

在宏中替换你捕获到的内容相对简单,通过$sty:ty捕获到的内容可用$sty来替换。好,让我们换掉那些u64吧:

macro_rules! recurrence {
    ( a[n]: $sty:ty = $($inits:expr),+ ... $recur:expr ) => {
        {
            use std::ops::Index;

            struct Recurrence {
                mem: [$sty; 2],
//                    ^~~~ changed
                pos: usize,
            }

            struct IndexOffset<'a> {
                slice: &'a [$sty; 2],
//                          ^~~~ changed
                offset: usize,
            }

            impl<'a> Index<usize> for IndexOffset<'a> {
                type Output = $sty;
//                            ^~~~ changed

                #[inline(always)]
                fn index<'b>(&'b self, index: usize) -> &'b $sty {
//                                                          ^~~~ changed
                    use std::num::Wrapping;

                    let index = Wrapping(index);
                    let offset = Wrapping(self.offset);
                    let window = Wrapping(2);

                    let real_index = index - offset + window;
                    &self.slice[real_index.0]
                }
            }

            impl Iterator for Recurrence {
                type Item = $sty;
//                          ^~~~ changed

                #[inline]
                fn next(&mut self) -> Option<$sty> {
//                                           ^~~~ changed
                    /* ... */
                }
            }

            Recurrence { mem: [1, 1], pos: 0 }
        }
    };
}

fn main() {
    let fib = recurrence![a[n]: u64 = 0, 1 ... a[n-1] + a[n-2]];

    for e in fib.take(10) { println!("{}", e) }
}Run

现在让我们来尝试更难的:如何将inits同时转变为字面值[0, 1]以及数组类型[$sty; 2]。首先我们试试:

            Recurrence { mem: [$($inits),+], pos: 0 }
//                             ^~~~~~~~~~~ changedRun

此段代码与捕获的效果正好相反:将inits捕得的内容排列开来,总共有1或多次,每条内容之间用逗号分隔。展开的结果与期望一致,我们得到标记序列:0, 1

不过,通过inits转换出字面值2需要一些技巧。没有直接可行的方法,但我们可以通过另一个宏做到。我们一步一步来。

macro_rules! count_exprs {
    /* ??? */
}Run

先写显而易见的情况:未给表达式时,我们期望count_exprs展开为字面值0

macro_rules! count_exprs {
    () => (0);
//  ^~~~~~~~~~ added
}Run

附注:你可能已经注意到了,这里的展开部分我用的是括号而非花括号。macro_rules其实不关心你用的是什么,只要它成对匹配即可:( ){ }[ ]。实际上,宏本身的匹配符(即紧跟宏名称后的匹配符)、语法规则外的匹配符及相应展开部分外的匹配符都可以替换。

调用宏时的括号也可被替换,但有些限制:当宏被以{...}(...);形式调用时,它总是会被解析为一个条目(item,比如,structfn声明)。在函数体内部时,这一特征很重要,它将消除“解析成表达式”和“解析成语句”之间的歧义。

有一个表达式的情况该怎么办?应该展开为字面值1

macro_rules! count_exprs {
    () => (0);
    ($e:expr) => (1);
//  ^~~~~~~~~~~~~~~~~ added
}Run

两个呢?

macro_rules! count_exprs {
    () => (0);
    ($e:expr) => (1);
    ($e0:expr, $e1:expr) => (2);
//  ^~~~~~~~~~~~~~~~~~~~~~~~~~~~ added
}Run

通过递归调用重新表达,我们可将扩展部分“精简”出来:

macro_rules! count_exprs {
    () => (0);
    ($e:expr) => (1);
    ($e0:expr, $e1:expr) => (1 + count_exprs!($e1));
//                           ^~~~~~~~~~~~~~~~~~~~~ changed
}Run

这样做可行是因为,Rust可将1 + 1合并成一个常量。那么,三种表达式的情况呢?

macro_rules! count_exprs {
    () => (0);
    ($e:expr) => (1);
    ($e0:expr, $e1:expr) => (1 + count_exprs!($e1));
    ($e0:expr, $e1:expr, $e2:expr) => (1 + count_exprs!($e1, $e2));
//  ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ added
}Run

附注:你可能会想,我们是否能翻转这些规则的排列顺序。在此情境下,可以。但在有些情况下,宏系统可能会对此挑剔。如果你发现自己有一个包含多项规则的宏系统老是报错,或给出期望外的结果;但你发誓它应该能用,试着调换一下规则的排序吧。

我们希望你现在已经能看出规律。通过匹配至一个表达式加上0或多个表达式并展开成1+a,我们可以减少规则列表的数目:

macro_rules! count_exprs {
    () => (0);
    ($head:expr) => (1);
    ($head:expr, $($tail:expr),*) => (1 + count_exprs!($($tail),*));
//  ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ changed
}Run

仅对此例:这段代码并非计数仅有或其最好的方法。若有兴趣,稍后可以研读计数一节。

有此工具后,我们可再次修改recurrence,确定mem所需的大小。

// added:
macro_rules! count_exprs {
    () => (0);
    ($head:expr) => (1);
    ($head:expr, $($tail:expr),*) => (1 + count_exprs!($($tail),*));
}

macro_rules! recurrence {
    ( a[n]: $sty:ty = $($inits:expr),+ ... $recur:expr ) => {
        {
            use std::ops::Index;

            const MEM_SIZE: usize = count_exprs!($($inits),+);
//          ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ added

            struct Recurrence {
                mem: [$sty; MEM_SIZE],
//                          ^~~~~~~~ changed
                pos: usize,
            }

            struct IndexOffset<'a> {
                slice: &'a [$sty; MEM_SIZE],
//                                ^~~~~~~~ changed
                offset: usize,
            }

            impl<'a> Index<usize> for IndexOffset<'a> {
                type Output = $sty;

                #[inline(always)]
                fn index<'b>(&'b self, index: usize) -> &'b $sty {
                    use std::num::Wrapping;

                    let index = Wrapping(index);
                    let offset = Wrapping(self.offset);
                    let window = Wrapping(MEM_SIZE);
//                                        ^~~~~~~~ changed

                    let real_index = index - offset + window;
                    &self.slice[real_index.0]
                }
            }

            impl Iterator for Recurrence {
                type Item = $sty;

                #[inline]
                fn next(&mut self) -> Option<$sty> {
                    if self.pos < MEM_SIZE {
//                                ^~~~~~~~ changed
                        let next_val = self.mem[self.pos];
                        self.pos += 1;
                        Some(next_val)
                    } else {
                        let next_val = {
                            let n = self.pos;
                            let a = IndexOffset { slice: &self.mem, offset: n };
                            (a[n-1] + a[n-2])
                        };

                        {
                            use std::mem::swap;

                            let mut swap_tmp = next_val;
                            for i in (0..MEM_SIZE).rev() {
//                                       ^~~~~~~~ changed
                                swap(&mut swap_tmp, &mut self.mem[i]);
                            }
                        }

                        self.pos += 1;
                        Some(next_val)
                    }
                }
            }

            Recurrence { mem: [$($inits),+], pos: 0 }
        }
    };
}
/* ... */Run

完成之后,我们开始替换最后的recur表达式。

/* ... */
                #[inline]
                fn next(&mut self) -> Option<u64> {
                    if self.pos < MEMORY {
                        let next_val = self.mem[self.pos];
                        self.pos += 1;
                        Some(next_val)
                    } else {
                        let next_val = {
                            let n = self.pos;
                            let a = IndexOffset { slice: &self.mem, offset: n };
                            $recur
//                          ^~~~~~ changed
                        };
                        {
                            use std::mem::swap;
                            let mut swap_tmp = next_val;
                            for i in range(0, MEMORY).rev() {
                                swap(&mut swap_tmp, &mut self.mem[i]);
                            }
                        }
                        self.pos += 1;
                        Some(next_val)
                    }
                }
/* ... */Run

现在试图编译的话...

recurrence.rs:77:48: 77:49 error: unresolved name `a`
recurrence.rs:77     let fib = recurrence![a[n]: u64 = 0, 1 ... a[n-1] + a[n-2]];
                                                                ^
recurrence.rs:7:1: 74:2 note: in expansion of recurrence!
recurrence.rs:77:15: 77:64 note: expansion site
recurrence.rs:77:50: 77:51 error: unresolved name `n`
recurrence.rs:77     let fib = recurrence![a[n]: u64 = 0, 1 ... a[n-1] + a[n-2]];
                                                                  ^
recurrence.rs:7:1: 74:2 note: in expansion of recurrence!
recurrence.rs:77:15: 77:64 note: expansion site
recurrence.rs:77:57: 77:58 error: unresolved name `a`
recurrence.rs:77     let fib = recurrence![a[n]: u64 = 0, 1 ... a[n-1] + a[n-2]];
                                                                         ^
recurrence.rs:7:1: 74:2 note: in expansion of recurrence!
recurrence.rs:77:15: 77:64 note: expansion site
recurrence.rs:77:59: 77:60 error: unresolved name `n`
recurrence.rs:77     let fib = recurrence![a[n]: u64 = 0, 1 ... a[n-1] + a[n-2]];
                                                                           ^
recurrence.rs:7:1: 74:2 note: in expansion of recurrence!
recurrence.rs:77:15: 77:64 note: expansion site

...等等,什么情况?这没道理...让我们看看宏究竟展开成了什么样子。

$ rustc -Z unstable-options --pretty expanded recurrence.rs

参数--pretty expanded将促使rustc展开宏,并将输出的AST再重转为源代码。此选项当前被认定为是unstable,因此我们还要添加-Z unstable-options。输出的信息(经过整理格式后)如下;特别留意$recur被替换掉的位置:

#![feature(no_std)]
#![no_std]
#[prelude_import]
use std::prelude::v1::*;
#[macro_use]
extern crate std as std;
fn main() {
    let fib = {
        use std::ops::Index;
        const MEM_SIZE: usize = 1 + 1;
        struct Recurrence {
            mem: [u64; MEM_SIZE],
            pos: usize,
        }
        struct IndexOffset<'a> {
            slice: &'a [u64; MEM_SIZE],
            offset: usize,
        }
        impl <'a> Index<usize> for IndexOffset<'a> {
            type Output = u64;
            #[inline(always)]
            fn index<'b>(&'b self, index: usize) -> &'b u64 {
                use std::num::Wrapping;
                let index = Wrapping(index);
                let offset = Wrapping(self.offset);
                let window = Wrapping(MEM_SIZE);
                let real_index = index - offset + window;
                &self.slice[real_index.0]
            }
        }
        impl Iterator for Recurrence {
            type Item = u64;
            #[inline]
            fn next(&mut self) -> Option<u64> {
                if self.pos < MEM_SIZE {
                    let next_val = self.mem[self.pos];
                    self.pos += 1;
                    Some(next_val)
                } else {
                    let next_val = {
                        let n = self.pos;
                        let a = IndexOffset{slice: &self.mem, offset: n,};
                        a[n - 1] + a[n - 2]
                    };
                    {
                        use std::mem::swap;
                        let mut swap_tmp = next_val;
                        {
                            let result =
                                match ::std::iter::IntoIterator::into_iter((0..MEM_SIZE).rev()) {
                                    mut iter => loop {
                                        match ::std::iter::Iterator::next(&mut iter) {
                                            ::std::option::Option::Some(i) => {
                                                swap(&mut swap_tmp, &mut self.mem[i]);
                                            }
                                            ::std::option::Option::None => break,
                                        }
                                    },
                                };
                            result
                        }
                    }
                    self.pos += 1;
                    Some(next_val)
                }
            }
        }
        Recurrence{mem: [0, 1], pos: 0,}
    };
    {
        let result =
            match ::std::iter::IntoIterator::into_iter(fib.take(10)) {
                mut iter => loop {
                    match ::std::iter::Iterator::next(&mut iter) {
                        ::std::option::Option::Some(e) => {
                            ::std::io::_print(::std::fmt::Arguments::new_v1(
                                {
                                    static __STATIC_FMTSTR: &'static [&'static str] = &["", "\n"];
                                    __STATIC_FMTSTR
                                },
                                &match (&e,) {
                                    (__arg0,) => [::std::fmt::ArgumentV1::new(__arg0, ::std::fmt::Display::fmt)],
                                }
                            ))
                        }
                        ::std::option::Option::None => break,
                    }
                },
            };
        result
    }
}Run

呃..这看起来完全合法!如果我们加上几条#![feature(...)]属性,并把它送去给一个nightly版本的rustc,甚至真能通过编译...究竟什么情况?!

附注:上述代码无法通过非nightly版rustc编译。这是因为,println!宏的展开结果依赖于编译器内部的细节,这些细节尚未被公开稳定化。

保持卫生

这儿的问题在于,Rust宏中的标识符具有卫生性。这就是说,出自不同上下文的标识符不可能发生冲突。作为演示,举个简单的例子。

macro_rules! using_a {
    ($e:expr) => {
        {
            let a = 42i;
            $e
        }
    }
}

let four = using_a!(a / 10);Run

此宏接受一个表达式,然后把它包进一个定义了变量a的区块里。我们随后用它绕个弯子来求4。这个例子中实际上存在2种句法上下文,但我们看不见它们。为了帮助说明,我们给每个上下文都上一种不同的颜色。我们从未展开的代码开始上色,此时仅看得见一种上下文:

macro_rules! using_a {
    ($e:expr) => {
        {
            let a = 42;
            $e
        }
    }
}

let four = using_a!(a / 10);

现在,展开宏调用。

let four = {
    let a = 42;
    a / 10
};

可以看到,在宏中定义的a与调用所提供的a处于不同的上下文中。因此,虽然它们的字母表示一致,编译器仍将它们视作完全不同的标识符。

宏的这一特性需要格外留意:它们可能会产出无法通过编译的AST;但同样的代码,手写或通过--pretty expanded转印出来则能够通过编译。

解决方案是,采用合适的句法上下文来捕获标识符。我们沿用上例,并作修改:

macro_rules! using_a {
    ($a:ident, $e:expr) => {
        {
            let $a = 42;
            $e
        }
    }
}

let four = using_a!(a, a / 10);

现在它将展开为:

let four = {
    let a = 42;
    a / 10
};

上下文现在匹配了,编译通过。我们的recurrence!宏也可被如此调整:显式地捕获an即可。调整后我们得到:

macro_rules! count_exprs {
    () => (0);
    ($head:expr) => (1);
    ($head:expr, $($tail:expr),*) => (1 + count_exprs!($($tail),*));
}

macro_rules! recurrence {
    ( $seq:ident [ $ind:ident ]: $sty:ty = $($inits:expr),+ ... $recur:expr ) => {
//    ^~~~~~~~~~   ^~~~~~~~~~ changed
        {
            use std::ops::Index;

            const MEM_SIZE: usize = count_exprs!($($inits),+);

            struct Recurrence {
                mem: [$sty; MEM_SIZE],
                pos: usize,
            }

            struct IndexOffset<'a> {
                slice: &'a [$sty; MEM_SIZE],
                offset: usize,
            }

            impl<'a> Index<usize> for IndexOffset<'a> {
                type Output = $sty;

                #[inline(always)]
                fn index<'b>(&'b self, index: usize) -> &'b $sty {
                    use std::num::Wrapping;

                    let index = Wrapping(index);
                    let offset = Wrapping(self.offset);
                    let window = Wrapping(MEM_SIZE);

                    let real_index = index - offset + window;
                    &self.slice[real_index.0]
                }
            }

            impl Iterator for Recurrence {
                type Item = $sty;

                #[inline]
                fn next(&mut self) -> Option<$sty> {
                    if self.pos < MEM_SIZE {
                        let next_val = self.mem[self.pos];
                        self.pos += 1;
                        Some(next_val)
                    } else {
                        let next_val = {
                            let $ind = self.pos;
//                              ^~~~ changed
                            let $seq = IndexOffset { slice: &self.mem, offset: $ind };
//                              ^~~~ changed
                            $recur
                        };

                        {
                            use std::mem::swap;

                            let mut swap_tmp = next_val;
                            for i in (0..MEM_SIZE).rev() {
                                swap(&mut swap_tmp, &mut self.mem[i]);
                            }
                        }

                        self.pos += 1;
                        Some(next_val)
                    }
                }
            }

            Recurrence { mem: [$($inits),+], pos: 0 }
        }
    };
}

fn main() {
    let fib = recurrence![a[n]: u64 = 0, 1 ... a[n-1] + a[n-2]];

    for e in fib.take(10) { println!("{}", e) }
}Run

通过编译了!接下来,我们试试别的数列。

for e in recurrence!(f[i]: f64 = 1.0 ... f[i-1] * i as f64).take(10) {
    println!("{}", e)
}Run

运行上述代码得到:

1
1
2
6
24
120
720
5040
40320
362880

成功了!


  1. 在目前的稳定版本(Rust 1.14.0)下,去掉双边逗号后的代码无法通过编译。rustc报错“error: $inits:expr may be followed by ..., which is not allowed for expr fragments”。解决方案是将这两处逗号替换为其它字面值,如分号;与之前的捕获所用分隔符不同即可。