Skip to main content

简易编译器实现

前言

该编译器实现的功能是将Lisp语言中的函数调用语法编译为c语言中的函数调用语法。本人对 JavaScript 语言更熟悉,所以代码使用 JavaScript 实现。

下面是我的编译器要做的事情:

image-20211231203159743

要求输入任意一段 Lisp 语言中的函数调用语法文本,最终输出 c 语言的函数调用语法。

本编译器主要包含三个阶段:

  • 解析
  • 转换
  • 代码生成

下面会依次说明我对每个过程理解和最终代码的实现。

解析阶段

在解析阶段又分为两个阶段:词法分析和语法分析。

词法分析

将源代码通过 tokenizer 分解为 tokens,这里的 tokens 是一个数组,数组的每一项称为一个 token,它的结构如下:

const token = {type: "paren", value: "("};

type 属性是用来描述源代码中的每一个字符类型,value 属性就是该字符的取值。

对于下面这个 Lisp 语句:

(add 2 (subtract 4 2))

它经过词法分析后,tokens 的结构如下:

pic.sm

语法分析

在语法分析阶段,利用词法分析得到的 tokens 数组,将其转换为 AST 抽象语法树,这个抽象语法树作用就是将之前 tokens 数组中毫无联系的 token,联系起来,抽象的描述了整个语句的语法和作用。

上面的 tokens 经过语法分析后会得到如下结构的抽象语法树:

pic.sm

转换阶段

在该阶段要做的事情就是操作来自解析阶段得到的 AST 抽象语法树,这一步我认为是编译过程中的核心阶段,你可以在该阶段做任何事情。比如增、删、替换 AST 中的某些节点,或者根据原来的 AST 生成一颗新的 AST。

对于 JS 来说,由于某些浏览器的兼容性,你会利用一些插件将 ES6+ 的语句转换为 ES5 的语句,就需要将得到的抽象语法树中表示 ES6+ 语法的那部分节点转换为符合 ES5 语法的节点。 而本编译器的目的是将 Lisp 函数调用的语法转换为 C 语言的函数调用的语法,因此,我们需要根据原来的 AST 抽象语法树来创建一颗新的符合 C 语言的抽象语法树。

想要转换抽象语法树,就需要知道如何遍历抽象语法树,这里使用深度优先遍历下面抽象语法树。

pic.sm

  1. 到达 Program 节点,这是抽象语法树的根节点
  2. 到达 CallExpression (add) 节点,这是根节点的左孩子节点
  3. 到达 NumberLiteral (2) 节点,这是 CallExpression (add) 的第一个参数
  4. 到达 CallExpress (subtract) 节点,这是根节点的右孩子节点
  5. 到达 NumberLiteral (4) 节点,这是 CallExpress (subtract) 的第一个参数
  6. 到达 NumberLiteral (2) 节点,这是 CallExpress (subtract) 的第二个参数

代码生成阶段

该阶段的作用是将新的 AST 转换为目标源码。

代码实现

tokenizer

该函数的作用是根据输入的源码文本生成 tokens

// 接收输入的字符串源码
function tokenizer(input) {

// 用于表示当前扫描到 input 的哪个下标了
let current = 0;

// 存放 token 的数组
let tokens = [];

// 使用 current 扫描 input
while (current < input.length) {

// 当前扫描到字符
let char = input[current];

// 判断当前字符是否为圆括号
if (char === '(') {

// 将该字符描述成一个 token,类型为 paren 表示为圆括号类型,取值为 (
tokens.push({
type: 'paren',
value: '(',
});

// current 指针后移
current++;

// 进入下一次循环
continue;
}

// 同理,这里用来检查当前扫描到的字符是否为闭合的圆括号
if (char === ')') {
tokens.push({
type: 'paren',
value: ')',
});
current++;
continue;
}

// 这里用来跳过源代码文本中的空格
let WHITESPACE = /\s/;
if (WHITESPACE.test(char)) {
current++;
continue;
}

// 当扫描到一个数字时,这里要将一个完整的数当作一个 token
// 比如 add 100 200,这里的 100 和 200 当作两个 token
// 所以内部要使用一个循环
let NUMBERS = /[0-9]/;
if (NUMBERS.test(char)) {

// 记录完整的数
let value = '';
// 只要当前扫描到的是数字,那么 current 就后移
while (NUMBERS.test(char)) {
value += char;
char = input[++current];
}

// 最后将完整的数字 token 存放到 tokens 中,类型为 number,取值为 value
tokens.push({ type: 'number', value });

// 进入下一次循环
continue;
}

// 如果扫描到了引号,说明遇到字符串了,我们要将完整的字符串当作一个 token
// 因此内部也要使用一个循环
if (char === '"') {
// 记录完整字符串的值
let value = '';

// 跳过遇到的第一个引号
char = input[++current];

// 记录两个引号之间的字符串值
while (char !== '"') {
value += char;
char = input[++current];
}

// 跳过遇到的第二个引号
char = input[++current];

// 将完整的字符串当作 token 存入 tokens 中,类型为 string
tokens.push({ type: 'string', value });

continue;
}

// 用于识别完整的单词,即函数标识符
let LETTERS = /[a-z]/i;
if (LETTERS.test(char)) {
let value = '';

while (LETTERS.test(char)) {
value += char;
char = input[++current];
}

// 将完整的标识符名称当作 token 存放到 tokens 中,类型为 name
tokens.push({ type: 'name', value });

continue;
}

// 这里用来识别输入到未知字符,抛出异常
throw new TypeError('I dont know what this character is: ' + char);
}

// 当整个输入的源码文本扫描完毕后,返回 tokens 数组
return tokens;
}

