前言
昨天完成了codewars上的1级题简单解释器实现,今天突发奇想上去看看总共有多少1级题,然后发现总共也只有三题。而且,这三题都是编译器解释器相关的,所以干脆都做了了事。
昨天做的是简单解释器,还有两题分别是编译器以及一个以类型为重点的不完整的类lisp解释器。其中编译器这题和之前做的解释器很像,所以就从编译器开始吧:
题目地址:http://www.codewars.com/kata/tiny-three-pass-compiler/train/javascript
github地址:https://github.com/woodensail/SimpleInteractiveInterpreter/blob/master/tiny-three-pass-compiler.js
前文地址:http://segmentfault.com/a/1190000004047915
本文地址:http://segmentfault.com/a/1190000004049048
与前文中解释器的差别
首先这题的复杂度比之前要低的多,所以几十分钟就完成了。之前题目中的语言还算是结构完整,而这题里的输入都不能算是一个语言,只能说是带参数的表达式而已。
没有参数,没有全局变量。相比算术表达式只多了参数而已。也因此,语法树生成过程异常简单,基本是和波兰表达式生成没区别了。
这题比之前多出的部分则是语义分析和汇编代码生成。
语义分析部分需要将常量运算优化掉,缩短代码长度。
汇编代码生成部分取代了前一题的执行部分。而是生成并返回汇编代码即可。
Pass1
这个没啥好讲的了,就是波兰表达式的生成略改而已,改动部分包括多了值栈和参数列表。
另外就是对参数和立即量做了区分,这一点做的比前一篇要好。前一篇里面参数与立即量部分不分家带来了不少麻烦。
Compiler.prototype.pass1 = function (program) {
var tokens = this.tokenize(program), index = tokens.indexOf(']'), args = {}, next, dataStack = [];
operatorStack = [];
for (var i = 1; i < index; i++) {
args[tokens[i]] = i - 1;
}
tokens = tokens.slice(index + 1);
tokens.unshift('(');
tokens.push(')');
while ((next = tokens.pop()) !== void 0) {
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]) {
operatorStack.push(next);
break;
} else {
dataStack.push({op: operatorStack.pop(), a: dataStack.pop(), b: dataStack.pop()});
}
}
} else if (next === '(') {
while ((next = operatorStack.pop()) !== ')') {
if (next === void 0) {
break
}
dataStack.push({op: next, a: dataStack.pop(), b: dataStack.pop()});
}
} else if (next === ')') {
operatorStack.push(next);
} else {
if (args[next] !== void 0) {
dataStack.push({op: 'arg', n: args[next]});
} else {
dataStack.push({op: 'imm', n: Number(next)});
}
}
}
return dataStack[0];
};
Pass2
pass2的目的是把立即量运算优化掉。实现方式是递归扫描。
如果当前节点是参数或立即量则直接返回当前节点。
否则依次对当前节点的两个参数调用pass2。这步过后,a和b应该都是参数或立即量。
如果a和b都是立即量,那么直接计算当前节点的结果。然后用计算出的结果构建一个新的立即量最后返回。
反之则直接返回当前节点。
Compiler.prototype.pass2 = function (ast) {
if ((ast.op === 'arg') || (ast.op === 'imm')) {
return ast;
}
ast.a = this.pass2(ast.a);
ast.b = this.pass2(ast.b);
if ((ast.a.op === 'imm') && (ast.b.op === 'imm')) {
return {op: 'imm', n: this.execOp(ast.op, ast.a.n, ast.b.n)}
} else {
return ast;
}
};
Pass3
首先所有操作都是以'PU'压栈结束的。
其中立即量和参数这俩个分别是将数字和参数放入寄存器后压栈。
其他的操作则是首先分别执行ab子节点。执行完毕后栈顶的一二元素分别是b,a个操作的结果。通过'PO','SW','PO'取出后执行算术操作,最后压栈就完成了。
需要注意的是这种方式生成的汇编代码有大量冗余,主要是无用的['PU', 'PO']以及['PU', 'IM/AR', 'PU', 'PO' "SW", "PO"]。
前者可以完全删去,后者可以优化为["SW" , 'IM/AR', "SW"]。
Compiler.prototype.pass3 = function (ast) {
switch (ast.op) {
case 'imm':
return ["IM " + ast.n, "PU"];
case 'arg':
return ["AR " + ast.n, "PU"];
case '+':
return this.pass3(ast.a).concat(this.pass3(ast.b)).concat(["PO", "SW", "PO", "AD", "PU"]);
case '-':
return this.pass3(ast.a).concat(this.pass3(ast.b)).concat(["PO", "SW", "PO", "SU", "PU"]);
case '*':
return this.pass3(ast.a).concat(this.pass3(ast.b)).concat(["PO", "SW", "PO", "MU", "PU"]);
case '/':
return this.pass3(ast.a).concat(this.pass3(ast.b)).concat(["PO", "SW", "PO", "DI", "PU"]);
}
};
总结
这题真是相当简单,我待会儿去看看最后一题去,那题似乎和前两题不太一样。