Skip to content

解析器的实现

语法解析在制作编程语言过程中处于关键的起始步骤,不过相较于其他部分,其入门难度确实偏高。

简介

在多数情形下,解析器可划分为两个主要部分,也就是分词器与语法解析部分。分词器的作用相当于对输入的代码预先进行处理,以便后续能更便利地解析代码。像 Python 这种将缩进作为语法组成部分的语言,分词器需要承担更多额外的工作任务。对于分词器的实现,既可以借助现成的工具来生成,也可以通过手写一个状态机的方式来达成。而语法分析环节就要复杂得多了。在学术领域,虽然普遍认为语法分析问题已得到解决,并且确实存在众多优秀的解析器生成器,但要想很好地运用这些工具,仍需对它们的底层原理有比较深入的理解才行。

快速入门

编程语言的语法能够通过一个非终结符的集合来描述。例如,最基础的加法运算可以相对不那么严谨地描述为规则 E -> E + T | T 以及 T -> NUMBER。更为学术性的解释可自行去参考上下文无关文法(CFG)和巴科斯范式(BNF),它们都是业界常用的用于语法描述的方式,从本质上来说,这些描述方式都是基于一系列递归函数构建的,此处就不过多深入阐述了。在具体的算法实现方面,存在多种选择,例如基于分析表的、自上而下的 LL 算法以及自下而上的 LR 算法,还有它们的各类变体。LL 算法实现起来相对简单,但有着较大的局限性;LR 算法则与之相反,功能更为强大但实现难度更高。在实际应用落地层面,有像 YACC/Bison 以及较为现代的 ANTLR4 这类工具,ANTLR4 支持的编程语言种类较多,有兴趣的话可以自行深入学习,它们都属于非常成熟的技术了。

即便不采用这些成熟的技术,出于对高度定制化以及性能优化方面的考量,也可以选择手写一个自上而下递归下降(Top-Down Recursive Descent,简称 TDRD)解析器。正如前文所述,语法从本质上看就是一堆递归函数,所以完全能够通过手写的方式来实现,例如 Rust 的解析器就是纯手写完成的,Felys 在早期也是如此。只不过手写这种解析器对于项目的结构设计要求颇高,很容易出现结构设计不合理的情况。

另外,还有一种相对较新的解析器思路,即解析表达式语法(PEG)及其变体 Packrat 解析器。这种解析器更像是一种功能强大的正则解析器,与传统解析器有所不同的是,Packrat 解析器支持无限回溯,并且它通过缓存机制,采用以空间换时间的策略来避免重复解析,最终能够达到线性时间复杂度。其实际落地的代码与手写的 TDRD 解析器比较相似,但代码结构会更加清晰美观。不过需要注意的是,PEG 的语法注解方式和 CFG、BNF 是不一样的,主要差异在于 PEG 中规则是有顺序要求的,而在 CFG 和 BNF 里规则顺序通常是无关紧要的。比如在 PEG 语法中,E -> E + T \ TE -> T \ E + T,是不等价的,但在 CFG 里这两个表达式则是等价的。不过这种解析器在实际应用中不算广泛,可能是由于其性能表现欠佳,尤其是在内存占用方面,据了解,目前只有 Python 和 Rust 的宏定义中有使用到它。更多的细节内容可以参考原论文

INFO

PEG 语法中使用 \ 而不是 | 来强调顺序的重要性,以此与传统语法描述方式形成区别。

技术选型

最终决定选用 Packrat 解析器,主要原因如下:Python 在早几年已经将原本的 LL 解析器替换成了 PEG 解析器,而且从实际情况来看,性能方面并没有出现明显的差异,同时可维护性却得到了极大的提升。再者,考虑到 Packrat 解析器实现起来较为直观、简单,并且有 Python 那边现成的案例可供参考,还有 Guido 的相关教学博客能够辅助学习。而对于其他传统的解析器生成器,我或多或少有所了解,它们的学习曲线同样比较陡峭,并且各自都存在一定程度的局限性。综合多方面因素考虑,最终决定自己手写一个 Packrat 解析器。

语法设计

这个语法文件单纯是用来表达语法的,并不能丢给哪个解析器生成器来生成。有些非终结符我这里没写出来,因为代码实现的时候直接调用 Rust 的内置函数。此外,有些细节和实际的实现略有不同或者简化,有需求直接参考源代码。

入口语法

program -> stmt* eof

语句语法

stmt -> \
    \ ctrl ";"
    \ ctrl
    \ expr ";"
    \ expr
    \ ";"

block -> "{" stmt* "}"

模版语法

pat -> \
    \ "_"
    \ ident
    \ "(" pat ("," pat)+ ")"
    \ lit

ident -> (alphabetic \ "_") (alphanumeric \ "_")*

表达式语法

expr -> tuple

tuple -> \
    \ "(" expr ("," expr)+ ")"
    \ disjunction

disjunction -> \
    \ disjunction "or" conjunction
    \ conjunction

conjunction -> \
    \ conjunction "and" inversion
    \ inversion

inversion -> \
    \ "not" inversion
    \ equality

equality -> \
    \ equality "==" comparison
    \ equality "!=" comparison
    \ comparison

comparison -> \
    \ comparison ">" term
    \ comparison ">=" term
    \ comparison "<" term
    \ comparison "<=" term
    \ term

term -> \
    \ term "+" factor
    \ term "-" factor
    \ factor

factor -> \
    \ factor "*" unary
    \ factor "/" unary
    \ factor "%" unary
    \ unary

unary -> \
    \ "+" unary
    \ "-" unary
    \ primary

primary -> \
    \ lit
    \ ident
    \ "(" expr ")"
    \ "|" (ident ("," ident)*)? "|" expr
    \ ctrl

控制流语法

ctrl -> \
    \ assign
    \ block
    \ "break" expr?
    \ "continue"
    \ "for" pat "in" expr block
    \ "match" expr "{" pat "=>" expr ("," pat "=>" expr)* "}"
    \ "if" block ("else" expr)?
    \ "loop" block
    \ "return" expr?
    \ "while" expr block

assign -> \
    \ pat "=" expr
    \ pat "+=" expr
    \ pat "-=" expr
    \ pat "*=" expr
    \ pat "/=" expr
    \ pat "%=" expr

字面量语法

lit -> \
    \ float
    \ int
    \ str
    \ bool

float -> \
    \ "0" ~ "." digit+
    \ digit+ "." digit+

int -> \
    \ "0x" ~ hex+
    \ "0o" ~ oct+
    \ "0b" ~ bin+
    \ "0" ~ !digit
    \ digit+

bool -> \
    \ "true"
    \ "false"

str -> "\"" (!"\"" char)* "\""