资讯专栏INFORMATION COLUMN

基于JavaScript的简单解释器实现(二)——函数解析与执行

greatwhole / 3102人阅读

摘要:函数执行函数执行使用后续遍历的方式来遍历语法树。对于每一个子节点,若其为函数则递归调用执行函数。如果当前方法是运算符方法,则调用该运算符的执行函数,并返回结果如果当前方法是函数,则解析所有形参的值后生产函数作用域,并以改作用域执行当前函数。

前言

昨晚奋斗了一下,终于把这题了解了。今天完善了一下代码,把剩下的部分放上来。目前剩下的有两个主要模块即函数解析与函数执行,以及两个小模块即运算符执行和变量解析。
题目地址:http://www.codewars.com/kata/52ffcfa4aff455b3c2000750/train/javascript
github地址:https://github.com/woodensail/SimpleInteractiveInterpreter
前文地址:http://segmentfault.com/a/1190000004044789
本文地址:http://segmentfault.com/a/1190000004047915

函数解析
var index = tokens.indexOf("=>"), paramObj = {}, params = [], fnName = tokens[1];

初始化参数,paramObj用于统计函数体中用到的参数,params为形参列表,index为函数运算符的位置,fnName为函数名

if (this.vars[fnName] !== void 0) {
    throw "name conflicting"
}

如果全局变量中存在该名称的变量,则抛出异常

for (var i = 2; i < index; i++) {
    if (paramObj[tokens[i]]) {
        throw "param conflicting"
    }
    paramObj[tokens[i]] = 1;
    params.push(tokens[i]);
}

统计形参,如果同名的形参则抛出异常。

var result = this.expressionParser(tokens.slice(index + 1));
var syntaxTree = result[0], varList = result[1];
varList.forEach(function (v) {
    if (!paramObj[v]) {
        throw "nonexistent param"
    }
});
this.functions[fnName] = {params: params, syntaxTree: syntaxTree}

调用表达式解析器解析函数体部分。检查函数体中用到的参数,如果存在形参列表中不存在的参数则抛出异常。
最后将该函数存入函数表。

变量解析
Interpreter.prototype.extractValue = function (key, scope) {
    scope = scope || {};
    var value = scope[key];
    if (value === void 0) {
        value = this.vars[key];
    }
    if (value === void 0) {
        value = key;
    }
    if ("number" === typeof value) {
        return value;
    }
    throw "nonexistent var";
};

按照就优先级分别尝试提取作用域中的变量和全局变量以及key自身。提取完毕后若value不为number则所请求的值不存在。

运算符实现
Interpreter.prototype.add = function (x, y, scope) {
    return this.extractValue(x, scope) + this.extractValue(y, scope);
};
Interpreter.prototype.sub = function (x, y, scope) {
    return this.extractValue(x, scope) - this.extractValue(y, scope);
};
Interpreter.prototype.mul = function (x, y, scope) {
    return this.extractValue(x, scope) * this.extractValue(y, scope);
};
Interpreter.prototype.div = function (x, y, scope) {
    return this.extractValue(x, scope) / this.extractValue(y, scope);
};
Interpreter.prototype.mod = function (x, y, scope) {
    return this.extractValue(x, scope) % this.extractValue(y, scope);
};
Interpreter.prototype.assign = function (x, y, scope) {
    var value = this.extractValue(y, scope);
    if (scope.x !== void 0) {
        return scope[x] = value;
    } else if ("number" === typeof x) {
        throw "assign to lValue"
    } else if (!this.functions[x]) {
        return this.vars[x] = value;
    }
    throw "name conflicting"
};

加减乘除模没什么特殊的就是解析变量后运算然后返回结果即可。
赋值语句需要对被赋值变量进行判断,如果当前函数作用域中有该变量则赋值后返回,如果被赋值对象为数字,则抛出左值异常。如果函数表中不存在对应函数则存入全局变量,否则抛出重名异常。

函数执行

函数执行使用后续遍历的方式来遍历语法树。先依次计算每个参数的结果后,再用获得的结果集执行根节点。

Interpreter.prototype.exec = function (syntaxTree, scope) {
    scope = scope || {};
    ……
};

形参为语法树和作用域。若未指定作用域则新建空作用域。

for (var i = 1; i < syntaxTree.length; i++) {
    if (syntaxTree[i] instanceof Array) {
        syntaxTree[i] = this.exec(syntaxTree[i], scope);
    }
}

对于每一个子节点,若其为函数则递归调用执行函数。这一步执行完毕后当前参数列表中应该只存在变量或数字立即量。

if (this.native[name]) {
    params = syntaxTree.slice(1);
    params.push(scope);
    return this.native[name].apply(this, params);
}

如果当前方法是运算符方法,则调用该运算符的执行函数,并返回结果

else if (this.functions[name]) {
    var fun = this.functions[name];
    params = {};

    fun.params.forEach(function (key, i) {
        var k = syntaxTree[i + 1];
        params[key] = _this.extractValue(k, scope);
    });

    return this.exec(fun.syntaxTree, params);
}

如果当前方法是函数,则解析所有形参的值后生产函数作用域,并以改作用域执行当前函数。

 else {
    return this.extractValue(syntaxTree, scope);
}

如果不是以上任一种,则当前执行的语句为数据,直接提取后返回。

总结

一个基本的解释器就算是完成了,有些没有技术含量的衔接代码我没有贴上来,大家可以去git上看。这个解释器再加上输入输出部分就可以构成一个REPL了。顺便,晒个AC图。

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

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

相关文章

  • 【译】小百行 JavaScript 打造 lambda 演算释器

    摘要:在开始解析之前,先通过词法分析器运行源码,这会将源码打散成语法中全大写的部分。我们基于每个规则的名称的左侧为其创建一个方法,再来看右侧内容如果是全大写的单词,说明它是一个终止符即一个,词法分析器会用到它。 本文转载自:众成翻译译者:文蔺链接:http://www.zcfy.cc/article/661原文:http://tadeuzagallo.com/blog/writing-a-l...

    KitorinZero 评论0 收藏0
  • JavaScript工作原理():V8引擎和5招高效代码

    摘要:引擎可以用标准解释器或即时编译器来实现,即时编译器以某种形式将代码编译为字节码。这里的主要区别在于不生成字节码或任何中间代码。请注意,不使用中间字节码表示法,不需要解释器。这允许在正常执行期间非常短的暂停。 本系列的第一篇文章重点介绍了引擎,运行时和调用栈的概述。第二篇文章将深入V8的JavaScript引擎的内部。我们还会提供一些关于如何编写更好的JavaScript代码的技巧。 概...

    leone 评论0 收藏0
  • JavaScript语言特性以及重要版本

    摘要:通常一个完成的不仅仅包含了还包括了以及相关版本该版本在中使用。基于原型函数先行的语言使用基于原型的的继承机制,函数是的第一等公民其他相关的语言特性编译型语言把做好的源程序全部编译成二进制代码的可运行程序。 转载请注明出处,创作不易,更多文章请戳 https://github.com/ZhengMaste... 前言:JavaScript诞生于1995年,它是一门脚本语言,起初的目...

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

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

    raoyi 评论0 收藏0
  • javascript引擎——V8

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

    luoyibu 评论0 收藏0

发表评论

0条评论

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