笔者最近正学习编译原理,为了将理论变为实践,因此创做了本系列来记录学习过程当中的思考与问题,注意文章中为了理论上描述方便增长了自创的术语。html
本系列使用 Java 语言来实现一个脚本解释器,该脚本语言命名为 Foo,其语法参考 JavaScript 语言,本系列代码地址 Github 。java
词法分析器的做用是将输入的字符串转变为一个个的记号(token),记号是由记号名(name)和属性值(value)构成的二元组(unit doublet)。git
经过构造有限自动机(finite automata, FA)来识别字符串是否为匹配某种规则(模式),编译原理书中用正规式来描述这种规则,但其描述性不强且不能描述匹配对,故本文统一采用扩展的巴斯克范式(ABNF),具体语法参考 RFC5234。github
当有限自动机匹配或不匹配输入串会执行不一样的动做,具体实现时是匹配则返回对应的记号或者忽略该字符串(例如注释)不然报词法错误,而有限自动机每每经过一段子程序(函数)来实现,将这些子程序组合起来就构成了词法分析器(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
则是存放下一个字符 。code
方法 char readChar()
用来读取下一个字符,当返回 -1
时代表读取完毕,其重载方法 char readChar(int offset)
用来指定偏移多少位置后读取字符,从 0 开始且 0 至关于调用了该方法的无参重载。htm
public class Lexer { private char currentChar = '\0'; private char nextChar = '\0'; private char readChar() { // ... } private char readChar(int offset) { // ... } }
接下来定义 Lexer
类的公有方法 Token nextToken()
来读取一个记号,它分析字符串的流程以下:token
currentChar
存放当前须要匹配的字符,若读取到文件末尾则返回 EOF
记号。注意如果代码较短,则这里的子过程并不必定须要写成函数。ip
整个词法分析器其实就是个不肯定的有限自动机(NFA),开始时并不知道匹配何种记号,这里称之为 不肯定匹配状态
。经过单个或多个字符就能肯定匹配何种记号并能够调用子过程,这时进入了 肯定匹配状态
,而子过程就是个肯定的有限自动机(DFA),称这些字符或字符序列为 匹配前缀
。
记号能够分为如下几类,这些记号根据匹配前缀能够分为须要双字符和只需单字符肯定,双字符肯定的记号只有注释和双字符符号,其余都为单字符肯定的,这也是为何前面须要声明 nextChar
变量存放下一个字符。其中的标识符包含了保留字,而符号分为运算符及界符。
有些状况下,单字符肯定的匹配会影响双字符肯定的匹配,为了消除这种歧义,就须要先进行双字符匹配再进行单字符匹配。
例如单行注释以双字符 //
做为匹配前缀,而单字符符号除号 /
会影响该双字符肯定的匹配,如果将单字符肯定的匹配放前面,则会匹配成两个除号记号。
在不一样的系统中,文件的换行有如下三种:
为了兼容考虑,匹配换行具体代码以下所示:
if (currentChar == '\r' || currentChar == '\n') { newLine(); continue; } private void newLine() { nextChar = readChar(); if (nextChar == '\n') { currentChar = readChar(); } else { currentChar = nextChar; nextChar = '\0'; } }