编译原理:从0写一个js解释器

前段时间写了一个js解释器,到目前已经已经把除了对象以外的JS大部分基础功能写出来了,在此分享和回顾一下重点技术细节和实现。 github.com/zuluoaaa/mak


0 初始化

我们输入一串有意义的js字符串

1 词法分析

遍历循环输入,将字符串逐个解释成有意义的数据结构(这里用token代表这个数据结构

var a = 1

上面这行代码,会被解析成以下4个token

[{token:"var"},{token:"indent",val:"a"},{token:"assign"},{token:"number",val:1}]

对于上述的输入,将字符串转换成token数组很简单,我们只要去逐个读取输入串的值并跳过其中的空格就可以导出这个值。

但是,这只是一个开始,你可能还需要处理一些不一样的输入,必须要求你读完某个部分的值之后才能判断这个值是什么token

例如:区分== 和 = , > 和 >= ……

解决办法也很简单,直接贴实现代码

case "<":
             next = nextChar();
            if(next === "="){
                token.type = tokenTypes.T_LE;//判断为 <=
            }else {
                token.type = tokenTypes.T_LT;//判断为 <
                putBack(next);
            }

PS:在这一步中,我们不关心语法和语义是否正确,我们只负责解析成token,是关键字就解析成对应的关键字token;如果是数字就解析成数字token,是字母我们就解析成变量token。 然后由后续的程序来处理这些问题。

2 语法分析

将token转换成AST (语法树),也就是将拿到的一组token转换成整体连接的语法结构

这一步就是整个解释器的重点了。

先举个例子

var a = 1 + 3 * 2;

所对应的语法树

=
      /  \
     +    a
    / \
    *  1
  /  \
 3    2

之所以要把变量A放在右侧,而不是左侧,是因为我们后续会通过前序遍历来解析执行AST,这样便于求值。

上面只是一个简单的例子,这一步骤最复杂的部分就是处理各种关键字 if,else ,function,while……

处理复杂的表达式也是令人头大,各种运算符、例如 &&,>,<=,+,-,*,/,?:……

举个例子

let a = 8+6*3-2*5 > 12*3+(1+5) ? 1 : 2;

将以上的表达式解析到对应的AST,不熟悉解释器和编译器的同学(比如之前的我),往往是卡在这一步,不知所措,陷入自我怀疑...

由于这里面处理的细节处理太多了,这里只将核心架构来告诉大家,具体实现的话,大家有兴趣可以到我项目代码里面进行翻看

负责解析的主方法的核心实现

function statement(){
    let tree = null,left = null;
    while (true){
        let {token}  = gData;
        //不同关键字,跳转到对应的解析函数
        switch (token.type) {
            case tokenTypes.T_VAR:
                left = varDeclaration();
                break;
            case tokenTypes.T_IF:
                left = ifStatement();
                break;
            case tokenTypes.T_WHILE:
                left = whileStatement();
                break;
            case tokenTypes.T_FUN:
                left = funStatement();
                break;
            case tokenTypes.T_RETURN:
                left = returnStatement();
                break;
            case tokenTypes.T_EOF://EOF是整个输入串已经执行完毕了,退出解析
                return tree;
            default:
                 left = normalStatement();
        }
        //基本上每次循环只解析一行语句,这里是将多行语句组合起来,最后将整个输入串组装成一棵语法树
        if(left !== null){
            if(tree === null){
                tree = left;
            }else{
                tree = new ASTNode().initTwoNode(ASTNodeTypes.T_GLUE,tree,left,null);
            }
        }
    }
}

function normalStatement() {
    let tree =  parseExpression(0);//执行表达式解析,得到语法树
    semicolon();//检查逗号
    return tree;
}

...

上述是语法解析,下面是最最核心的表达式解析(例如解析算术表达式 1+3*(6+1)

首先定义一组前缀解析和一组中缀解析的map,根据类型自动到对应的解析方法,这样的话,我们有任何新增要解析的符号,直接往里面新增就可以了,而不需要改动函数内部的实现

const prefixParserMap = {
    [tokenTypes.T_IDENT]:identifier,//变量
    [tokenTypes.T_INT]:int,
    [tokenTypes.T_STRING]:str,
    [tokenTypes.T_LPT]:group,//括号
    [tokenTypes.T_LMBR]:array,//中括号
    [tokenTypes.T_ADD]:prefix.bind(null,tokenTypes.T_ADD),
    [tokenTypes.T_SUB]:prefix.bind(null,tokenTypes.T_SUB),
};

const infixParserMap = {
    [tokenTypes.T_LPT]:{parser:funCall,precedence:precedenceList.call},
    [tokenTypes.T_QST]:{parser:condition,precedence:precedenceList.condition},//三元表达式

    [tokenTypes.T_ASSIGN]:{parser:assign,precedence:precedenceList.assign},//= 赋值符

    [tokenTypes.T_AND]:{parser:infix.bind(null,precedenceList.and),precedence:precedenceList.and},
    [tokenTypes.T_OR]:{parser:infix.bind(null,precedenceList.and),precedence:precedenceList.and},
    [tokenTypes.T_ADD]:{parser:infix.bind(null,precedenceList.sum),precedence:precedenceList.sum},
    [tokenTypes.T_SUB]:{parser:infix.bind(null,precedenceList.sum),precedence:precedenceList.sum},

    [tokenTypes.T_GT]:{parser:infix.bind(null,precedenceList.compare),precedence:precedenceList.compare},
    [tokenTypes.T_GE]:{parser:infix.bind(null,precedenceList.compare),precedence:precedenceList.compare},
    ...
};

表达式解析核心实现,使用普拉特分析法(也是递归下降分析法的一种

function parseExpression(precedenceValue) {
    let {token} = gData;

    //获取当前token对应的前缀解析函数
    let prefixParser = prefixParserMap[token.type];

    if(!prefixParser){
        errPrint(`unknown token : ${token.value}${token.type})`)
    }

    let left = prefixParser();//执行解析函数
    scan();
    if(token.type === tokenTypes.T_SEMI
        || token.type === tokenTypes.T_RPT
        || token.type === tokenTypes.T_EOF
        || token.type === tokenTypes.T_COMMA
        || token.type === tokenTypes.T_COL
        || token.type === tokenTypes.T_RMBR
    ){
        return left;
    }
    let value = getPrecedence();//获取当前运算符的优先级
    while (value>precedenceValue){
// 如果当前运算符的优先大于之前的优先级,就继续向下解析
// 例如1+6*7,很明显 * 的优先级是大于 + 的,所以我们先解析 6 * 7再回去解析前面的
        let type = token.type;
        if(token.type === tokenTypes.T_SEMI
            || token.type === tokenTypes.T_RPT
            || token.type === tokenTypes.T_EOF
            || token.type === tokenTypes.T_COMMA
            || token.type === tokenTypes.T_RMBR
        ){
            return left;
        }
        let infix = infixParserMap[type]; 
        scan();
        left = infix.parser(left,type);

        if(infixParserMap[token.type]){
            value = getPrecedence();
        }
    }

    return left;
}

关于普拉特解析法,特别推荐这位大佬写的关于普拉特介绍 journal.stuffwithstuff.com

3 解释执行AST

将前一步得到的AST语法树通过前序遍历,挨个节点执行,求值,一个简单的解释器就这么搞定了。

function interpretAST(astNode,result=null,scope){
    ...

    let leftResult,rightResult;
    if(astNode.left){
        leftResult = interpretAST(astNode.left,null,scope);
    }
    if(astNode.right){
        rightResult = interpretAST(astNode.right,leftResult,scope);
    }

    ...

    switch (astNode.op) {
        case ASTNodeTypes.T_VAR:
            scope.add(astNode.value);
            return;
        case ASTNodeTypes.T_INT:
            return astNode.value;
        case ASTNodeTypes.T_STRING:
            return astNode.value;
        case ASTNodeTypes.T_ADD:
            if(rightResult === null || typeof rightResult === "undefined"){
                return leftResult;
            }
            return leftResult + rightResult;
        case ASTNodeTypes.T_SUB:
            if(rightResult === null || typeof rightResult === "undefined"){
                return -leftResult;
            }
            return leftResult - rightResult;
        case ASTNodeTypes.T_MUL:
            return leftResult * rightResult;
        case ASTNodeTypes.T_DIV:
            return leftResult / rightResult;
        case ASTNodeTypes.T_ASSIGN:
            return rightResult;
        case ASTNodeTypes.T_IDENT:
            return findVar(astNode.value,scope);
        case ASTNodeTypes.T_GE:
            return  leftResult >= rightResult;
        case ASTNodeTypes.T_GT:
            return  leftResult > rightResult;
        ...

最后

完毕,有兴趣的同学可以到我的github 仓库上查看完整的实现 github.com/zuluoaaa/mak

写的可能不是很好,如有错误,请指出。

发布于 04-30