解释器和编译器的前端是相似的。编译器包括前端(词法分析、语法分析、语义分析)和后端(优化、中间语言、指令生成)。解释器的前端不需要语义分析,仅靠语法分析构建一个语法树即可。后端就是解释运行环境。
词法分析太简单了。这里从语法分析讲起。
脚本的语法分析
绝大多数脚本的语法非常简单,任何一种自顶向下的分析法基本都可以胜任。LL(1)是个不错的选择——简单、高效。LL1的要求很严格:1、不允许左递归(这个通常有);2、不允从左至右的二义性冲突(这个很难)。
通常情况下,第一点不满足的话,是可以消除的。简单文法一般不会有间接左递归(复杂文法都很少用到间接左递归),要有也是直接左递归。消除直接左递归的方法是非常简单的。举个直观的例子:
1. expr -> expr + term
2. expr -> term
3. term -> term * factor
4. term -> factor
5. factor -> digits | identifier
该文法描述了由+、×两个二元运算符和数字、标识符构成的所有中缀表达式。1和3是左递归的,隐含了一个同级运算符从左至右组合计算的意义。消除这种直接左递归的方法是:
1. 构造一个新的产生式expr-aux -> + term | ε,蕴含符右边就是原来的右部去掉左端递归因子后剩下的部分。
2. 删除原左递归的产生式。
3. 所有相同非终结符导出的产生式后面加上expr-aux:expr -> term expr-aux
实际上就是把左递归变成了右递归。这样会产生一个问题:右递归隐含的组合顺序是从右至左。这只能在语义分析的时候再去调整。
如果不想再去调整,可以使用手动写递归下降来分析器解决。递归下降允许左递归。递归下降分析左递归文法时的流程如下:
从左至右的二义性冲突是最难解决的问题,该问题的复杂度没有上限。解决方法只有展开冲突产生式、合并冲突左因子。当文法中出现某些特定结构的时候,展开与合并是没有终结的:合并后总是会出现新的冲突,而且冲突模式与上一次的相似,还有,就是通常这种递归式的冲突是多重的。
好在脚本很简单,如果真的出现了这种二义性冲突,可以不改变功能而修改一下文法来达到需求。实际上,许多脚本甚至可以用正则表达式来分析,因为它们大多数都可以表示为正则文法(III型文法)。大多数的复杂语言都是上下文无关文法(II型),很少有I型或O型文法用于编程语言。