## 如何使用 JavaScript 实现一门编程语言(6) —— Interpreter
到目前为止,我们写了3个函数:InputStream,TokenStream 和 parse。为了从一段代码中获取AST,我们可以执行以下操作:
```
var ast = parse (TokenStream (InputStream (code ))) ;
```
获取AST后就可以编写Interpreter(解释器)了,这比写parser容易。我们只需走AST,以正常顺序执行表达式。
**执行环境**
正确执行表达式的关键是正确维护执行环境 - 一个拥有绑定变量的上下文。它将作为参数传递给我们的evaluate函数。每次我们进入”lambda”节点时,我们都必须用新变量(函数的参数)扩展环境,并用运行时传递的值对它们进行初始化。如果一个参数影响了外部作用域的变量,我们必须小心地在离开函数时恢复先前的值。
实现这个最简单的方法是使用JavaScript的原型继承。当我们输入一个函数时,我们将创建一个新的环境。这种方式当我们退出时,我们不需要做任何事情——外部env已经包含在对象本身。
以下是Environment对象的定义:
```
function Environment(parent) {
this.vars = Object.create(parent ? parent.vars : null);
this.parent = parent;
}
Environment.prototype = {
extend: function() {
return new Environment(this);
},
lookup: function(name) {
var scope = this;
while (scope) {
if (Object.prototype.hasOwnProperty.call(scope.vars, name))
return scope;
scope = scope.parent;
}
},
get: function(name) {
if (name in this.vars)
return this.vars[name];
throw new Error("Undefined variable " + name);
},
set: function(name, value) {
var scope = this.lookup(name);
// let's not allow defining globals from a nested environment
if (!scope && this.parent)
throw new Error("Undefined variable " + name);
return (scope || this).vars[name] = value;
},
def: function(name, value) {
return this.vars[name] = value;
}
};
```
一个Environment对象有一个parent属性指向父作用域。在全局作用域下parent为null。它有一个vars属性保存绑定的变量。
有以下方法:
- extend() - 创建一个子作用域。
- lookup(name) - 查找具有给定名称的变量的作用域。
- get(name) - 获取变量的当前值。如果变量未定义,则会抛出错误。
- set(name, value) - 设置变量的值。这需要查找变量定义的实际作用域。如果找不到并且我们不在全局范围内,则抛出错误。
- def(name, value) - 这会在当前范围内创建(或覆盖)一个变量。
**evaluate函数**
有了执行环境,我们就可以回到我们的主要问题(编写Interpreter)中来,这个evaluate函数包含了一个大switch循环,按节点类型执行不同的逻辑,下面是每个节点的说明:
```
function evaluate(exp, env) {
switch (exp.type) {
```
常量节点,我们只返回它们的值:
```
case "num":
case "str":
case "bool":
return exp.value;
```
变量是从环境中提取的。请记住,”var” token,其value属性包含变量名称名称:
```
case "var":
return env.get(exp.value);
```
对于赋值,我们需要检查左侧是否是一个 “var” token(如果不是,则抛出一个错误;现在我们不支持赋值给其他任何东西)。然后我们使用env.set设置值。
请注意,该值需要首先通过evaluate递归调用来计算。
```
case "assign":
if (exp.left.type != "var")
throw new Error("Cannot assign to " + JSON.stringify(exp.left));
return env.set(exp.left.value, evaluate(exp.right, env));
```
一个”binary”节点需要用到两个操作数。我们稍后会写这个apply_op函数,这很简单。同样,我们需要递归调用evaluator来计算left和right操作数
```
case "binary":
return apply_op(exp.operator,
evaluate(exp.left, env),
evaluate(exp.right, env));
```
一个”lambda”节点实际上会产生一个JavaScript闭包,所以它就像普通函数一样可以从JavaScript中调用。我写了一个make_lambda函数,将在后面定义它:
```
case "lambda":
return make_lambda(env, exp);
```
评估if节点很简单:首先评估if条件。如果不成立,则评估then分支并返回其值。最后如果存在else分支,就评估,否则返回false.
```
case "if":
var cond = evaluate(exp.cond, env);
if (cond !== false) return evaluate(exp.then, env);
return exp.else ? evaluate(exp.else, env) : false;
```
“prog”节点是一系列表达式。我们只是按顺序评估它们并返回最后一个的值。对于一个空序列,返回值默认为false。
```
case "prog":
var val = false;
exp.prog.forEach(function(exp){ val = evaluate(exp, env) });
return val;
```
对于一个”call”节点,我们需要调用evaluate计算func函数。首先我们评估一下,它应该返回一个正常的JS函数,然后我们评估并应用该函数。
```
case "call":
var func = evaluate(exp.func, env);
return func.apply(null, exp.args.map(function(arg){
return evaluate(arg, env);
}));
```
我们的程序不应该到下面这一步,但是为了防止在解析器中添加新的节点类型,并且忘记更新评估程序,让我们抛出一个明确的错误。
```
default:
throw new Error("I don't know how to evaluate " + exp.type);
}
}
```
这是evaluate函数的核心,你可以看到它非常简单。最后,我们还需要编写两个函数,先从最简单的apply_op函数开始:
```
function apply_op(op, a, b) {
function num(x) {
if (typeof x != "number")
throw new Error("Expected number but got " + x);
return x;
}
function div(x) {
if (num(x) == 0)
throw new Error("Divide by zero");
return x;
}
switch (op) {
case "+" : return num(a) + num(b);
case "-" : return num(a) - num(b);
case "*" : return num(a) * num(b);
case "/" : return num(a) / div(b);
case "%" : return num(a) % div(b);
case "&&" : return a !== false && b;
case "||" : return a !== false ? a : b;
case "<" : return num(a) < num(b);
case ">" : return num(a) > num(b);
case "<=" : return num(a) <= num(b);
case ">=" : return num(a) >= num(b);
case "==" : return a === b;
case "!=" : return a !== b;
}
throw new Error("Can't apply operator " + op);
}
```
它接收运算符和其中需要运算的2个数,然后执行并返回而已。与JavaScript不同,我们只要求运算符的操作数为数字,并且使用num和div函数来检查。对于字符串我们会定义其他的东西。
而make_lambda则有点微妙:
```
function make_lambda(env, exp) {
function lambda() {
var names = exp.vars;
var scope = env.extend();
for (var i = 0; i < names.length; ++i)
scope.def(names[i], i < arguments.length ? arguments[i] : false);
return evaluate(exp.body, scope);
}
return lambda;
}
```
正如你所看到的,它返回一个简单的JavaScript函数,包含环境和表达式进行执行。
重点是,创建这个函数时什么都不会发生,但是当它被调用时,它会创建时的环境所使用的参数/值(如果传递的值少于函数的参数列表,缺少的部分默认为false)。 最后evaluate其函数体。
**原始功能**
如你所见,我们的语言没有提供任何方式与外界进行交流。
在一些代码示例中,我使用了一些print和println函数,但它们没有在任何地方定义。
这些必须被定义为原始函数(也就是说,我们将用JavaScript编写它们并将它们插入到全局环境中)。
现在把它放在一起,这里有一个测试程序:
```
// some test code here
var code = "sum = lambda(x, y) x + y; print(sum(2, 3));";
// remember, parse takes a TokenStream which takes an InputStream
var ast = parse(TokenStream(InputStream(code)));
// create the global environment
var globalEnv = new Environment();
// define the "print" primitive function
globalEnv.def("print", function(txt){
console.log(txt);
});
// run the evaluator
evaluate(ast, globalEnv); // will print 5
```
您可以 [下载](http://annn.me/assets/download/lambda-eval1.js) 我们迄今为止编写的代码。它可以在NodeJS中运行——,例如:
```
echo 'sum = lambda(x, y) x + y; println(sum(2, 3));' | node lambda-eval1.js
```
- Web 开发笔记
- 从输入 URL 到页面加载完成的过程中都发生了什么事情?
- 从浏览器接收url到开启网络请求线程
- 开启网络线程到发出一个完整的http请求
- 从服务器接收到请求到对应后台接收到请求
- 后台和前台的http交互
- http的缓存
- 解析页面流程
- HTML解析,构建DOM
- CSS解析,构建CSSOM
- 资源外链的下载
- CSS的可视化格式模型
- 包含块(Containing Block)
- 控制框(Controlling Box)
- BFC(Block Formatting Context)
- IFC(Inline Formatting Context)
- 其它
- JS引擎解析过程
- JS的解释阶段
- JS的预处理阶段
- JS的执行阶段
- 回收机制
- 参考资料
- JavaScript模块化编程
- AMD
- requireJS
- CommonJS
- UMD
- ES6模块
- 参考资料
- 使用 JavaScript 实现一门编程语言
- 如何使用 JavaScript 实现一门编程语言(1) —— 前言
- 如何使用 JavaScript 实现一门编程语言(2) —— 编写一个解析器
- 如何使用 JavaScript 实现一门编程语言(3) —— Input stream
- 如何使用 JavaScript 实现一门编程语言(4) —— Token stream
- 如何使用 JavaScript 实现一门编程语言(5) —— AST
- 如何使用 JavaScript 实现一门编程语言(6) —— Interpreter
- 完整代码
- 参考资料
- 前端布局概论
- 参考资料
- Windows 笔记
- 错误解决
- win10应用商店无法登录提示0x80070426解决方法
- 使用技巧
- 设置 Hyper-V 和 VMware 共存
- Powershell
- WSL