资讯专栏INFORMATION COLUMN

基于JavaScript的简单解释器实现(一)——表达式的语法树生成

DataPipeline / 523人阅读

摘要:语法树这一章主要是完成语法树的生成。其中由于函数声明部分过于简单,没必要生成语法树,打算留到下一章一起处理。主循环结束后数据栈中的第一位元素则为语法树。这是最后生成的语法树总结总之,语法树就算是生成完毕了。

前言

这个系列是关于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

这是最后生成的语法树:

总结

总之,语法树就算是生成完毕了。上面的代码还有缺陷,主要是没有做异常检查之类的。但是至少对于符合语法的表达式是没什么问题了。下一章会做函数声明的解析和保存。

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/78199.html

相关文章

  • javascript引擎——V8

    摘要:类将源代码解释并构建成抽象语法树,使用类来创建它们,并使用类来分配内存。类抽象语法树的访问者类,主要用来遍历抽象语法树。在该函数中,先使用类来生成抽象语法树再使用类来生成本地代码。 通过上一篇文章,我们知道了JavaScript引擎是执行JavaScript代码的程序或解释器,了解了JavaScript引擎的基本工作原理。我们经常听说的JavaScript引擎就是V8引擎,这篇文章我们...

    luoyibu 评论0 收藏0
  • 第6章:可维护性软件构建方法 6.3可维护性构建技术

    摘要:遵循特定规则,利用操作符,终止节点和其他非终止节点,构造新的字符串非终结符是表示字符串的树的内部节点。语法中的生产具有这种形式非终结符终结,非终结符和运算符的表达式语法的非终结点之一被指定为根。 大纲 基于状态的构建 基于自动机的编程 设计模式:Memento提供了将对象恢复到之前状态的功能(撤消)。 设计模式:状态允许对象在其内部状态改变时改变其行为。 表驱动结构* 基于语法的构...

    young.li 评论0 收藏0
  • JavaScript 是如何工作:解析、抽象语法(AST)+ 提升编译速度5个技巧

    摘要:无论你使用的是解释型语言还是编译型语言,都有一个共同的部分将源代码作为纯文本解析为抽象语法树的数据结构。和抽象语法树相对的是具体语法树,通常称作分析树。这是引入字节码缓存的原因。 这是专门探索 JavaScript 及其所构建的组件的系列文章的第 14 篇。 想阅读更多优质文章请猛戳GitHub博客,一年百来篇优质文章等着你! 如果你错过了前面的章节,可以在这里找到它们: JavaS...

    raoyi 评论0 收藏0
  • WebAssembly 那些事儿

    摘要:的目标是对高级程序中间表示的适当低级抽象,即代码旨在由编译器生成而不是由人来写。表示把源代码变成解释器可以运行的代码所花的时间表示基线编译器和优化编 WebAssembly 那些事儿 什么是 WebAssembly? WebAssembly 是除 JavaScript 以外,另一种可以在网页中运行的编程语言,并且相比之下在某些功能和性能问题上更具优势,过去我们想在浏览器中运行代码来对网...

    邱勇 评论0 收藏0
  • 浏览器是如何工作?(How browser work?)

    摘要:解析器的工作通常分为两个内容词法分析器有时称为标记生成器负责把输入分解为很多符号,解析器负责根据该语言的语法规则来分析文档结构,从而构建解析树。解析器通常会向词法分析器询问是否有新的符号,并且试图通过一条语法规则的来进行匹配。 浏览器是如何工作的(How browser work) 1. 介绍 1.1 本文涉及到的浏览器 1.2 浏览器的主要功能 1.3 浏览器的主要结构 1.4...

    vincent_xyb 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<