实现脚本解释器 - 词法分析器
本系列介绍
笔者最近正学习编译原理,为了将理论变为实践,所以创作了本系列来记录学习过程中的思考与问题,注意文章中为了理论上描述方便增加了自创的术语。
本系列使用 Java 语言来实现一个脚本解释器,该脚本语言命名为 Foo,其语法参考 JavaScript 语言,本系列代码地址 Github 。
词法分析器介绍
词法分析器的作用是将输入的字符串转变为一个个的记号(token),记号是由记号名(name)和属性值(value)构成的二元组(unit doublet)。
通过构造有限自动机(finite automata, FA)来识别字符串是否为匹配某种规则(模式),编译原理书中用正规式来描述这种规则,但其描述性不强且不能描述匹配对,故本文统一采用扩展的巴斯克范式(ABNF),具体语法参考 RFC5234。
当有限自动机匹配或不匹配输入串会执行不同的动作,具体实现时是匹配则返回对应的记号或者忽略该字符串(例如注释)否则报词法错误,而有限自动机往往通过一段子程序(函数)来实现,将这些子程序组合起来就构成了词法分析器(lexer)。
基本的准备
首先需要编写一个记号类,其包含了记号名和属性值,由于属性值会被赋予不同的类型,所以使用 Object
类型,类中的常量来表示不同的记号名。
public class Token {
public static final String TOKEN_EOF = "<eof>";
// omit other token constants
private private String name = TOKEN_EOF;
private Object value = null;
// getters and setters
}
接下来就可以来编写 Lexer
词法分析器类,先抛弃其他一些细节来分析下面定义的两个私有属性和两个个私有方法的作用。其中属性 currentChar
用来存放当前读取的字符,而 nextChar
则是存放下一个字符 。
方法 char readChar()
用来读取下一个字符,当返回 -1
时表明读取完毕,其重载方法 char readChar(int offset)
用来指定偏移多少位置后读取字符,从 0 开始且 0 相当于调用了该方法的无参重载。
public class Lexer {
private char currentChar = '\0';
private char nextChar = '\0';
private char readChar() {
// ...
}
private char readChar(int offset) {
// ...
}
}
分析字符串流程
接下来定义 Lexer
类的公有方法 Token nextToken()
来读取一个记号,它分析字符串的流程如下:
currentChar
存放当前需要匹配的字符,若读取到文件末尾则返回EOF
记号。- 根据匹配的单字符或双字符,调用确定的子过程。
- 子过程匹配完毕,读取下一个字符,并返回相对应的记号或者跳转回步骤 1 。
注意若是代码较短,则这里的子过程并不一定需要写成函数。
匹配前缀与匹配状态
整个词法分析器其实就是个不确定的有限自动机(NFA),开始时并不知道匹配何种记号,这里称之为 不确定匹配状态
。通过单个或多个字符就能确定匹配何种记号并可以调用子过程,这时进入了 确定匹配状态
,而子过程就是个确定的有限自动机(DFA),称这些字符或字符序列为 匹配前缀
。
记号可以分为以下几类,这些记号根据匹配前缀可以分为需要双字符和只需单字符确定,双字符确定的记号只有注释和双字符符号,其他都为单字符确定的,这也是为什么前面需要声明 nextChar
变量存放下一个字符。其中的标识符包含了保留字,而符号分为运算符及界符。
- 注释
- 空白符号
- 换行
- 标识符
- 数字
- 字符串
- 双字符符号
- 单字符符号
- 终止记号
消除歧义
有些情况下,单字符确定的匹配会影响双字符确定的匹配,为了消除这种歧义,就需要先进行双字符匹配再进行单字符匹配。
例如单行注释以双字符 //
作为匹配前缀,而单字符符号除号 /
会影响该双字符确定的匹配,若是将单字符确定的匹配放前面,则会匹配成两个除号记号。
匹配换行
在不同的系统中,文件的换行有以下三种:
- CRLF Windows
- LF Linux
- CR Unix
为了兼容考虑,匹配换行具体代码如下所示:
if (currentChar == '\r' || currentChar == '\n') {
newLine();
continue;
}
private void newLine() {
nextChar = readChar();
if (nextChar == '\n') {
currentChar = readChar();
} else {
currentChar = nextChar;
nextChar = '\0';
}
}