编译原理实战一:如何用JS实现一个词法分析器?

2,547 阅读7分钟

词法分析的工作是将一个长长的字符串识别出一个个的单词,这一个个单词就是 Token。而且词法分析的工作是一边读取一边识别字符串的,不是把字符串都读到内存再识别

字符串是一连串的字符形成的,怎么把它断开成一个个的 Token 呢?分割的依据是什么呢? 其实,我们实现词法分析器的过程,就是写出正则表达式,画出有限自动机的图形,然后根据图形直观地写出解析代码的过程。

下图展示了简单的正则表达式规则 image.png

解析 age >= 45

以解析age >= 45 为例,词法分析的过程如下图所示 image.png

我们来描述一下标识符、比较操作符和数字字面量这三种 Token 的词法规则。

  • 标识符:第一个字符必须是字母,后面的字符可以是字母或数字。
  • 比较操作符:> 和 >=(其他比较操作符暂时忽略)。
  • 数字字面量:全部由数字构成(像带小数点的浮点数,暂时不管它。

我们就是依据这样的规则,来构造有限自动机的。这样,词法分析程序在遇到 age、>= 和 45 时,会分别识别成标识符、比较操作符和数字字面量。不过上面的图只是一个简化的示意图,一个严格意义上的有限自动机是下面这种画法: image.png

解释一下上图的 5 种状态。

  1. 初始状态:刚开始启动词法分析的时候,程序所处的状态。
  2. 标识符状态:在初始状态时,当第一个字符是字母的时候,迁移到状态 2。当后续字符是字母和数字时,保留在状态 2。如果不是,就离开状态 2,写下该 Token,回到初始状态。
  3. 大于操作符(GT):在初始状态时,当第一个字符是 > 时,进入这个状态。它是比较操作符的一种情况。
  4. 大于等于操作符(GE):如果状态 3 的下一个字符是 =,就进入状态 4,变成 >=。它也是比较操作符的一种情况。
  5. 数字字面量:在初始状态时,下一个字符是数字,进入这个状态。如果后续仍是数字,就保持在状态 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”为例,我们把有限自动机修改成下面的样子,借此解决关键字和标识符的冲突。 image.png

这个思路其实很简单。在识别普通的标识符之前,你先看看它是关键字还是保留字就可以了。具体做法是:

当第一个字符是 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();