JavaScript高手进阶:把Lisp编译为JavaScript
简介
本文将用200行代码,实现一个微型编译器,其功能为:把Lisp代码编译为JavaScript代码。通过一个简单的实例,展示编译器的实现原理。
本文由JShaman.com原创,转载请保留此信息。
JShaman,专注于JavaScript源代码安全,是国内专业的JS代码保护服务商。
正文
Lisp是一种古老、简洁的语言,其语法格式如:(add 2 3)。本文我们要做的,是将这种语法代码编译为JavaScript代码。
其过程,主要包含以下几步:
第一步:词法分析、语法分析,将Lisp语法生成为tokens,即分词结果。
第二步:从Tokens生成AST,即生成Lisp语言的抽象语法树。
第三步:将Lisp的AST转化为JavaScript的AST。
第四步:基于JavaScript的AST,生成JavaScript代码。
以下,用代码详解各操作过程。
例程中,使用一行Lisp代码:(add 2 3)。
对其进行分词,代码及说明如下图:
分词结果:
美化一下,方便查看:
可以看到,分别提取出了括号、数值。每一项目都对应了type、value。这个结果,通常被称为tokens,下文中也使用这个称谓。
接下来处理tokens,把它转为ast:
运行结果:
可以看到此时生成的AST中,已经有了CallExpression、Program等结构,如下图:
但它只是Lisp的AST,与JavaScript的AST是有差异的。JavaScript中,Add(2,3)这句代码的AST应该是如下形态:
那么,这时就需要对生成出来的AST进行修改了,目标:改为JavaScript格式的AST。
这部分操作较为复杂,传入AST为参数,起始处理Program节点,再处理它的数组body节点,再对此body节点分析,来到CallExpression后,调用第二参数中事先准备好的Enter函数,在Enter函数中,构建符合JavaScript规范的AST。以此类推,一层层处理次级节点。详情如下图所示:
需要注意的是,在AST中新增了一个成员:_context,并把它把与新AST的Body相关联。这样,对_context的赋值,即等于对新AST中Body成员的操作。
其效果,用一个小例子展示,如下:
var ast={};
var new_ast={type:'ooops',body:[]};
ast._context=new_ast.body;
ast._context.push("js加密,就用:jshaman.com");
console.log(new_ast)
上面这部分AST转化代码,参考的是super-tiny-compiler,阅读其代码时,感觉它把简单问题搞复杂了:只是遍历JSON数据,并新建一个JSON变量,却使用了复杂的多个函数交叉嵌套调用。
下面,给出一个简单的方法,使用一个函数中完成相同功能:
最后一步,当JavaScript规范的AST构建好后,即可依此生成JavaScript代码。
这个功能,相对简单,代码如下所示:
最后,附上完整代码,保存为JS即可在NodeJS环境中使用,也可直接在浏览器中运行。
function tokenizer(input) {
let current = 0;
let tokens = [];
while (current < input.length) {
let char = input[current];
if (char === '(') {
tokens.push({type: 'paren',value: '('});
current++;
continue;
}
if (char === ')') {
tokens.push({type: 'paren',value: ')',});
current++;
continue;
}
let WHITESPACE = /\s/;
if (WHITESPACE.test(char)) {
current++;
continue;
}
let NUMBERS = /[0-9]/;
if (NUMBERS.test(char)) {
let value = '';
while (NUMBERS.test(char)) {
value += char;
char = input[++current];
}
tokens.push({ type: 'number', value });
continue;
}
let LETTERS = /[a-z]/i;
if (LETTERS.test(char)) {
let value = '';
while (LETTERS.test(char)) {
value += char;
char = input[++current];
}
tokens.push({ type: 'name', value });
continue;
}
throw new TypeError('I dont know what this character is: ' + char);
}
console.log("\ntokens:",JSON.stringify(tokens));
return tokens;
}
function parser(tokens) {
let current = 0;
function walk() {
let token = tokens[current];
if (token.type === 'number') {
current++;
return {type: 'NumberLiteral',value: token.value};
}
if (token.type === 'paren' && token.value === '(') {
token = tokens[++current];
let node = {
type: 'CallExpression',
name: token.value,
params: [],
};
token = tokens[++current];
while (
(token.type !== 'paren') ||
(token.type === 'paren' && token.value !== ')')
) {
node.params.push(walk());
token = tokens[current];
}
current++;
return node;
}
throw new TypeError(token.type);
}
let ast = {
type: 'Program',
body: [],
};
while (current < tokens.length) {
ast.body.push(walk());
}
console.log("\nast:", JSON.stringify(ast));
return ast;
}
function traverser(ast, visitor) {
function traverseArray(array, parent) {
//array是上层传入的数组
array.forEach(function(child){
traverseNode(child, parent);
});
}
//2、node就是ast
function traverseNode(node, parent) {
let methods = visitor[node.type];
//本例中,第一次methods为undefined,不执行这里,因为第一次调用是traverseNode(ast)
if (methods && methods.enter) {
methods.enter(node, parent);
}
//这里第一次type是program,第二次是callExpression(add),然后是两次NumberLiteral(函数的两个参数:2、3)
switch (node.type) {
case 'Program':
//body":[{"type":"CallExpression","name":"add","params":[{"type":"NumberLiteral","value":"2"},{"type":"NumberLiteral","value":"3"}]}]
//遍历body数组,node传入后做为parent使用
//3、
traverseArray(node.body, node);
break;
case 'CallExpression':
//"params":[{"type":"NumberLiteral","value":"2"},{"type":"NumberLiteral","value":"3"}]
//3、
traverseArray(node.params, node);
break;
case 'NumberLiteral':
break;
default:
throw new TypeError(node.type);
}
}
//1
traverseNode(ast);
}
function transformer(ast) {
//新ast
let newAst = {
type: 'Program',
body: [],
};
//给ast加个新属性:_context
ast._context = newAst.body;
var visitor = {
NumberLiteral: {
enter(node, parent) {
parent._context.push({
type: 'NumberLiteral',
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: expression,
};
}
parent._context.push(expression);
},
}
}
//调用traverser,第一个参数是ast(含_context),第二个参数visitor是个对像,包含数字、函数的enter方法
traverser(ast, visitor);
console.log("\nnew ast:", JSON.stringify(newAst));
return newAst;
}
function codeGenerator(node) {
switch (node.type) {
case 'Program':
return node.body.map(codeGenerator)
.join('\n');
case 'ExpressionStatement':
return (
codeGenerator(node.expression) +
';'
);
case 'CallExpression':
return (
codeGenerator(node.callee) +
'(' +
node.arguments.map(codeGenerator)
.join(', ') +
')'
);
case 'Identifier':
return node.name;
case 'NumberLiteral':
return node.value;
default:
throw new TypeError(node.type);
}
}
var input = '(add 2 3)';
console.log("\nlisp code:",input);
let tokens = tokenizer(input);
let ast = parser(tokens);
//新ast
var new_ast = {
type: 'Program',
body: [],
};
console.log("new ast",JSON.stringify(new_ast))
let newAst = transformer(ast);
let output = codeGenerator(newAst);
console.log("\njavascript code:",output);
console.log();
优化后的JavaScript规范AST重建代码,放在上面代码之后,可直接运行:
t(ast,new_ast);
console.log(JSON.stringify(new_ast));
output = codeGenerator(newAst);
console.log("\njavascript code:",output);
console.log();
function t(ast, parent_ast){
//如果ast节点是数组
if(Array.isArray(ast) == true){
//获取每个成员,并递归调用
for(i=0; i<ast.length; i++){
t(ast[i],parent_ast);
}
}else{
for(var key in ast){
//JS代码AST的根节点
if(ast[key]=="Program"){
//console.log("Program")
t(ast.body, parent_ast);
}
//call调用
if(ast[key]=="CallExpression"){
//console.log("CallExpression")
//call的ast
var expression = {
type: 'CallExpression',
callee: {
type: 'Identifier',
name: ast.name,
},
arguments: [],
};
parent_ast.body.push(expression);
t(ast.params, parent_ast.body[parent_ast.body.length-1]['arguments']);
}
//数值
if(ast[key]=="NumberLiteral"){
//console.log("NumberLiteral",parent_ast);
parent_ast.push({
type: 'NumberLiteral',
value: ast.value,
});
}
}
}
}