前言#
昨天我们学习了模式相关的内容,今天我们来学习构建block,即复用宏里面的代码。
AST强制转换#
rust解析器在面对tt置换(substitutions)的场景有些捉襟见肘,在解析器期望获得一块特殊的语法结构(grammar construct)时,发现是一坨替换了的tt tokens,那么这个时候就很容易出问题。解析器遇到这种场景会选择放弃解析它而不是尝试去解析。
macro_rules! as_expr { ($e:expr) => {$e} }
macro_rules! as_item { ($i:item) => {$i} }
macro_rules! as_pat { ($p:pat) => {$p} }
macro_rules! as_stmt { ($s:stmt) => {$s} }
macro_rules! as_ty { ($t:ty) => {$t} }
as_item!{struct Dummy;}
fn main() {
as_stmt!(let as_pat!(_): as_ty!(_) = as_expr!(42));
}上面这些AST强制转换一般用在下推式累积的宏,是为了让解析器将最终的tt序列当做是一种特殊的语法结构而不是tt,这样就可以避免上面提到的问题。
注意之所有只有这些特殊的宏是取决于宏的展开,而不是它们可以捕获什么。
计数#
接下来将介绍一些用于在macro_rules!宏中计数的技术,如果你只想知道哪个最好用,可以来这:look here
反复替换#
在宏里面做计数是一件比较难搞的事情。最简单的方案就是使用替换匹配的反复内容,比如:
macro_rules! replace_expr {
($_t:tt $sub:expr) => {$sub};
}
macro_rules! count_tts {
($($tts:tt)*) => {0usize $(+ replace_expr!($tts 1usize))*};
}
fn main() {
assert_eq!(count_tts!(0 1 2), 3);
}对于小数量的token序列来说,还行,但是500以上就炸了,让编译器执行宏500次类似下面这样:
0usize + 1usize + /* ~500 `+ 1usize`s */ + 1usize注意,是基于AST来计算的,所以这棵树会有500层那么深!
递归#
递归老朋友了,前面基本上例子都是基于递归的。
macro_rules! count_tts {
() => {0usize};
($_head:tt $($tail:tt)*) => {1usize + count_tts!($($tail)*)};
}一直到匹配不到token为止。
注意:在rustc 1.2,对于处理大量未知类型的字面量数字,编译器存在严重的性能问题,因为需要去推断每个字面量的类型。我们这里使用的是明确的usize类型来避免这个问题。如果usize不适合,那么你可以使用as关键字声明,比如0 as $ty, 1 as $ty等。
递归方案的问题还是那个老问题,有递归次数限制。
当然,你也可以写一堆的重复元变量来匹配,既然深度不满足,那就往广度拓展:
macro_rules! count_tts {
($_a:tt $_b:tt $_c:tt $_d:tt $_e:tt
$_f:tt $_g:tt $_h:tt $_i:tt $_j:tt
$_k:tt $_l:tt $_m:tt $_n:tt $_o:tt
$_p:tt $_q:tt $_r:tt $_s:tt $_t:tt
$($tail:tt)*)
=> {20usize + count_tts!($($tail)*)};
($_a:tt $_b:tt $_c:tt $_d:tt $_e:tt
$_f:tt $_g:tt $_h:tt $_i:tt $_j:tt
$($tail:tt)*)
=> {10usize + count_tts!($($tail)*)};
($_a:tt $_b:tt $_c:tt $_d:tt $_e:tt
$($tail:tt)*)
=> {5usize + count_tts!($($tail)*)};
($_a:tt
$($tail:tt)*)
=> {1usize + count_tts!($($tail)*)};
() => {0usize};
}
fn main() {
assert_eq!(700, count_tts!(
,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
// Repetition breaks somewhere after this
,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,, ,,,,,,,,,,
));
}切片(slice)长度#
第三个方法就是帮助编译器构造浅层(shallow)AST,这不会导致栈溢出。
我们可以基于构造一个数组字面量然后调用len的方法:
macro_rules! replace_expr {
($_t:tt $sub:expr) => {$sub};
}
macro_rules! count_tts {
($($tts:tt)*) => {<[()]>::len(&[$(replace_expr!($tts ())),*])};
}测试过支持10K+的token,甚至可以更高。
数组(array)长度#
上面的方法可以改进一下,使用一个稳定的泛型常量(Rust 1.51)。测试过在20K token的情况下仅稍慢于上面基于切片的方法,因为这是在常量上下文(const contexts)上执行的。
const fn count_helper<const N: usize>(_: [(); N]) -> usize { N }
macro_rules! replace_expr {
($_t:tt $sub:expr) => { $sub }
}
macro_rules! count_tts {
($($smth:tt)*) => {
count_helper([$(replace_expr!($smth ())),*])
}
}枚举(enum)计数#
这个方法可以用于计算一组(a set)相互不同的标识符的场景:
macro_rules! count_idents {
() => {0};
($last_ident:ident, $($idents:ident),* $(,)?) => {
{
#[allow(dead_code, non_camel_case_types)]
enum Idents { $($idents,)* $last_ident }
const COUNT: u32 = Idents::$last_ident as u32 + 1;
COUNT
}
};
}稍微有点抽象,实际上是基于枚举类型自动计数的特性
这个方法有两个缺点:
- 比如上面这个例子,它仅可以计算有效的标识符(并且不是关键字)
- 标识符不能重复
位扭动(Bit twiddling)#
最后一个要介绍的方法也是基于递归的方案,不过是基于位操作符的
macro_rules! count_tts {
() => { 0 };
($odd:tt $($a:tt $b:tt)*) => { (count_tts!($($a)*) << 1) | 1 };
($($a:tt $even:tt)*) => { count_tts!($($a)*) << 1 };
}还是需要依赖于tt递归,不过这种方法非常聪明,因为它实际上在偶数时将其输入减半,然后将计数器乘以 2(或者在这种情况下向左移动 1 位,这是等效的)。如果输入为奇数(uneven),也是减半运算,并将结果左移一位之后和十进制1做或运算,这相当于添加 1,因为此时由于先前的移位,最低位必须为 0。这样重复下去,直到匹配到规则 () => 0。
来看例子,比如我们的输入是:
count_tts!(0 0 0 0 0 0 0 0 0 0);这里有10个token,那么第一次调用走第三条规则,为什么呢?
- 因为第一条肯定不符合;
- 第二条则是因为长度问题,第一个
0单独拿出来导致后面只剩下9个0,这9个0并不能完全重复,即最终会有一个单独多出来的tt,所以也不满足
所以最终走了第三条规则,注意第三条规则是0 0为一组,所以$a的个数实际上只有5个,直接减了半,所以第一次调用的过程如下:
count_tts!(0 0 0 0 0) << 1然后这个时候剩下5个0,正好命中第二条规则,所以第二次调用即第一次递归会走第二规则,即:0 ((0 0), (0, 0)),这个时候$a只有两次,又压缩一半,那么第二次的过程如下:
(count_tts!(0, 0) << 1) | 1然后又符合第三条规则,第三次过程如下:
count_tts!(0) << 1然后第四次又符合第二条规则,过程如下:
count_tts!() << 1 | 1到这所有的0都没了,最后第五次符合第一条规则展开为0,
然后我们把这些个过程的中间产物和并:
(((0 << 1) | 1) << 1 << 1) | 1 << 1然后我们来计算下:
- (0 << 1) | 1 => 0 | 1 => 1
- (1 << 1 << 1) | 1 => 100 | 1 => 100 | 001 => 101
- 101 << 1 => 1010
注意这里做或运算的1是十进制的1,所以计算的时候需要转换成相应的二进制数1。

