基于JavaScript的简朴诠释器完成(一)——表达式的语法树天生

媒介

这个系列是关于CodeWars上的一条1Kyu题:Simple Interactive Interpreter。也就是完成一个简朴的交互式诠释器。
问题地点:http://www.codewars.com/kata/52ffcfa4aff455b3c2000750/train/javascript
github地点:https://github.com/woodensail/SimpleInteractiveInterpreter
本文地点:http://segmentfault.com/a/1190000004044789

补充

11月26日更新:
增加了对左连系运算符的支撑。
增加了变量统计功用,用于支撑下一步的函数parser
当表达式不相符语法请求时,抛出非常

完成请求

详细的细节能够拜见上面的原题网站,也许的请求以下:
1:支撑变量赋值语句

x = 7

2:支撑四则运算,表达式中能够运用变量

x = (1 + 2) * y

3:函数声明:

fn add x y => x + y

4:函数挪用

z = add a b

5:其他
也就是定名争执检测,作用域链等,人人本身看吧。

语法树

这一章主如果完成语法树的天生。个中因为函数声明部份过于简朴,没必要天生语法树,盘算留到下一章一同处置惩罚。所以只做了表达式的语法树天生。

起首,问题所给的言语构造基本上是前缀表达式和中缀表达式的混淆。所以只须要将语句内里中缀的部份转化为前缀即可获得波兰式。
固然,我为了轻易下一步处置惩罚照样挑选将其进一步转化为语法树的构造。然则完成思绪照旧能够参考波兰式天生

准备工作

var SPACE = {}, params = [], operatorStack = [], dataStack = [SPACE], expressionFlag = true, lValue, rValue, operator, vars = {};

声明变量:
params 用于存储函数挪用的参数,实在这里不须要初始化,但我懒得改了。
operatorStack 运算符栈,用于存储种种操纵符
dataStack 存储数据。包含数值,变量以及语法树的节点
expressionFlag 因为改言语中没有逗号,所以没有显式的标志来支解相邻的两个表达式。因而须要自行推断前一个表达式是不是终了。
lValue,rValue 相似params, 只不过是给运算符用的,实在能够去掉,但我懒得改。
operator 一个用于存储当前运算符的暂时变量

tokens = tokens.slice();
tokens.push(')');
tokens.unshift('(');
while (tokens.length) {
  ……
}
var varList = [];
for (var k in vars) {
    varList.push(k);
}
if (dataStack[0] === SPACE) {
    dataStack.shift();
} else {
    throw 'expression error'
}
if (dataStack.length > 1) {
    throw 'expression error'
}
return [dataStack[0], varList];

复制token数组,防备修正原始数据。向开首和末端加上括号,以简化背面的操纵。末了就是最先主轮回。
主轮回终了后数据栈中的第一位元素则为语法树。若数据栈中元素数目多于一个或栈低占位符被掏出,申明语句有错。

主轮回

var next = tokens.pop();

起首掏出一个token。须要注重的是这里用的是pop,也就是从后向前扫描。然后依据token范例做差别处置惩罚。

1:token为运算符

if (operators[next]) {
    while (true) {
        if (!operatorStack.length) {
            operatorStack.push(next);
            break;
        } else if (operatorStack[operatorStack.length - 1] === ')') {
            operatorStack.push(next);
            break;
        }  else if ((operators[operatorStack[operatorStack.length - 1]] === operators[next] ) && (next !== '=')) {
            operatorStack.push(next);
            break;
        } else if (operators[operatorStack[operatorStack.length - 1]] > operators[next]) {
            operatorStack.push(next);
            break;
        } else {
            operator = operatorStack.pop();
            lValue = dataStack.pop();
            rValue = dataStack.pop();
            dataStack.push([operator, lValue, rValue]);
        }
    }
    expressionFlag = true;
    continue;
}

