Pest 与 PEG 文法
引入
解析表达式文法 (Parsing expression grammar, PEG) 是一种相比 CFG 更现代的语言表示形式。具有如下特点:
-
不同选项不可交换。越靠前的越优先选择。 例子:对于
A -> B | C
,在 CFG 中,B 和 C 都可以匹配,B | C
和C | B
并无不同。而 PEG 中,对于A -> B | C
如果 B 匹配成功,则不再匹配C
-
支持断言,从而消除歧义。
-
贪婪匹配,也就是说在一个选项之内,会尽可能地匹配此选项规则所允许的字符
-
但是不支持左递归。
规则
匹配任何字符
any_char = {ANY}
匹配 “abc”,然后 “def”
sequence = {"abc" ~ "def"}
匹配任何字符,除了 "
any_but_quote = { !"\"" ~ ANY }
!"c"
的作用是检查是否不是字符 c
。如果满足,则继续,否则失败。就像是 assert not eq
。 &""
!
和 &
不会消耗输入。
重复匹配
匹配任意次
expr = { "a"* }
// 或者
expr = { "a"{0,} }
匹配至少一次
expr = { "a"+ }
// 或者
expr = { "a"{1,} }
Pair
Pair 是一种区间结构。它是 Pest 对结构化的文本的抽象。举个例子:
if (a) doA() else doB()
可以对应于下面的 Pair:
+---------------------+
| Pair 1 | if (a) doA() else doB()
+---------------------+
| +---------------+|
| | Pair 2 || (a)
| +---------------+|
| | +-----------+|
| | | Pair 3 || doA()
| | +-----------+|
| +---------------+|
| | +-----------+|
| | | Pair 4 || doB()
| | +-----------+|
+---------------------+
对应于如下的文法规则:
cond_stmt = { "if" ~ "(" ~ cond_expr ~ ")" ~ block ~ "else" ~ block }
静默规则
不产生 Pair 和 Token 的规则称为静默规则。例如常见的 WHITESPACE
WHITESPACE = _{ " " | "\t" | "\r" | "\n" }
这个规则中,我们读取空白字符,但由于文法允许任意空白字符,因此我们设置为静默规则,从而不产生空白字符的 Token 干扰语法树的构建。
需要注意的是,如果右手边不是具体的字面量,而是另一个规则,那么效果等同于直接展开到对应规则。例如:
int_literal = ${ "-"? ~ digits }
digits = _{ oct_digits | ( "0x" ~ hex_digits ) | dec_digits }
上述两条规则,由于 digits
是静默规则,因此 int_literal
中的子结构 digits
会被直接展开,而不是作为一个 Pair。等价于:
int_literal = ${ "-"? ~ ( oct_digits | ( "0x" ~ hex_digits ) | dec_digits ) }
相应的制导代码:
1fn parse_int_literal(pair: Pair<Rule>) -> ParseResult<i64> {
2 let is_neg = pair.as_str().starts_with('-');
3 let pairs = pair.into_inner();
4 let pair = pairs.peek().unwrap(); // digits
5 let rule = pair.as_rule();
6 let radix = match rule {
7 Rule::hex_digits => 16,
8 Rule::oct_digits => 8,
9 Rule::dec_digits => 10,
10 _ => unreachable!(),
11 };
12 // ...
13}
原子规则和复合原子规则
原子规则和复合原子规则都不跳过空格。对于解决 MyClass<Int,Double> a;
和 Myclass < Int, Double > a;
这样的歧义,是有用的。
记法:
atomic = @{ ... }
compound_atomic = ${ ... }
区别:复合原子规则不会产生空格的 token。
典型的例子是字符串。字符串中的空格是有意义的,因此不能跳过空格。
str_literal = ${ "\"" ~ str_inner ~ "\"" }
str_inner = _{ (str_esc | str_char)* }
str_char = { !("\"" | "\\") ~ ANY }
str_esc = { "\\" ~ ("\"" | "\\" | "n" | "r" | "t") }
这里 str_literal
是复合原子规则,因此中间的空格会被保留,但不会产生空格的 token。