这种方法的时间复杂度是O(log(n))而不是O(n)。
当然,还是得受限于递归的层数限制。
算盘计数器#
临时信息:需要更有说服力的例子。 该用例采用 Rust 分组机制无法表示的匹配嵌套结构, 实在是过于特殊,因此不适作为例子使用。
注意,本篇需要先了解下推式累积和TT撕咬机。
这种技术可以用于一些需要保持跟踪动态的(varying)计数器的场景,这种计数器初始值为0或者接近0,然后支持一下操作:
- 加一
- 减一
- 可以和
0比较(或者任意固定、有限的值)
来看个用例:
macro_rules! abacus {
((- $($moves:tt)*) -> (+ $($count:tt)*)) => {
abacus!(($($moves)*) -> ($($count)*))
};
((- $($moves:tt)*) -> ($($count:tt)*)) => {
abacus!(($($moves)*) -> (- $($count)*))
};
((+ $($moves:tt)*) -> (- $($count:tt)*)) => {
abacus!(($($moves)*) -> ($($count)*))
};
((+ $($moves:tt)*) -> ($($count:tt)*)) => {
abacus!(($($moves)*) -> (+ $($count)*))
};
// Check if the final result is zero.
(() -> ()) => { true };
(() -> ($($count:tt)+)) => { false };
}
fn main() {
let equals_zero = abacus!((++-+-+++--++---++----+) -> ());
assert_eq!(equals_zero, true);
}顺便看下它的展开:
= note: expanding `abacus! { (++-+-+++--++---++----+) -> () }`
= note: to `abacus! ((+-+-+++--++---++----+) -> (+))`
= note: expanding `abacus! { (+-+-+++--++---++----+) -> (+) }`
= note: to `abacus! ((-+-+++--++---++----+) -> (+ +))`
= note: expanding `abacus! { (-+-+++--++---++----+) -> (+ +) }`
= note: to `abacus! ((+-+++--++---++----+) -> (+))`
= note: expanding `abacus! { (+-+++--++---++----+) -> (+) }`
= note: to `abacus! ((-+++--++---++----+) -> (+ +))`
= note: expanding `abacus! { (-+++--++---++----+) -> (+ +) }`
= note: to `abacus! ((+++--++---++----+) -> (+))`
= note: expanding `abacus! { (+++--++---++----+) -> (+) }`
= note: to `abacus! ((++--++---++----+) -> (+ +))`
= note: expanding `abacus! { (++--++---++----+) -> (+ +) }`
= note: to `abacus! ((+--++---++----+) -> (+ + +))`
= note: expanding `abacus! { (+--++---++----+) -> (+ + +) }`
= note: to `abacus! ((--++---++----+) -> (+ + + +))`
= note: expanding `abacus! { (--++---++----+) -> (+ + + +) }`
= note: to `abacus! ((-++---++----+) -> (+ + +))`
= note: expanding `abacus! { (-++---++----+) -> (+ + +) }`
= note: to `abacus! ((++---++----+) -> (+ +))`
= note: expanding `abacus! { (++---++----+) -> (+ +) }`
= note: to `abacus! ((+---++----+) -> (+ + +))`
= note: expanding `abacus! { (+---++----+) -> (+ + +) }`
= note: to `abacus! ((---++----+) -> (+ + + +))`
= note: expanding `abacus! { (---++----+) -> (+ + + +) }`
= note: to `abacus! ((--++----+) -> (+ + +))`
= note: expanding `abacus! { (--++----+) -> (+ + +) }`
= note: to `abacus! ((-++----+) -> (+ +))`
= note: expanding `abacus! { (-++----+) -> (+ +) }`
= note: to `abacus! ((++----+) -> (+))`
= note: expanding `abacus! { (++----+) -> (+) }`
= note: to `abacus! ((+----+) -> (+ +))`
= note: expanding `abacus! { (+----+) -> (+ +) }`
= note: to `abacus! ((----+) -> (+ + +))`
= note: expanding `abacus! { (----+) -> (+ + +) }`
= note: to `abacus! ((---+) -> (+ +))`
= note: expanding `abacus! { (---+) -> (+ +) }`
= note: to `abacus! ((--+) -> (+))`
= note: expanding `abacus! { (--+) -> (+) }`
= note: to `abacus! ((-+) -> ())`
= note: expanding `abacus! { (-+) -> () }`
= note: to `abacus! ((+) -> (-))`
= note: expanding `abacus! { (+) -> (-) }`
= note: to `abacus! (() -> ())`
= note: expanding `abacus! { () -> () }`
= note: to `true`简单地说就是利用下推式累积加上逻辑上的限制,让它正好可以相互抵消:
- 左边第一个遇到
+的时候,右边跟着+ - 左边第一个遇到
-的时候,右边减去一个+。
上面两点同样适用于-开头的场景,即右边不一定都是+取决于你的输入。
这么一看确实是像来回拨算盘(实际上也算是一元计数)
注意这里面一条规则实际上组合了多条规则,比如()和($($count:tt)+)合并成($($count:tt)*),所以第一个+可以匹配到第四条规则。
如果你希望展示计数的数值,你可以使用上一章节学到的计数方法。
比如上面的例子可以调整成:
macro_rules! abacus {
// ...
// This extracts the counter as an integer expression.
(() -> ()) => {0};
(() -> (- $($count:tt)*)) => {
- ( count_tts!($( $count_tts:tt )*) )
};
(() -> (+ $($count:tt)*)) => {
count_tts!($( $count_tts:tt )*)
};
}
// One of the many token tree counting macros in the counting chapter
macro_rules! count_tts {
// ...
}作者注:上面那个例子实际上有些不必要的复杂,可以使用重复内容,比如:
macro_rules! abacus {
(-) => {-1};
(+) => {1};
($( $moves:tt )*) => {
0 $(+ abacus!($moves))*
}
}解析#
有时解析一些Rust的item是很有用的。这章节我们将编写一些用来解析某种程度上比较复杂的item比如struct以及函数的宏。这些宏的目标不是解析item的整体语法结构,而是解析其中一部分比较有用并且不太复杂的,也就是说我们会忽略比如泛型这些。
我们接下来主要关注这些宏的matcher,至于转录器(transcribers)的逻辑是实现例子的目标部分,就不需要过多关注。
函数#
一个简单的函数的matcher一般指的是忽略了一些功能比如unsafe、async以及泛型where从句这些。如果你有需要,推荐你使用过程宏去实现。
来看例子:
macro_rules! function_item_matcher {
(
$( #[$meta:meta] )*
// ^~~~attributes~~~~^
$vis:vis fn $name:ident ( $( $arg_name:ident : $arg_ty:ty ),* $(,)? )
// ^~~~~~~~~~~~~~~~argument list!~~~~~~~~~~~~~~^
$( -> $ret_ty:ty )?
// ^~~~return type~~~^
{ $($tt:tt)* }
// ^~~~~body~~~~^
) => {
$( #[$meta] )*
$vis fn $name ( $( $arg_name : $arg_ty ),* ) $( -> $ret_ty )? { $($tt)* }
}
}这是一个简单的例子,展示了如何匹配一个函数。
有点类似派生(Derive)宏,但是对函数的处理比派生宏弱(weaker)一点。
理论上我们一般会使用模式片段限定符来实现捕获这些函数参数,但是这在当前是不允许的。幸运的是我们不在函数中使用非标识符模式,所以这个问题不大。
方法(TODO)#
比函数复杂一些,因为带有self
待完善,嘻嘻
结构体#
直接看例子:
macro_rules! struct_item_matcher {
// Unit-Struct
(
$( #[$meta:meta] )*
// ^~~~attributes~~~~^
$vis:vis struct $name:ident;
) => {
$( #[$meta] )*
$vis struct $name;
};
// Tuple-Struct
(
$( #[$meta:meta] )*
// ^~~~attributes~~~~^
$vis:vis struct $name:ident (
$(
$( #[$field_meta:meta] )*
// ^~~~field attributes~~~~^
$field_vis:vis $field_ty:ty
// ^~~~~~a single field~~~~~~^
),*
$(,)? );
) => {
$( #[$meta] )*
$vis struct $name (
$(
$( #[$field_meta] )*
$field_vis $field_ty
),*
);
};
// Named-Struct
(
$( #[$meta:meta] )*
// ^~~~attributes~~~~^
$vis:vis struct $name:ident {
$(
$( #[$field_meta:meta] )*
// ^~~~field attributes~~~!^
$field_vis:vis $field_name:ident : $field_ty:ty
// ^~~~~~~~~~~~~~~~~a single field~~~~~~~~~~~~~~~^
),*
$(,)? }
) => {
$( #[$meta] )*
$vis struct $name {
$(
$( #[$field_meta] )*
$field_vis $field_name : $field_ty
),*
}
}
}没了。。
枚举#
枚举比结构体要复杂一些,所以我们还得用上前面学过的一些模式:TT撕咬机以及内部规则。
解析来的例子里我们不会重新构建枚举,而是看一下它的所有token长啥样,因为重新构建枚举需要我们通过下推式累积临时收集所有被解析的token。
macro_rules! enum_item_matcher {
// tuple variant
(@variant $variant:ident (
$(
$( #[$field_meta:meta] )*
// ^~~~field attributes~~~~^
$field_vis:vis $field_ty:ty
// ^~~~~~a single field~~~~~~^
),* $(,)?
//∨~~rest of input~~∨
) $(, $($tt:tt)* )? ) => {
// process rest of the enum
$( enum_item_matcher!(@variant $( $tt )*) )?
};
// named variant
(@variant $variant:ident {
$(
$( #[$field_meta:meta] )*
// ^~~~field attributes~~~!^
$field_vis:vis $field_name:ident : $field_ty:ty
// ^~~~~~~~~~~~~~~~~a single field~~~~~~~~~~~~~~~^
),* $(,)?
//∨~~rest of input~~∨
} $(, $($tt:tt)* )? ) => {
// process rest of the enum
$( enum_item_matcher!(@variant $( $tt )*) )?
};
// unit variant
(@variant $variant:ident $(, $($tt:tt)* )? ) => {
// process rest of the enum
$( enum_item_matcher!(@variant $( $tt )*) )?
};
// trailing comma
(@variant ,) => {};
// base case
(@variant) => {};
// entry point
(
$( #[$meta:meta] )*
$vis:vis enum $name:ident {
$($tt:tt)*
}
) => {
enum_item_matcher!(@variant $($tt)*)
};
}没啦。。需要用到的时候再回来看吧
总结#
貌似和标题下面的可重复使用没有任何关系。。。不过我们今天学习了一些特殊用法比如计数,还是很实用的。
