10个稳定版本
1.0.9 | 2022年12月19日 |
---|---|
1.0.8 | 2021年7月1日 |
1.0.7 | 2020年6月16日 |
1.0.6 | 2019年8月18日 |
1.0.0 | 2018年6月18日 |
#2310 in Rust模式
112,462 monthly downloads
用于 345 个crate (8 直接)
105KB
2.5K SLoC
标记树调用约定
该库试图构建一个高质量、模块化、可互操作的高质量tt-muncher构建块生态系统。
tt-munching是一种强大的解析技术,用于解析具有复杂性的宏规则输入语法。在构建越来越多的复杂tt-muncher宏时,共享处理某些常见输入模式的代码变得很有价值,而不是每次都以低质量和未经过充分测试的方式重新实现对这些模式的支持。
该库提供的核心宏是 tt_call!
和 tt_return!
。它们共同提供了一种灵活的方式来沿着递归下降调用层次结构传播输入和输出标记。也可以将它们视为宏规则的灵活的仅库稳定实现。
[dependencies]
tt-call = "1.0"
版本要求:tt-call需要Rust编译器版本1.31或更高。
调用约定规则
-
符合tt-call的宏必须使用花括号来调用。
some_macro! { /* ... */ }
Rust语法对于括号和方括号宏调用后的标点符号非常特别。在表达式或类型位置,它们后面不能跟分号。在项目或语句位置,它们后面必须跟分号。这种不一致性递归地应用于它们传递给任何辅助宏,这意味着括号和方括号宏调用必须决定是否只支持表达式和类型位置或只支持项目和语句位置。它们不能同时支持两者,这对广泛应用的宏构建块来说是个问题。
在花括号调用后没有这样的标点符号要求。一致地使用花括号使得相同的宏构建块可以在任何语法位置使用。
-
输入和输出值必须以下列键值形式传递。
$key:ident = [{ $($value:tt)* }]
这是由宏
tt_call!
和tt_return!
来执行的。一致性对于组合性很重要,并且使得能够编写操作任意tt-call宏输入或输出的高阶宏成为可能。除非是专门作为tt-call构建块的库,通常tt-call宏将是私有的,具有用户界面的非tt-call入口点的辅助函数。因此,严格的键值语法无需向公共宏的用户公开。
-
在其键值输入之前,每条规则都必须接受一个
$caller:tt
。这是一个由
tt_call!
和tt_return!
使用的透明tt包,用于记录调用层次结构。一个tt_return!
接受一个$caller
以返回。 -
每条规则必须展开为正好一个宏调用,没有其他内容。
期望输出令牌通过
tt_return!
返回。不允许展开为无内容,展开为多个宏调用,或展开为除了宏调用之外的任何内容。
示例
以下是从内置tt_replace!
宏实现中摘取的一条规则,作为语法的示例。该宏接受一个令牌流,并对每个与给定谓词匹配的令牌进行替换。例如,调用者可能希望将令牌self
替换为单个令牌__value
。
此处显示的规则负责执行替换步骤。它匹配输入中的第一个令牌$first:tt
,使用tt_if!
调用谓词,并将$first
作为输入。如果谓词返回true,则递归使用累积的替换令牌;如果谓词返回false,则递归在剩余的令牌上,保留$first
不变。
{
$caller:tt
condition = [{ $condition:ident }]
replace_with = [{ $($with:tt)* }]
tokens = [{ $($tokens:tt)* }]
rest = [{ $first:tt $($rest:tt)* }]
} => {
tt_if! {
condition = [{ $condition }]
input = [{ $first }]
true = [{
private_replace! {
$caller
condition = [{ $condition }]
replace_with = [{ $($with)* }]
tokens = [{ $($tokens)* $($with)* }]
rest = [{ $($rest)* }]
}
}]
false = [{
private_replace! {
$caller
condition = [{ $condition }]
replace_with = [{ $($with)* }]
tokens = [{ $($tokens)* $first }]
rest = [{ $($rest)* }]
}
}]
}
};
以下是另一个从tt_replace!
中选择的宏规则。如果tt-muncher到达其输入的末尾,则它使用tt_return!
将完成的令牌返回给调用者。
{
$caller:tt
condition = [{ $condition:ident }]
replace_with = [{ $($with:tt)* }]
tokens = [{ $($tokens:tt)* }]
rest = [{ }]
} => {
tt_return! {
$caller
tokens = [{ $($tokens)* }]
}
};
以下是一个为tt_replace!
提供的调用者谓词示例。该谓词确定输入令牌是否为小写self
。
macro_rules! is_lowercase_self {
// Input token is `self`.
{
$caller:tt
input = [{ self }]
} => {
tt_return! {
$caller
is = [{ true }]
}
};
// Input token is anything other than `self`.
{
$caller:tt
input = [{ $other:tt }]
} => {
tt_return! {
$caller
is = [{ false }]
}
};
}
从现在起,使用我们的is_lowercase_self!
作为条件谓词调用tt_replace!
可以用来实现一元闭包的奇特语法:closure!(self + 1)
应该展开为|__value| __value + 1
。
请注意,这个面向用户的closure!
宏不遵循tt-call调用约定。然而,它内部使用了几个tt-call辅助构建块。
macro_rules! closure {
($($expr:tt)+) => {
|__value| tt_call! {
macro = [{ tt_replace }]
condition = [{ is_lowercase_self }]
replace_with = [{ __value }]
input = [{ $($expr)+ }]
}
};
}
fn main() {
let add_one = closure!(self + 1);
println!("{}", add_one(1));
}
动机
这可能看起来像是对本应非常简单的宏调用进行了很多仪式。毕竟,我们能否以如下更直接的方式编写is_lowercase_self
?
macro_rules! is_lowercase_self {
(self) => { true };
($other:tt) => { false };
}
fn main() {
println!("{}", is_lowercase_self!(self)); // true
println!("{}", is_lowercase_self!(not_self)); // false
}
有资格回答是肯定的。按照现在的写法,简单的is_lowercase_self!
的行为就像它看起来那样。
但是,如果我们想构建 tt_replace!
或类似的宏,该宏需要调用 is_lowercase_self!
作为辅助函数,那么我们就无法使用这个更简单的宏来实现。无论我们的宏做什么,它都没有方法在展开自身之前先展开 is_lowercase_self!
。如果它首先展开自身,那么它就无法使用 is_lowercase_self!
的展开结果来决定当前标记是否应该被替换。
tt_call!
和 tt_return!
抽象以及 $caller:tt
调用层次结构跟踪对于构建可以自由传递任意标记并按可通知其调用者展开的方式组合的宏至关重要。
声明性宏的未来 eager expansion(急切展开)功能可能会使 tt-call 方法变得不必要。急切展开被列为 声明性宏 2.0 的跟踪问题 中未解决的问题,但人们认为它还相当遥远,如果它真的发生的话。即使如此,也不清楚是否希望允许宏展开到任意标记。今天,宏总是展开为一个表达式、项、语句、类型或模式。急切展开并不意味着限制将被解除,以允许宏展开到任意标记,如 ! @ #
。标记树调用约定今天提供了支持传递和返回任意标记流的急切展开。
一旦函数式过程宏稳定了,那将取决于您选择的宏输入语法,以确定过程宏是否是一个更好的选择,但请注意,它们提供了自己的 DIY 解析冒险,并且一旦您掌握了两者,它们甚至可能比 tt-call 更糟糕。此外,过程宏必须在库的其余部分之外单独定义,因此它们不适合快速的单次使用辅助宏。
设计哲学
正如您可能已经预料到的那样,调用约定设计优先考虑可扩展性和可组合性,而不是简洁性。熟悉调用约定(可能是您在编写宏六个月后)的读者应该能够单独查看任何单个 tt-call 规则,并舒适地从头到尾阅读它所做的事情,并识别其目的。
链接
-
实现
closure!(self + 1)
的代码,如上所示,可以全部在examples/replace.rs
中找到。 -
作为一个更详细的 tt-call 宏示例,
examples/comma_separated.rs
展示了一个对 Rust 类型进行原始名称改写的宏。它使用parse_type!
,这是$:ty
的 tt-call 版本。static MANGLED: &[&str] = mangle_type_names! { std::fs::File, &'a mut str, impl Display, fn(s: &str) -> String, }; fn main() { assert_eq!(MANGLED, [ "_std_fs_File", "_ref_mut_str", "_impl_Display", "_fn_s_ref_str_to_String" ]); }
许可证
根据您的选择,在 Apache License, Version 2.0 或 MIT 许可证下许可。除非您明确表示,否则根据 Apache-2.0 许可证定义的,您有意提交以包含在此软件包中的任何贡献,均将根据上述许可证双重许可,不附加任何额外的条款或条件。