语法解析
语法解析的职责
一个语法解析器必须能够判断在给定编程语法的情况下,用户编写的程序在语法上是否合理。通常,编程语法需要用某种形式来描述,下面给出形式化的定义:
- Context-free grammar:CFG是用来描述如何构成句子的一系列规则的集合
- Sentence:根据CFG生成的字符串
- Production:CFG中的每条规则称为production
- Nonterminal symbol:production中的字符变量,能够被规则替换
- Terminal symbol:句子中出现的单词,实际上是句法分析生成的Token
- Start symbol:grammer中的起始字符变量
描述表达式的语法例子
1 Expr -> Expr + Term
2 | Expr - Term
3 | Term
4 Term -> Term * Factor
5 | Term / Factor
6 | Factor
7 Factor -> ( Expr )
8 | num
9 | name
Expr是我们的起始字符,Term、Factor是字符变量,num、name是我们的Token。对于每条规则,方便起见,顺序编了序号。现在看看我们根据定义的语法可以生成些什么?依次运用规则1->3->6->8->4->6->9->7->2->3->6->9->6->8,最终生成的表达式为4+a*(b-7)。这个过程可以使用语法生成树来表示:
解析
解析与生成的过程相反:给定特定的sentence,我们需要构建出这棵语法生成树。根据构建过程可以将解析器分为两大类:
- Top-down parsers 从根节点出发,最终生长到叶子结点,在构建过程中,解析器会利用替换规则,将每个字符变量结点替换为一棵子树,直到所有叶子结点都不再可替换为止。
- Bottom-up parsers 从叶子结点出发生长到根节点,在构建过程中,解析器寻找满足替换规则的父结点,随后将该结点加入树中。
无论哪种解析方法,其中都涉及到替换规则的选择,不好的选择会对解析性能产生严重的影响,因此,合理高效的选择机制是解析算法的研究重点。
按照解析复杂度,我们可以把CFG(Context free grammar)分为4层:
任意CFG 没有限制条件的CFG,解析时间复杂度高达
O(n^3)LR(1) 这类解析器使用自底向上算法,从左向右解析,基于当前字符,每次朝前看至多一个token。
LL(1) 该类解析器是LR(1)的子集,使用自顶向下算法,从左向右解析,每次朝前看至多一个token。
RG Regular grammar是一类特殊的CFG,替换规则只有两种:或者,其中a为终止字符,A、B为字符变量。
几乎所有的编程语言结构都能够使用LR(1)形式表达,LL(1)形式更为常见。
从顶向下解析
通用算法描述
上面给出了一种通用地从顶向下的左替解析算法,root表示根节点,focus表示当前替换规则中最左边的字符,算法使用数据栈存储替换规则中待匹配的字符,word表示当前的输入字符。首先进行初始化工作,接着进入一个大循环:如果当前字符是可替换地,那么选择一条规则替换当前字符,将规则中待匹配的字符逆序压入栈中,更新focus;如果focus不可替换且匹配到了word,此时将输入字符序列下一字符读取到word中,弹出栈顶元素,更新focus;如果所有输入字符均已匹配,返回解析树,否则进行回溯操作。
回溯操作通常自底向上逐层进行:如果当前所有替换规则均替换失败,回溯到上一层,重新进行替换,如果回溯到顶层依然匹配失败,此时返回语法错误提示。回溯实际上是对整个语法结构的遍历,这会耗费大量时间,如果有某种算法可以避免回溯,这将大大提高解析性能。
语法结构性问题
将通用算法应用到我们本篇定义的表达式语法结构上,此时如果我们每次替换规则都选1,会发生什么事情呢?算法不会终止!这种现象称为Left-recursion。解决方法也很简单,我们可以重写语法替换规则,使得语法结构是Right-recursion即可:
Expr -> Term Expr'
Expr' -> + Term Expr'
| - Term Expr'
| \epsilon
Term. -> Factor Term'
Term' -> * Factor Term'
| / Factor Term'
| \epsilon
显式Left-recursion消除可以使用以下方法:
的作用是终止替换,如果没有这条规则,就会不断循环下去。当然,除了直接显式的Left-recursion,还存在隐式Left-recursion,诸如,这最终会导致,下面给出一个消除Left-recursion的算法:
算法总体分为两步:首先将所有隐式Left-recursion转换为显式Left-recursion,接着消除显式Left-recursion。隐式Left-recursion可以从图的角度来理解:我们把所有具有形式的替换规则表示成有向边,所有的字符变量当做图节点,那么隐式Left-recursion意味着图中有环,显然,隐式Left-recursion转换为显式Left-recursion的过程就是削减环的大小,直到图中只存在长度为1的环。
无回溯解析
前面讲到,Top-down leftmost parser在解析时涉及回溯操作,这会降低解析性能。通过引入look-ahead技术,我们可以消解回溯,做到无回溯解析,对应地,这类语法叫做Backtrack-free grammar,也称作predictive grammar。在介绍无回溯解析算法前,先引入两个概念:First-set、Follow-set。
- First-set 对于特定语法字符 ,是一个集合,包含所有从出发,利用语法替换规则生成的句子的首部终止字符。
- Follow-set 对于给定的字符变量,是一个集合,包含所有利用语法替换规则生成的句子中跟着立即出现的终止字符。
First-set 具有如下性质:
下面给出一个计算First-set的算法:
算法还是很直观地:首先计算终止字符的First-set,对于非终止字符来说,如果存在替换规则,那么。考虑包含的情况,此时,以此类推,直到计算完所有为止。
我们试着给出前面表达式语法的First set:
| Expr | Expr' | Term | Term' | Factor | |
|---|---|---|---|---|---|
| First | (,name,num | (,name,num | (,name,num |
如果只使用First set,可能会出现一个问题:look-ahead字符不在First集合中怎么办,直接返回语法错误?这里的关键在于对的处理上,替换规则意思是跳过当前字符变量,转入其他替换规则中,但是从first-set的定义上可以看出,并不关心跳转操作,所以我们使用Follow-set来定义了跳转操作。下面给出计算Follow-set的算法:
有人可能有点迷糊:为啥这里只更新了Follow(),Follow(A)呢?这是因为A出现在替换规则的左边,我们无处得知与A所关联的结构信息,该信息只能从替换规则的右边获取。要想更新Follow(A),我们要找到如下规则:。
前面表达式语法的Follow set:
| Expr | Expr' | Term | Term' | Factor | |
|---|---|---|---|---|---|
| Follow | eof,) | eof,) | eof,+,-,) | eof,+,-,) | eof,+,-,*,/,) |
结合First set与Follow set,我们给出对于规则的First set定义:
对于任意的替换规则,如果存在如下条件:
我们就说具有该规则的语法是backtrack-free的。基于此,引出两种Top-down解析算法:Recursive-Descent算法、Table-Driven算法。
Recursive-Descent算法
Recursive-Descent的想法是朴素的:对于每个字符变量S,根据给出的语法结构,将其写成对应的一个递归函数,这样以来,我们就把Backtrack-free grammar转换为多个互相调用的递归函数组合,下面给出一个实现例子:
| 序号 | Production | |
|---|---|---|
| 2 | ||
| 3 | ||
| 4 |
Eprime()
/*Expr'-> + Term Expr' | - Term Expr'*/
if (word = + or word = -) then begin;
word <- NextWord();
if (Term())
then return EPrime();
else return false;
end;
else if (word = ) or word = eof)
then return false;
else begin;
report a syntax error;
return false;
end;
Table-Driven LL(1) Parser
基于数据栈和二维表,同样可以实现Top-down解析算法:
数据栈保存待生成访问的节点,二维表保存语法结构所有的look-ahead信息。focus表示待生成的解析树节点,word表示输入字符流当前读取字符。首先初始化工作,将eof,起始字符S压入栈中,focus初始化为S。接着进入一个大循环:根据focus不同形式,执行不同逻辑,这里分为两部分:全部匹配成功退出阶段、单叶子结点匹配阶段、解析树扩展阶段。
- 解析树扩展阶段 focus为非终止字符变量,此时查二维表T,查找替换规则成功,则进行子树生成,弹出栈顶元素,将替换规则右手所有非字符按从右到左顺序压入栈中。查找失败返回错误扩展信息。
- 单叶子结点匹配阶段 focus为终止字符,如果focus与word匹配,则弹出栈顶元素,word更新为下一输入字符。否则返回匹配失败信息。
- 全部匹配成功退出阶段 focus为eof且word为eof,返回匹配成功信息,退出循环。
关于二维表的构建,流程如下:
对于满足backtrack-free条件的语法结构,构建二维信息表时间复杂度为,其中P为规则集合,T为终止字符集合。如果不满足backtrack-free条件,此时表中元素出现多条规则,这需要我们使用left-factor技术优化语法,使其具有backtrack-free性质。
总结
本篇介绍了语法解析的作用以及解析器的分类,详细说明了LL(1)解析器的实现算法及细节。Top-down解析算法主要缺点在于无法解析Left-recursive的语法,尽管可以使用技术手段消解Left-recursive,如果有某种解析算法可以应用到Left-recursive,可以为解析过程节省额外步骤,在下篇,我们会介绍Bottom-up解析相关技术原理,敬请期待。