a:若此时运算符栈为空,则将该运算符入栈。
b:若栈顶运算符为右括号,则将该运算符入栈。
c:若栈顶运算符优先级即是当前运算符且当前运算符不是左连系运算符,则将该运算符入栈。
d:若栈顶运算符优先级小于当前运算符,则将该运算符入栈。
e:若非以上四种状况。则运算符栈出栈存入operator,数据栈出栈两次离别存入lValue,rValue,然后将[operator, lValue, rValue]压入数据栈。并继承轮回直到涌现前四种状况为止。
前面的轮回终了后将expressionFlag设为真,以标志当前表达式未终了。末了挪用continue跳过背面的部份。

2:token为左括号

else if (next === '(') {
    next = operatorStack.pop();
    while (next !== ')') {
        if (next === void 0) {
            break
        }
        lValue = dataStack.pop();
        rValue = dataStack.pop();
        dataStack.push([next, lValue, rValue]);
        next = operatorStack.pop();
    }
    continue;
}

延续出栈直到栈顶元素为右括号为止。关于每一个出栈的操纵符将其存入operator并从数据栈中出栈两次获得lValue和rValue,并将[operator, lValue, rValue]压入数据栈。末了continue跳过后续。

3:expressionFlag的推断

if (expressionFlag) {
    expressionFlag = false;
} else {
    while (operatorStack.length) {
        operator = operatorStack.pop();
        if (operator === ')') {
            operatorStack.push(operator);
            break;
        } else {
            lValue = dataStack.pop();
            rValue = dataStack.pop();
            dataStack.push([operator, lValue, rValue]);
        }
    }
}

若token不是前两种状况,则须要推断expressionFlag。若expressionFlag为真则将其置为假,规范该token处置惩罚完后,当前表达式能够终了。
若其为假则申明当前表达式已终了,须要最先下一个表达式。此时须要将运算符栈重复出栈并与数据栈顶的两位兼并后压入数据栈,直到栈顶运算符为右括号为止。

4:token为右括号或其他在函数列表中不存在的内容

if (next === ')') {
    expressionFlag = true;
    operatorStack.push(next);
} else if (!this.functions[next]) {
    if (!this.regexNum.test(next)) {
        vars[next] = 1;
    } else {
        next = Number(next);
    }
    dataStack.push(next);
}

将token入栈,个中若token为右括号,则expressionFlag置真示意表达式未终了。若不为右括号,当next为纯数字时将其转化为Number型,否则在变量表中标记。

5:token在函数表中存在

else {
    params = [next];
    for (var i in this.functions[next].params) {
        params.push(dataStack.pop());
    }
    dataStack.push(params);
}

初始化params而且第一位为当前token。依据函数表中形参的数目,从数据栈中掏出一样数目的数据,压入params。
将params压入数据栈

运转剖析:

这里用’a*(test q (e+q))-(a+b)/e’做例子来跟踪并展现顺序是如何运转的。
起首tokenize,结果是:

["(","a","*","(","test","q","(","e","+","q",")",")","-","(","a","+","b",")","/","e",")"]

