最困难的部分已经结束了, 剩下的部分都非常简单
本文将实现 Parser 的核心逻辑
也就是使用上一篇文章封装好的 Parser Combinator 描述文法
源码: src/parser/grammar.rs
回忆一下
在这里给出我们上上篇文章定义好的文法
Expr -> Add ;
Add -> Mul (("+" | "-") Mul)* ;
Mul -> Factor (("*" | "/") Factor)* ;
Factor -> "(" Expr ")"
| Number
;
Number -> NUM ;
这里为了少写冗余代码, 我们再最后最后化简一步, 然后统一一下命名
Expr -> Term (("+" | "-") Term)* ;
Term -> Factor (("*" | "/") Factor)* ;
Factor -> "(" Expr ")"
| Literal
;
Literal -> NUM ;
定义一下
在正式开始写之前, 应该先将 AST 上的节点类型定义一下, 这里就直接用 enum 穷举, 源码位置在 src/parser/node.rs , 实现如下
#[derive(Debug, Clone, PartialOrd, PartialEq)]
pub enum Node {
Literal {
kind: SyntaxKind,
value: i32,
raw: String,
},
Expr {
kind: SyntaxKind,
left: Box<Node>,
op: SyntaxKind,
right: Box<Node>,
},
}
需要注意的是, Expr 表示表达式节点, left 和 right 可以是 Expr 或者 Literal, 是一个递归类型, 因此需要使用 Box 包裹一下将实际内容存放到堆上, 否则编译器无法计算需要分配多少内存, 编译无法通过
写一下
直接开始写, 现在只需要根据现有的 Parser Combinator 来描述文法就可以了
literal
Literal 文法如下
Literal -> NUM ;
代码如下, 使用 single_token 匹配出一个 NUM 类型的 Token, 再使用 map 将他映射为 Literal 节点类型即可
pub fn literal() -> impl Parser<'static, Node> {
single_token(NUM).map(|(_, value)| Literal {
kind: NUM,
// 这里直接调 unwrap 即可, 因为前面的 Lexer 可以保证一定可以被转为整数
// 不考虑溢出的情况的话...
value: value.parse().unwrap(),
raw: value,
})
}
factor
下一个是 Factor, 文法如下
Factor -> "(" Expr ")"
| Literal
;
对于 "(" Expr ")" 我们可以直接用一连串 and_then 来连续匹配, 如下
fn factor() -> impl Parser<'static, Node> {
either(
literal(),
single_token(token!["("])
// 这里假设已经事先定义出 expr 函数
.and_then(|_| expr())
.and_then(|node| {
single_token(token![")"])
.map(move |_| node.to_owned())
}),
)
}
term
Term 文法如下
Term -> Factor (("*" | "/") Factor)* ;
这里我们直接使用 zero_or_more 来将 ("*" | "/") Factor) 匹配 0 次或更多次, 我们先不考虑节点的构建, 搭出框架如下
fn term() -> impl Parser<'static, Node> {
factor().and_then(|left| {
// 将传入 Parser 重复匹配 0 次或更多次
zero_or_more(
either(
single_token(token!["*"]),
single_token(token!["/"])
).and_then(|(op, _)| {
// 这里将整个结果映射为一个二元组 (操作符, 操作数)
factor().map(move |right| (op, right))
}),
)
// 使用 map 将匹配到的内容映射为 Node 类型
.map(move |node_list| {
// TODO 如何构建 Expr 节点
})
})
}
这里简单的解释一下
zero_or_more会将给定的Parser重复匹配 0 次或更多次, 返回值是一个装有匹配结果的Vector- 在上面的实现中,
zero_or_more内部的Parser将输出映射为一个二元组, 包括操作符以及操作数, 具体类型为(SyntaxKind, Node) - 整个
zero_or_more的输出就是一个装有这个二元组的Vector, 具体来说, 输出类型为Vec<(SyntaxKind, Node)>
我们先暂时忽略这里的节点构建, 先完成最后一个 Expr 的文法描述
expr
这里其实是 Term 的镜像版本, 因此就不多做解释了, 文法如下
Expr -> Term (("+" | "-") Term)* ;
实现如下
pub fn expr() -> impl Parser<'static, Node> {
term().and_then(|left| {
zero_or_more(
either(
single_token(token!["+"]),
single_token(token!["-"])
).and_then(|(op, _)| {
term().map(move |right| (op, right))
}),
)
.map(move |node_list| {
// TODO 如何构建 Expr 节点
})
})
}
build_expr_node
经过上面的实现, 可以发现, 目前可以抽离一个用来构建表达式节点的公用函数, 因为 expr 和 term 都会需要, 并且他们的结构几乎一样, 下面就来实现
先来看看 Expr 节点的结构
pub enum Node {
Expr {
kind: SyntaxKind,
left: Box<Node>,
op: SyntaxKind,
right: Box<Node>,
},
}
Expr 的 left 和 right 都是一个递归类型, 那么我们应该怎么来构建呢, 具体来说就是该怎么来构建这棵 AST
看这个例子 "1 + 2 + 3"
如果我们希望按照正常计算顺序来计算的话, 结合之前提到的 AST 的遍历顺序, 这棵树应该长下面这样
那么 "1 + 2 + 3 + 4" 呢, 长这样
我们会发现, 这棵树总是应该往左下角生长
其实这里是存在问题的, 因为 AST 往左下角生长意味着左递归, 而我们使用的解析算法无法处理左递归的文法, 因此在前面的文章中就对左递归问题进行过消除, 实际上是改写成了右递归
这里就产生了一个冲突, 我们希望通过非左递归的文法, 解析出一棵左递归的树, 因此这里不可能从文法层面去实现这个需求
只能把 向左递归生长 的逻辑实现在这个函数里面, 那么每个 Expr 有 left, right 字段, 对应关系又如何呢, 以下简单展开一下
举例如 "1 + 2 + 3", 他会匹配 Expr 的文法
Expr -> Term (("+" | "-") Term)* ;
对 Expr 进行简单的整理并推导展开
我们希望优先计算 1 + 2, 这意味着 "1 + 2" 应该单独形成一个 Expr 并作为子节点, 则有
而带上右边的 "+ 3" 时, 1 + 2 形成的 Expr 就应该作为这个表达式的左子节点, 如下
这样我们就得到了我们期望中的往左递归生长的树
下面再将无关的标识去掉, 只关注各个部分与 left, right 字段的对应关系, 如下
经过分析, 具体实现还是比较简单的, 结合上文提到的 node_list 的类型, 实现如下
fn build_expr_node(expr: Node, mut node_list: Vec<(SyntaxKind, Node)>) -> Node {
match node_list.len() {
// 若当前 node_list 为空, 回溯
0 => expr,
// 当前 node_list 不为空, 递归构建
_ => {
// 这里已经对 node_list 长度进行过判断, 可以放心 unwrap
let (op, right) = node_list.pop().unwrap();
// 构建表达式节点
Expr {
kind: match op {
token!["+"] => ADD_EXPR,
token!["-"] => SUB_EXPR,
token!["*"] => MUL_EXPR,
token!["/"] => DIV_EXPR,
// 这里应该直接 panic
_ => UNKNOW,
},
// 向左递归构建子树
left: Box::new(build_expr_node(expr, node_list)),
op,
right: Box::new(right),
}
}
}
}
如上就可以完成节点的递归构建, 最主要的是需要理清文法中各部分在 Expr 中对应的角色, 具体一点说就是需要理清文法中各部分在 Expr 中要赋值给哪个字段
完善一下
如上完成了 build_expr_node 函数, 现在已经可以来完善前面的部分了
/// Expr -> Term (("+" | "-") Term)*
pub fn expr() -> impl Parser<'static, Node> {
term().and_then(|left| {
zero_or_more(
either(
single_token(token!["+"]),
single_token(token!["-"])
).and_then(|(op, _)| {
term().map(move |right| (op, right))
}),
)
.map(move |node_list| build_expr_node(left.to_owned(), node_list))
})
}
/// Term -> Factor (("*" | "/") Factor)*
fn term() -> impl Parser<'static, Node> {
factor().and_then(|left| {
zero_or_more(
either(
single_token(token!["*"]),
single_token(token!["/"])
).and_then(|(op, _)| {
factor().map(move |right| (op, right))
}),
)
.map(move |node_list| build_expr_node(left.to_owned(), node_list))
})
}
跑一下
别跑了, 我在源码里面写了很多测试用例, 太长了, 而且很丑, 就不 copy 过来了, 代码是没问题的
模块导出
目前已经完成了 parser 的大部分逻辑, 简单封装一个函数作为模块导出
源码: src/parser/mod.rs
pub fn syntax(tokens: TokenStream) -> Result<Node, String> {
match expr().parse(tokens) {
Ok((_, n)) => Ok(n),
Err(output) => Err(format!(
"panic at parsing `{}`",
output.iter().fold(String::new(), |mut res, (_, cur)| {
res.push_str(cur);
res
})
)),
}
}
Q&A
Q: 为什么变量名一直改来改去的?
A: 最开始书写文法的时候, 为了看起来更清晰, 就选择 Add 和 Mul 为文法命名, Expr -> Add 这种文法其实是没有意义的, 因为只有一个产生式, 相当于是整了个别名, 实际实现的时候就没必要存在, 因此就干脆一点把这条冗余的规则去掉了, 然后对应的修改了一下文法命名, 其实应该是不影响阅读和理解的
Q: 上篇文章说 Parser Combinator 优雅, 优雅在哪?
A: 很神奇的一点就在于传说可读性差的函数式编程范式, 在这里可读性意外的高, 只需要把变量名连在一起读就能读懂大概的意思, 此外笔者在实现的时候发现 Parser Combinator 的设计还意外的契合软件工程中 高复用, 细粒度, 高可控, 易测试 的理念, 简单来说就是每个 Parser 和 Combinator 都是 小类短方法, 这里的开发体验真的不错, 当然也因为代码量很小, 但是好歹看起来挺清晰挺舒服的