parser

该函数的作用是根据 tokens 生成抽象语法树 AST

// 参数为 tokens
function parser(tokens) {

// 使用 current 指针来扫描数组
let current = 0;

// 由于要构建树结构,所以这里使用递归
function walk() {

// 从 tokens 中取出一个 token
let token = tokens[current];

// 如果 token 的类型是 number
if (token.type === 'number') {

// 扫描数组的指针后移
current++;

// 返回 Numberliteral 类型的 AST 节点
return {
type: 'NumberLiteral',
value: token.value,
};
}

// 同理,如果 token 是 string 类型,则返回 StringLiteral 类型的 AST 节点
if (token.type === 'string') {
current++;
return {
type: 'StringLiteral',
value: token.value,
};
}

// 如果 token 是一个左圆括号
if (
token.type === 'paren' &&
token.value === '('
) {

// 跳过该 token,在 Lisp 语法中,左圆括号就是表示有函数调用(本编辑器只实现函数调用语法的编译)
// 所以左圆括号的下一个 token 就是该函数的标识符
token = tokens[++current];

// 创建一个 CallExpression 类型的节点,name 表示标识符即为函数名称,params
let node = {
type: 'CallExpression',
name: token.value,
params: [],
};

// 取到下一个 token,此时该 token 可能是一个 number 类型的 token,也可能是一个左括号
token = tokens[++current];

// 接下来开始遍历后面的 token,直到 current 遇到右括号,目的是获取函数调用时所有的参数
// 但是很可能出现一种情况就是参数也可能是一个函数调用表达式,这时就要使用递归了
//
// 比如下面这个 Lisp 语法的函数调用
// (add 2 (subtract 4 2))
//
// 当我们从左括号开始遍历 tokens 时,后续会遇到两个右括号
//
// [
// { type: 'paren', value: '(' },
// { type: 'name', value: 'add' },
// { type: 'number', value: '2' },
// { type: 'paren', value: '(' },
// { type: 'name', value: 'subtract' },
// { type: 'number', value: '4' },
// { type: 'number', value: '2' },
// { type: 'paren', value: ')' }, <<< 右括号
// { type: 'paren', value: ')' }, <<< 右括号
// ]
//
// 现在开始递归调用 walk 函数,来递增 current

// 使用 while 循环递归,直到 token 是一个右括号

// 如果当前 token 的类型不是一个括号,或者是括号,但不是右括号
//
while (
(token.type !== 'paren') ||
(token.type === 'paren' && token.value !== ')')
) {
// 递归调用 walk 函数,它的返回值是一个 AST 节点,作为 CallExpression node 节点的参数
node.params.push(walk());
// 更新 token
token = tokens[current];
}

// 递增 current,跳过右括号
current++;

// 返回该节点
return node;
}

// 执行到此行代码,说明扫描到了无法识别的 token,直接抛出异常
throw new TypeError(token.type);
}

// 初始化 AST,默认根节点类型为 Program
let ast = {
type: 'Program',
body: [],
};

// 下面就可以来调用 walk 函数生成 AST 了,这里使用 while 循环的原因是,本编译器支持多个函数平行调用
// (add 2 2)
// (subtract 4 2)
//
while (current < tokens.length) {
ast.body.push(walk());
}

// 最后返回 AST
return ast;
}

traverser

该函数用于遍历 AST

/**
* 这里使用了一个 visitor 对象用来访问 AST 中不同的节点
* 当遇到一个类型匹配上的节点后,能够调用 visitor 上面的一些方法
*
* traverse(ast, {
* Program: {
* enter(node, parent) {
* // ...
* },
* exit(node, parent) {
* // ...
* },
* },
*
* CallExpression: {
* enter(node, parent) {
* // ...
* },
* exit(node, parent) {
* // ...
* },
* },
*
* NumberLiteral: {
* enter(node, parent) {
* // ...
* },
* exit(node, parent) {
* // ...
* },
* },
* });
*/