然后最先轮回,我会在每一个操纵的下发顺次说明操纵完成后的数据栈,运算符栈以及expressionFlag
1:’)’, 右括号,压入运算符栈。
[] [‘)’] true
2:’e’, 非运算符或括号或函数,压入数据栈。
[‘e’] [‘)’] false
3:’/’, 运算符,栈顶为右括号,压入运算符栈。
[‘e’] [‘)’, ‘/’] true
4:’)’, 右括号,压入运算符栈。
[‘e’] [‘)’, ‘/’, ‘)’] true
5:’b’, 非运算符或括号或函数,压入数据栈。
[‘e’, ‘b’] [‘)’, ‘/’, ‘)’] false
6:’+’, 运算符,栈顶为右括号,压入运算符栈。
[‘e’, ‘b’] [‘)’, ‘/’, ‘)’, ‘+’] true
7:’a’, 非运算符或括号或函数,压入数据栈。
[‘e’, ‘b’, ‘a’] [‘)’, ‘/’, ‘)’, ‘+’] false
8:’a’, 左括号,实行运算符出栈操纵,直到右括号为止。
[‘e’, [‘+’,’a’,’b’]] [‘)’, ‘/’] false
9:’-‘, 运算符,优先级小于栈顶元素,实行运算符出栈操纵,然后压入运算符栈。
[[‘/’,[‘+’,’a’,’b’],’e’]] [‘)’, ‘-‘] true
10:’)’, 右括号,压入运算符栈。
[[‘/’,[‘+’,’a’,’b’],’e’]] [‘)’, ‘-‘, ‘)’] true
11:’)’, 右括号,压入运算符栈。
[[‘/’,[‘+’,’a’,’b’],’e’]] [‘)’, ‘-‘, ‘)’, ‘)’] true
12:’q’, 非运算符或括号或函数,压入数据栈。
[[‘/’,[‘+’,’a’,’b’],’e’], ‘q’] [‘)’, ‘-‘, ‘)’, ‘)’] false
13:’+’, 运算符,栈顶为右括号,压入运算符栈。
[[‘/’,[‘+’,’a’,’b’],’e’], ‘q’] [‘)’, ‘-‘, ‘)’, ‘)’, ‘+’] true
14:’e’, 非运算符或括号或函数,压入数据栈。
[[‘/’,[‘+’,’a’,’b’],’e’], ‘q’, ‘e’] [‘)’, ‘-‘, ‘)’, ‘)’, ‘+’] false
15:'(‘, 左括号,实行运算符出栈操纵,直到右括号为止。
[[‘/’,[‘+’,’a’,’b’],’e’], [‘+’,’e’,’q’]] [‘)’, ‘-‘, ‘)’] false
16:’q’, 非运算符或括号或函数,压入数据栈。因为expressionFlag为false,须要提早出栈到右括号为止。
[[‘/’,[‘+’,’a’,’b’],’e’], [‘+’,’e’,’q’], ‘q’] [‘)’, ‘-‘, ‘)’,] false
17:’test’, 函数,实行函数出栈顺序。因为expressionFlag为false,须要提早出栈到右括号为止。
[[‘/’,[‘+’,’a’,’b’],’e’], [‘test’,’q’,[‘+’,’e’,’q’]]] [‘)’, ‘-‘, ‘)’] false
18:'(‘, 左括号,实行运算符出栈操纵,直到右括号为止。
[[‘/’,[‘+’,’a’,’b’],’e’], [‘test’,’q’,[‘+’,’e’,’q’]]] [‘)’, ‘-‘] false
18:’*’, 运算符,优先级大于即是栈顶运算符,压入运算符栈。
[[‘/’,[‘+’,’a’,’b’],’e’], [‘test’,’q’,[‘+’,’e’,’q’]]] [‘)’, ‘-‘, ‘*’] true
18:’a’, 非运算符或括号或函数,压入数据栈。
[[‘/’,[‘+’,’a’,’b’],’e’], [‘test’,’q’,[‘+’,’e’,’q’]], ‘a’] [‘)’, ‘-‘, ‘*’] false
18:'(‘, 左括号,实行运算符出栈操纵,直到右括号为止。
[[‘-‘,[‘*’,’a’,[‘test’,’q’,[‘+’,’e’,’q’]]],[‘/’,[‘+’,’a’,’b’],’e’]]] [] false

这是末了天生的语法树:
《基于JavaScript的简朴诠释器完成(一)——表达式的语法树天生》

总结

总之,语法树就算是天生终了了。上面的代码另有缺点,主如果没有做非常搜检之类的。然则最少关于相符语法的表达式是没什么问题了。下一章会做函数声明的剖析和保留。

    原文作者:WoodenSail
    原文地址: https://segmentfault.com/a/1190000004044789
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