rust - 如何扩展递归宏规则中的子模式?
问题描述
我正在编写一个宏来方便地将enum
类型变量中的嵌套结构与编译时模板匹配。这个想法是利用 Rust 的模式匹配在结构的某些位置强制执行特定值,或将变量绑定到其他有趣的位置。基本思想在我的实现中有效,但对于嵌套模式却失败了。我认为问题在于,一旦宏输入的一部分被解析,因为$<name>:pat
它以后不能被解析为$<name>:tt
.
为了避免模棱两可地使用术语模式,我将根据 Rust 文档使用以下符号:
- 模式是出现在武器
match
、if let
语句中的内容,并由片段说明符在宏中匹配$<name>:pat
。 - 匹配器是宏中语法规则的左侧。
- 模板是我的宏的输入部分,它决定了宏的扩展方式。
enum
这是我正在使用的类型的简化版本:
#[derive(Debug, Clone)]
enum TaggedValue {
Str(&'static str),
Seq(Vec<TaggedValue>),
}
例如,下面的表达式
use TaggedValue::*;
let expression = Seq(vec![
Str("define"),
Seq(vec![Str("mul"), Str("x"), Str("y")]),
Seq(vec![Str("*"), Str("x"), Str("y")]),
]);
可以通过这个宏调用来匹配:
match_template!(
&expression, // dynamic input structure
{ println!("fn {}: {:?}", name, body) }, // action to take after successful match
[Str("define"), [Str(name), _, _], body] // template to match against
);
在这里,成功匹配标识符name
并body
绑定到相应的子元素,expression
并作为块中的变量作为第二个参数传递给宏。
这是我编写所述宏的努力:
macro_rules! match_template {
// match sequence template with one element
($exp:expr, $action:block, [$single:pat]) => {
if let Seq(seq) = $exp {
match_template!(&seq[0], $action, $single)
} else {
panic!("mismatch")
}
};
// match sequence template with more than one element
($exp:expr, $action:block, [$first:pat, $($rest:tt)*]) => {
if let Seq(seq) = $exp {
// match first pattern in sequence against first element of $expr
match_template!(&seq[0], {
// then match remaining patterns against remaining elements of $expr
match_template!(Seq(seq[1..].into()), $action, [$($rest)*])
}, $first)
} else {
panic!("mismatch")
}
};
// match a non sequence template and perform $action on success
($exp:expr, $action:block, $atom:pat) => {
if let $atom = $exp $action else {panic!("mismatch")}
};
}
对于非嵌套模板,它按预期工作,对于嵌套模板,我可以手动嵌套宏调用。但是,在单个宏调用中直接指定嵌套模板会失败并出现编译错误。
match_template!(
&expression,
{
match_template!(
signature,
{ println!("fn {}: {:?}", name, body) },
[Str(name), _, _]
)
},
[Str("define"), signature, body]
);
// prints:
// fn mul: Seq([Str("*"), Str("x"), Str("y")])
match_template!(
&expression,
{ println!("fn {}: {:?}", name, body) },
[Str("define"), [Str(name), _, _], body]
);
// error[E0529]: expected an array or slice, found `TaggedValue`
// --> src/main.rs:66:25
// |
// 66 | [Str("define"), [Str(name), _, _], body]
// | ^^^^^^^^^^^^^^^^^ pattern cannot match with input type `TaggedValue`
我怀疑错误是说它[Str(name), _, _]
作为单个切片模式匹配,它被第三个宏规则接受,它导致类型不匹配。但是,我希望它是一个标记树,以便第二条规则可以将其分解为一系列模式。
我试图将第二条规则更改为,($exp:expr, $action:block, [$first:tt, $($rest:tt)*]) =>
但这只会导致错误发生在外部级别。
需要对宏进行哪些修改才能递归地扩展此类模板?
(我不认为像在递归宏中那样使用令牌咀嚼来解析 Rust 中的匹配臂,因为我明确地想在模式中绑定标识符。)
这就是我期望宏调用扩展到的内容(为简洁起见,忽略不匹配的分支。此外,我通过后缀seq
变量来模拟宏卫生):
// macro invocation
match_template!(
&expression,
{ println!("fn {}: {:?}", name, body) },
[Str("define"), [Str(name), _, _], body]
);
// expansion
if let Seq(seq_1) = &expression {
if let Str("define") = &seq_1[0] {
if let Seq(seq_1a) = Seq(seq_1[1..].into()) {
if let Seq(seq_2) = &seq_1a[0] {
if let Str(name) = &seq_2[0] {
if let Seq(seq_2a) = Seq(seq_2[1..].into()) {
if let _ = &seq_2a[0] {
if let Seq(seq_2b) = Seq(seq_2a[1..].into()) {
if let _ = &seq_2b[0] {
if let Seq(seq_1b) = Seq(seq_1a[1..].into()) {
if let body = &seq_1b[0] {
{ println!("fn {}: {:?}", name, body) }
}
}
}
}
}
}
}
}
}
}
}
完整的扩展有点冗长,但这个稍微缩短的版本抓住了应该发生的事情的本质:
if let Seq(seq) = &expression {
if let Str("define") = &seq[0] {
if let Seq(signature) = &seq[1] {
if let Str(name) = &signature[0] {
if let body = &seq[2] {
println!("fn {}: {:?}", name, body)
}
}
}
}
}
最后,这是另一个游乐场链接,显示了递归扩展的各个步骤。它非常密集。
解决方案
实际上,问题似乎在于宏匹配逗号分隔的模式列表。因此,在输入[Str("define"), [Str(name), _, _], body]
中,宏将内部解释[...]
为无法匹配类型表达式的切片模式TaggedValue
。
解决方案是将输入扩展为令牌树。但是,这需要一个小技巧,因为单个标记树不能代表所有模式。特别是,该形式的模式Variant(value)
由两个标记树组成:Variant
和(value)
。在调用宏的终端(非递归)规则之前,可以将这两个标记组合回一个模式。
例如,在单元素模板中匹配此类模式的规则如下所示:
($exp:expr, $action:block, [$single_variant:tt $single_value:tt]) =>
这些标记一起传递给宏的另一个调用
match_template!(&seq[0], $action, $single_variant $single_value)
它们被终端规则匹配为单个模式
($exp:expr, $action:block, $atom:pat) =>
最终的宏定义包含两个额外的规则来解释Variant(value)
模式:
macro_rules! match_template {
($exp:expr, $action:block, [$single:tt]) => {
if let Seq(seq) = $exp {
match_template!(&seq[0], $action, $single)
} else {
panic!("mismatch")
}
};
($exp:expr, $action:block, [$single_variant:tt $single_value:tt]) => {
if let Seq(seq) = $exp {
match_template!(&seq[0], $action, $single_variant $single_value)
} else {
panic!("mismatch")
}
};
($exp:expr, $action:block, [$first:tt, $($rest:tt)*]) => {
if let Seq(seq) = $exp {
match_template!(&seq[0], {
match_template!(Seq(seq[1..].into()), $action, [$($rest)*])
}, $first)
} else {
panic!("mismatch")
}
};
($exp:expr, $action:block, [$first_variant:tt $first_value:tt, $($rest:tt)*]) => {
if let Seq(seq) = $exp {
match_template!(&seq[0], {
match_template!(Seq(seq[1..].into()), $action, [$($rest)*])
}, $first_variant $first_value)
} else {
panic!("mismatch")
}
};
($exp:expr, $action:block, $atom:pat) => {
if let $atom = $exp $action else {panic!("mismatch")}
};
}
这是完整示例的链接:playground。
推荐阅读
- c++ - 如何在没有 .lib 或 .dll 文件的情况下在 Qt Creator 中添加库
- selector - 我们如何自定义位于 APTran 中的 APActiveProjectAttribute() 选择器属性
- javascript - 有没有办法从 src 文件夹将 JavaScript 函数导入到 React 组件之外
- c# - 为源类型“DbSet<>”找到了查询模式的多个实现。对“哪里”的模棱两可的呼唤
- javascript - Javascript Object.assign() 和方法长度
- python - 在 Gurobi 运行时获取 Python 中所有可行的解决方案
- javascript - 如何预先延迟加载组件?
- google-sheets - 根据 Google 表格上的匹配条件(最好通过脚本)获取列中最后一个非空单元格值
- spacy - spacy 总结模型架构
- azure - 从 Azure 数据工厂访问应用服务环境中的 Azure Functions