词法分析的工作是将一个长长的字符串识别出一个个的单词,这一个个单词就是 Token。而且词法分析的工作是一边读取一边识别字符串的,不是把字符串都读到内存再识别
字符串是一连串的字符形成的,怎么把它断开成一个个的 Token 呢?分割的依据是什么呢? 其实,我们实现词法分析器的过程,就是写出正则表达式,画出有限自动机的图形,然后根据图形直观地写出解析代码的过程。
下图展示了简单的正则表达式规则
解析 age >= 45
以解析age >= 45 为例,词法分析的过程如下图所示
我们来描述一下标识符、比较操作符和数字字面量这三种 Token 的词法规则。
- 标识符:第一个字符必须是字母,后面的字符可以是字母或数字。
- 比较操作符:> 和 >=(其他比较操作符暂时忽略)。
- 数字字面量:全部由数字构成(像带小数点的浮点数,暂时不管它。
我们就是依据这样的规则,来构造有限自动机的。这样,词法分析程序在遇到 age、>= 和 45 时,会分别识别成标识符、比较操作符和数字字面量。不过上面的图只是一个简化的示意图,一个严格意义上的有限自动机是下面这种画法:
解释一下上图的 5 种状态。
- 初始状态:刚开始启动词法分析的时候,程序所处的状态。
- 标识符状态:在初始状态时,当第一个字符是字母的时候,迁移到状态 2。当后续字符是字母和数字时,保留在状态 2。如果不是,就离开状态 2,写下该 Token,回到初始状态。
- 大于操作符(GT):在初始状态时,当第一个字符是 > 时,进入这个状态。它是比较操作符的一种情况。
- 大于等于操作符(GE):如果状态 3 的下一个字符是 =,就进入状态 4,变成 >=。它也是比较操作符的一种情况。
- 数字字面量:在初始状态时,下一个字符是数字,进入这个状态。如果后续仍是数字,就保持在状态 5。
这里我想补充一下,你能看到上图中的圆圈有单线的也有双线的。双线的意思是这个状态已经是一个合法的 Token 了,单线的意思是这个状态还是临时状态。
按照这 5 种状态迁移过程,你很容易编成程序。我们先从状态 1 开始,在遇到不同的字符时,分别进入 2、3、5 三个状态:
if (this.isAlpha(ch)) {
//第一个字符是字母
if (ch === "i") {
newState = DfaState.Id_let1;
} else {
//进入Id状态
newState = DfaState.Id;
}
this.token.type = TokenType.Identifier;
} else if (this.isDigit(ch)) {
//第一个字符是数字
newState = DfaState.LetLiteral;
this.token.type = TokenType.LetLiteral;
}
上面的代码中,nextState 是接下来要进入的状态。用枚举(enum)类型定义了一些枚举值来代表不同的状态,让代码更容易读。
其中 Token 是自定义的一个数据结构,它有两个主要的属性:一个是“type”,就是 Token 的类型,它用的也是一个枚举类型的值;一个是“text”,也就是这个 Token 的文本值。
我们接着处理进入 2、3、5 三个状态之后的状态迁移过程:
case DfaState.Initial:
state = this.initToken(ch);
break;
case DfaState.Id:
if (this.isAlpha(ch) || this.isDigit(ch)) {
this.appendTokenText(ch);
} else {
state = this.initToken(ch);
}
break;
case DfaState.GT:
if (ch === '=') {
this.token.type = TokenType.GE;
state = DfaState.GE;
this.appendTokenText(ch)
} else {
state = this.initToken(ch)
}
break;
case DfaState.GE:
case DfaState.Assignment:
case DfaState.Plus:
case DfaState.Minus:
case DfaState.Star:
case DfaState.Slash:
case DfaState.SemiColon:
case DfaState.LeftParen:
case DfaState.RightParen:
state = this.initToken(ch); //退出当前状态,并保存Token
break;
case DfaState.LetLiteral:
if (this.isDigit(ch)) {
this.appendTokenText(ch)
} else {
state = this.initToken(ch)
}
break;
运行这个示例程序,你就会成功地解析类似“age >= 45”这样的程序语句。
解析 int age = 40
如果把这个语句涉及的词法规则用正则表达式写出来,是下面这个样子:
Int: 'int'
Id : [a-zA-Z_] ([a-zA-Z_] | [0-9])*
Assignment : '='
int 这个关键字,与标识符很相似,都是以字母开头,后面跟着其他字母。
换句话说,int 这个字符串,既符合标识符的规则,又符合 int 这个关键字的规则,这两个规则发生了重叠。这样就起冲突了,我们扫描字符串的时候,到底该用哪个规则呢?
当然,我们心里知道,int 这个关键字的规则,比标识符的规则优先级高。普通的标识符是不允许跟这些关键字重名的。
在这里,我们来回顾一下:什么是关键字?
关键字是语言设计中作为语法要素的词汇,例如表示数据类型的 int、char,表示程序结构的 while、if,表述特殊数据取值的 null、NAN 等。
除了关键字,还有一些词汇叫保留字。保留字在当前的语言设计中还没用到,但是保留下来,因为将来会用到。我们命名自己的变量、类名称,不可以用到跟关键字和保留字相同的字符串。那么我们在词法分析器中,如何把关键字和保留字跟标识符区分开呢?
以“int age = 40”为例,我们把有限自动机修改成下面的样子,借此解决关键字和标识符的冲突。
这个思路其实很简单。在识别普通的标识符之前,你先看看它是关键字还是保留字就可以了。具体做法是:
当第一个字符是 i 的时候,我们让它进入一个特殊的状态。接下来,如果它遇到 n 和 t,就进入状态 4。但这还没有结束,如果后续的字符还有其他的字母和数字,它又变成了普通的标识符。比如,我们可以声明一个 intA(int 和 A 是连着的)这样的变量,而不会跟 int 关键字冲突。完整的代码见最下方。
上面的两个例子虽然简单,但其实已经讲清楚了词法原理,就是依据构造好的有限自动机,在不同的状态中迁移,从而解析出 Token 来只要再扩展这个有限自动机,增加里面的状态和迁移路线,就可以逐步实现一个完整的词法分析器了。
本篇主要参考极客时间中课程使用JS实现的词法分析器,运行示例程序如下代码
let string = "int age = 45;";
console.log(string);
const lexer = new SimpleLexer();
let tokenReader = lexer.tokenize(string);
dump(tokenReader);
string = "inta age = 45;";
console.log(string);
tokenReader = lexer.tokenize(string);
dump(tokenReader);
string = "age >= 45;";
console.log(string);
tokenReader = lexer.tokenize(string);
dump(tokenReader);
运行结果如下图所示
完整代码实现
源码地址:SimpleLexer
// 定义各种状态机
const DfaState = {
Initial: Symbol("Initial"),
If: Symbol("If"),
Id_if1: Symbol("Id_if1"),
Id_if2: Symbol("Id_if2"),
Else: Symbol("Else"),
Id_else1: Symbol("Id_else1"),
Id_else2: Symbol("Id_else2"),
Id_else3: Symbol("Id_else3"),
Id_else4: Symbol("Id_else4"),
Int: Symbol("Int"),
Id_int1: Symbol("Id_int1"),
Id_int2: Symbol("Id_int2"),
Id_int3: Symbol("Id_int3"),
Id: Symbol("Id"),
GT: Symbol("GT"),
GE: Symbol("GE"),
Assignment: Symbol("Assignment"),
Plus: Symbol("Plus"),
Minus: Symbol("Minus"),
Star: Symbol("Star"),
Slash: Symbol("Slash"),
SemiColon: Symbol("SemiColon"),
LeftParen: Symbol("LeftParen"),
RightParen: Symbol("RightParen"),
IntLiteral: Symbol("IntLiteral"),
};
// 定义token类型
export const TokenType = {
Identifier: Symbol("Identifier"),
IntLiteral: Symbol("IntLiteral"),
GT: Symbol("GT"),
Plus: Symbol("Plus"),
Minus: Symbol("Minus"),
Star: Symbol("Star"),
Slash: Symbol("Slash"),
SemiColon: Symbol("SemiColon"),
LeftParen: Symbol("LeftParen"),
RightParen: Symbol("RightParen"),
Assignment: Symbol("Assignment"),
Int: Symbol("Int"),
};
// 单个token对象
class SimpleToken {
constructor() {
this.type = null;
this.text = null;
}
getType() {
return this.type;
}
getText() {
return this.text;
}
}
class TokenReader {
constructor(tokens) {
this.tokens = tokens;
this.pos = 0;
}
read() {
if (this.pos < this.tokens.length) {
return this.tokens[this.pos++];
}
return null;
}
peek() {
if (this.pos < this.tokens.length) {
return this.tokens[this.pos]
}
return null;
}
unread() {
if (this.pos > 0) {
this.pos--;
}
}
getPosition() {
return this.pos;
}
setPosition(position) {
if (position >= 0 && position < this.token.length) {
this.pos = position;
}
}
}
export function dump(tokenReader){
let token = null;
while((token = tokenReader.read())){
console.log(token.type ,token.getText())
}
}
export class SimpleLexer {
constructor() {
this.tokenText = null;
this.tokens = [];
this.token = null;
}
appendTokenText(ch) {
this.tokenText = this.tokenText + ch;
}
// 是否是字母
isAlpha(ch) {
return (ch >= "a" && ch <= "z") || (ch >= "A" && ch <= "Z");
}
// 是否是数字
isDigit(ch) {
return ch >= "0" && ch <= "9";
}
// 是否是空白符
isBlank(ch) {
return ch === " " || ch === "\t" || ch === "\n";
}
/**
* 有限状态机进入初始状态。
* 这个初始状态其实并不做停留,它马上进入其他状态。
* 开始解析的时候,进入初始状态;某个Token解析完毕,也进入初始状态,在这里把Token记下来,然后建立一个新的Token。
* @param ch
* @return
*/
initToken(ch) {
if (this.tokenText.length > 0) {
this.token.text = this.tokenText;
if(this.token.type){
this.tokens.push(this.token);
}
this.tokenText = "";
this.token = new SimpleToken();
}
let newState = DfaState.Initial;
if (this.isAlpha(ch)) {
//第一个字符是字母
if (ch === "i") {
newState = DfaState.Id_int1;
} else {
//进入Id状态
newState = DfaState.Id;
}
this.token.type = TokenType.Identifier;
} else if (this.isDigit(ch)) {
//第一个字符是数字
newState = DfaState.IntLiteral;
this.token.type = TokenType.IntLiteral;
} else if (ch === ">") {
//第一个字符是>
newState = DfaState.GT;
this.token.type = TokenType.GT;
} else if (ch === "+") {
newState = DfaState.Plus;
this.token.type = TokenType.Plus;
} else if (ch === "-") {
newState = DfaState.Minus;
this.token.type = TokenType.Minus;
} else if (ch === "*") {
newState = DfaState.Star;
this.token.type = TokenType.Star;
} else if (ch === "/") {
newState = DfaState.Slash;
this.token.type = TokenType.Slash;
} else if (ch === ";") {
newState = DfaState.SemiColon;
this.token.type = TokenType.SemiColon;
} else if (ch === "(") {
newState = DfaState.LeftParen;
this.token.type = TokenType.LeftParen;
} else if (ch === ")") {
newState = DfaState.RightParen;
this.token.type = TokenType.RightParen;
} else if (ch === "=") {
newState = DfaState.Assignment;
this.token.type = TokenType.Assignment;
}
this.appendTokenText(ch);
return newState;
}
getTokens(code) {
return code.replace(/(.)(?=[^$])/g, "$1,").split(",")
}
tokenize(code) {
this.tokens = [];
const reader = new TokenReader(this.getTokens(code))
this.tokenText = "";
this.token = new SimpleToken();
let chI = 0;
let ch = 0;
let state = DfaState.Initial;
try {
while ((chI = reader.read())) {
ch = chI;
// console.log(ch);
switch (state) {
case DfaState.Initial:
state = this.initToken(ch);
break;
case DfaState.Id:
if (this.isAlpha(ch) || this.isDigit(ch)) {
this.appendTokenText(ch);
} else {
state = this.initToken(ch);
}
break;
case DfaState.GT:
if (ch === '=') {
this.token.type = TokenType.GE;
state = DfaState.GE;
this.appendTokenText(ch)
} else {
state = this.initToken(ch)
}
break;
case DfaState.GE:
case DfaState.Assignment:
case DfaState.Plus:
case DfaState.Minus:
case DfaState.Star:
case DfaState.Slash:
case DfaState.SemiColon:
case DfaState.LeftParen:
case DfaState.RightParen:
state = this.initToken(ch); //退出当前状态,并保存Token
break;
case DfaState.IntLiteral:
if (this.isDigit(ch)) {
this.appendTokenText(ch)
} else {
state = this.initToken(ch)
}
break;
case DfaState.Id_int1:
if (ch === 'n') {
state = DfaState.Id_int2;
this.appendTokenText(ch)
} else if (this.isDigit(ch) || this.isAlpha(ch)) {
state = DfaState.Id; //切换回Id状态
this.appendTokenText(ch);
} else {
state = this.initToken(ch)
}
break;
case DfaState.Id_int2:
if (ch === 't') {
state = DfaState.Id_int3;
this.appendTokenText(ch)
} else if (this.isDigit(ch) || this.isAlpha(ch)) {
state = DfaState.Id; //切换回Id状态
this.appendTokenText(ch);
} else {
state = this.initToken(ch)
}
break;
case DfaState.Id_int3:
if (this.isBlank(ch)) {
this.token.type = TokenType.Int;
state = this.initToken(ch)
} else {
state = DfaState.Id;
this.appendTokenText(ch)
}
break;
default:
break;
}
}
// 把最后一个token送进去
if (this.tokenText.length > 0) {
this.initToken(ch)
}
} catch (error) {
console.log(error)
}
return new TokenReader(this.tokens)
}
}
// const simpleLexer = new SimpleLexer();