前端编译原理 the-super-tiny-compiler

时隔四个月的更新

其实后面的递归步骤只是借助 JS 的引用数据类型特性，模拟了类似 c 语言的指针功能 😐 , 就是简单的传递指针，不断生成类似链表的数据结构，填充数据，最终得到目标代码... 而已.. 已.

起初

the-super-tiny-compiler 是 github 上的一个使用 js 编写的编译器，代码注释中称其可能是最小的编译器，可以将 lisp 风格的语言编写为 c 风格的语言

这个编译器项目可以说是麻雀虽小，五脏俱全

但是本人再阅读器源代码的时候，在生成新 ast 的过程中，对其递归过程产生了不解

所以从现在开始，要对源代码进行一下分析

编译器原理简单来说就是
词法分析
语法分析（生成 ast）
将 oldAst -> newAst
最后将产生的 newAst 生成目标语言语法进行输出
将 (add 2 (subtract 4 2)) 作为输入，得到的 ast 结构如下

Copy
const ast = {
type: "Program",
body: [
{
type: "CallExpression",
name: "add",
params: [
{
type: "NumberLiteral",
value: "2",
},
{
type: "CallExpression",
name: "subtract",
params: [
{
type: "NumberLiteral",
value: "4",
},
{
type: "NumberLiteral",
value: "2",
},
],
},
],
},
],
};

得到这个结构后，执行了这样一个函数

Copy

function transformer(ast) {
let newAst = {
type: "Program",
body: []
}

ast._context = newAst.body
}

给 ast 对象下添加了一个新属性，将该属性指向了 newAst 的 body 属性中
然后向下执行了

Copy
function transformer(ast) {
let newAst = {
type: "Program",
body: []
}

// 这里改变 ast 的属性就可以之间影响到 newAst
ast._context = newAst.body

traverser(ast, {
// 处理数字
NumberLiteral: {
enter(node, parent) {
parent._context.push({
type: 'NumberLiteral',
value: node.value
})
}
},
// 字符串
StringLiteral: {
enter(node, parent) {
parent._context.push({
type: 'StringLiteral',
value: node.value,
});
},
},

CallExpression: {
enter(node, parent) {
let expression = {
type: 'CallExpression',
callee: {
type: 'Identifier',
name: node.name
},
arguments: []
}

node._context = expression.arguments;

if (parent.type !== "CallExpression") {
expression = {
type: 'ExpressionStatement',//表达式语句
expression
}
}

parent._context.push(expression)
}
}

})

return newAst//返回 newAst
}

可以看到，newAst 的数据变化只执行了一个 traverser 函数就完成了，函数把刚刚的 ast 当作参数，以及根据不同类型对 newAst 中的 body 复制的行为，

这个函数的内部是这样子的

Copy
function traverser(ast, visitor) {

function traverseArray(array, parent) {

}

function traverseNode(node, parent) {
// 判断传入进来的 node 有没有对应的属性
let methods = visitor[node.type]

// 如果有 就给其父节点的 body 赋值进去
if (methods && methods.enter) {
methods.enter(node, parent)
}

// 然后再把 visiter 中不包括的属性进行单独处理
switch (node.type) {
// first exec 执行最外层的 遍历 节点下的 body
case "Program":
traverseArray(node.body, node);
break;

case "CallExpression":
traverseArray(node.params, node)
break;

case "NumberLiteral":
case "StringLiteral":
break;

default:
throw new TypeError(node.type)
}
}

traverseNode(ast, null)

}

执行他的时候，主线为 traverser -> traverseNode ->
ast 作为 node 参数传入进去，这个函数的执行过程就进行了递归调用
第一次执行 methods 为 undefined 然后进入 switch 语句中
第一次的 ast type 是 Program 然后就将 ast 的 body 当作参数传递进去，然后 break; 掉
至此，函数主线已经执行完毕了
之后的执行流程则是对 ast.body 进行的一个伪递归或者叫嵌套调用，函数每次根据其传入的 tree 参数，根据表达式，参数，去判断是否生成新的 ast

值得一提的是，在生成 newAst 的时候
有类似这样的语句
node._context = expression.arguments;
将传入节点的_context 属性指向当前对象下的某个属性，达到引用的效果，这时，使用 visitor 遍历语法数的时候，不管传入的对象是什么，因为已经在上层构建好了内存地址的引用关系，所以只需要给 parent._context 属性添加只就可以了

最后的最后就是将生成的新 ast 生成我们的目标语言，这个没什么好说的，递归生成字符串就好了。

总结，看似简单的小项目，如果自己去实现，不知到要考虑多少细节，所以说，前端深入之路，任重而道远！