// 接收 ast 和 visitor
function traverser(ast, visitor) {

// 该函数用来遍历一个数组,遍历过程中会调用 traverseNode 函数
// 因为 AST 中某些节点的属性是数组类型的,比如根节点 body 属性,函数调用式中的 params 属性
function traverseArray(array, parent) {
array.forEach(child => {
traverseNode(child, parent);
});
}

// 该函数可以接收一个节点和其父节点,这两个参数可以传递给 visitor 内部的方法
function traverseNode(node, parent) {

// 获取 visitor 内部对应于 node.type 的方法
let methods = visitor[node.type];

// 如果该方法存在,并且具有 enter 属性,我们就调用 enter,传入 node 和 parent
if (methods && methods.enter) {
methods.enter(node, parent);
}

// 根据当前节点类型传入不同的参数进行遍历
switch (node.type) {

// 当前节点是根节点,则遍历 node.body 数组
case 'Program':
traverseArray(node.body, node);
break;

// 当前节点是函数调用式,则遍历 node.params 数组
case 'CallExpression':
traverseArray(node.params, node);
break;

// 如果节点是数字或者字符串类型,那么不用递归遍历
case 'NumberLiteral':
case 'StringLiteral':
break;

// 无法识别节点类型,抛出异常
default:
throw new TypeError(node.type);
}

// 如果该方法存在,并且具有 exit 属性,我们就调用 exit,传入 node 和 parent
if (methods && methods.exit) {
methods.exit(node, parent);
}
}

// 最后传入 AST 根节点和 null,因为根节点是没有父节点的,所以为 null
traverseNode(ast, null);
}

transformer

该函数用于转换 AST

/**
* 将之前构造的 ast 传入到 traverser 函数中,利用 visitor 在遍历 ast 的过程中创建一个新的 ast
*
* ----------------------------------------------------------------------------
* 原始 AST | 转换后的 AST
* ----------------------------------------------------------------------------
* { | {
* type: 'Program', | type: 'Program',
* body: [{ | body: [{
* type: 'CallExpression', | type: 'ExpressionStatement',
* name: 'add', | expression: {
* params: [{ | type: 'CallExpression',
* type: 'NumberLiteral', | callee: {
* value: '2' | type: 'Identifier',
* }, { | name: 'add'
* type: 'CallExpression', | },
* name: 'subtract', | arguments: [{
* params: [{ | type: 'NumberLiteral',
* type: 'NumberLiteral', | value: '2'
* value: '4' | }, {
* }, { | type: 'CallExpression',
* type: 'NumberLiteral', | callee: {
* value: '2' | type: 'Identifier',
* }] | name: 'subtract'
* }] | },
* }] | arguments: [{
* } | type: 'NumberLiteral',
* | value: '4'
* ---------------------------------- | }, {
* | type: 'NumberLiteral',
* | value: '2'
* | }]
* | }
* | }
* | }]
* | }
* ----------------------------------------------------------------------------
*/


function transformer(ast) {

// 初始化新的 ast
let newAst = {
type: 'Program',
body: [],
};

// 给旧的 ast 绑定一个 _context 属性指向新的 ast 的 body
ast._context = newAst.body;

// 调用 traverser,传入旧的 ast
traverser(ast, {

// 遍历过程中遇到 NumberLiteral 类型节点,我们就对应的放入一个新节点到到新的 ast 中
NumberLiteral: {

enter(node, parent) {
parent._context.push({
type: 'NumberLiteral',
value: node.value,
});
},
},

// 同理,遇到 StringLiteral 类型节点,也进行相同操作
StringLiteral: {
enter(node, parent) {
parent._context.push({
type: 'StringLiteral',
value: node.value,
});
},
},

// 接下来,如果遇到函数调用式
CallExpression: {
enter(node, parent) {

// 这里就修改为符合 C 语言语法的函数调用式
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);
},
}
});

// 最后返回这个新的 ast
return newAst;
}

codeGenerator

该函数的作用是将传入的 ast 转换为源代码字符串

// 由于是树结构,因此会递归调用该函数
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;

// 字符串节点
case 'StringLiteral':
return '"' + node.value + '"';

// 无法识别的节点则抛出异常
default:
throw new TypeError(node.type);
}
}

compiler

组合编译器的各个阶段功能函数

function compiler(input) {
let tokens = tokenizer(input);
let ast = parser(tokens);
let newAst = transformer(ast);
let output = codeGenerator(newAst);

return output;
}