Antlr 文法设计中的尖括号问题
聊一聊使用 Antlr 设计编程语言文法时,>
符号作为大于号( a > 0
)、泛型尖括号( List<int>
)、按位右移运算符( a >> 1
)这些语义时碰到的问题。
文法设计
首先在词法上,一般会分开,把 <
>
视为一个独立的 token,比如:
1 | // Lexer grammar |
接着是定义语法,一个一个来,先定义比较大小的语法:
1 | // Parser grammar |
为了避免引入与本文无关的复杂性,这里先忽略其他操作符的优先级和结合性,以及引起左递归等问题,认为左值和右值都是表达式。
然后定义按位左移、右移的语法。这通常是两个大于号、小于号的组合。
可以参考按位与 &
、逻辑且 &&
的做法,把 >>
<<
视为两个独立的 token:
1 | // Lexer grammar |
对应的语法就是:
1 | // Parser grammar |
看上去非常顺利,语法设计完成了。
但是,如果还要引入泛型语法,使用尖括号 <
>
来表示类型参数,像这样:
1 | // Parser grammar |
我们不关注 type_parameters
语法的具体定义,假设它可以递归地表达这个泛型类型的语法。
generic_expr
已经能表达类似 List<int>
的泛型类型了。但通常,泛型的参数本身,还可以嵌套泛型,比如 List<List<int>>
。这就导致了一些冲突:
词法分析结果:
List<int>
被解释为下面几个 token:
1 | List < int > |
而 List<List<int>>
被解释为
1 | List < List < int > > |
…吗?还记得 SHIFT_RIGHT
吗?它也是两个 >
组成的 token。事实上,由于 Antlr 的词法分析是贪婪的,最后两个 token 会被合并为一个 SHIFT_RIGHT
,导致词法分析结果变成了:
1 | - LIST | LESS | LIST | LESS | INT | GREATER | GREATER |
词法分析的意外会进一步导致语法分析的崩溃:泛型语法应该以 GREATER
结尾,但现在是 SHIFT_RIGHT
结尾,这就造成了语法错误。
尖括号的二义性
有办法在词法分析时避免这种情况吗?答案是否定的——词法分析阶段,还没有完整 b 的分析出语法结构,更别说语义了。仅根据词法,难以区分 >
是大于号还是泛型的结束。也就是说,与 &
符号不同的是,>
符号在语法上有多种意义,难以在词法上提前区分。
那么,问题出在右移的词法上。不应该声明两个大于号为一个 token,而是,在语法上,通过两个 GREATER
来表示右移。
1 | // Parser grammar |
不过左移的词法,如果从设计上不会出现其他的两个 <
连续的情况,那就可以保留。
从 js 到 ts 的做法
我们知道 JavaScript 是没有类型语法的,更没有泛型。
在 JavaScript 的 antlr 词法文件中,确实有 >>
作为独立 token 的定义:
1 | LeftShiftArithmetic : '<<'; |
而在这门语言中扩展类型,升级到 TypeScript,就会碰到泛型问题,所以 TypeScript 的 antlr 词法文件中,去掉了 >>
的定义:
1 | LeftShiftArithmetic : '<<'; |
C++ 的做法
C++ 同样有泛型,也没有定义 >>
的 token,甚至连 <<
的 token 都没有。
不用尖括号的泛型语法
go 和 python 的泛型语法使用方括号 []
,避免了 >
的歧义。因此,词法文件中声明了 >>
的 token
https://github.com/antlr/grammars-v4/blob/bdf2e9a5e618f54e7a2ad95610e314a199f10f77/golang/GoLexer.g4
左移赋值语法的 token 设计
和尖括号有关的文法,还有两种,大于等于 >=
,和右移赋值 >>=
。
假设我们现在有这些词法 token:
1 | // Lexer grammar |
那么,右移赋值的应该在词法上设计为完整的 token,还是在语法上拼接 GREATER 和 GREATER_EQUAL 呢?
后者有点奇怪,毕竟大于等于在语义上和前一个大于号没有什么关联。假设采用前者:
1 | RIGHT_SHIFT_ASSIGN : '>>=' ; |
这就有个问题,输入 a >>= 1
时,中间的部分,在词法分析阶段,是会被识别为 GREATER GREATER_EQUAL
(大于号和大于等于号),还是 RIGHT_SHIFT_ASSIGN
(右移赋值号)?在 antlr 中,答案是后者,antlr 会尝试匹配字符最长的 token。