热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

手把手教你完成一个简朴的编译器

手把手教你完成一个简朴的编译器1、概述本日我们将进修开辟一个编译器,然则呢,这个编译器并非说什么都能都编译,它只是一个超等小的编译器,重要用于申明编译器的一些基本的道理。我们这个编
手把手教你完成一个简朴的编译器

1、 概述

本日我们将进修开辟一个编译器,然则呢,这个编译器并非说什么都能都编译,它只是一个超等小的编译器,重要用于申明编译器的一些基本的道理。

《手把手教你完成一个简朴的编译器》

我们这个编译器可以将相似于lisp言语的函数挪用编译成相似于C言语的函数挪用。假如你对lisp言语和C言语这两者都不熟习,没紧要,什么言语实在无所谓,但接下来照样会给你一个疾速的引见。

假如我们有两个函数离别是add和subtract,假如用它们来盘算下面的表达式:

2 + 2
4 - 2
2 + (4 - 2)

那末在lisp言语中它可以长这模样:

(add 2 2) // 2 + 2
(subtract 4 2) // 4 - 2
(add 2 (subtract 4 2)) // 2 + (4 - 2)

而在C言语中它长这个模样:

add(2, 2)
subtract(4, 2)
add(2, subtract(4, 2))

相称简朴吧?

好吧,这是因为这仅仅只是我们这个编译器所须要处置惩罚的情况。 这既不是list言语的完整语法,也不是C言语的完整语法。 但这点语法已足以用来演示当代编译器所做的大部份事变。

大部份编译器所做的事变都可以剖析为三个重要的步鄹: 剖析、转换和代码天生。

  1. 剖析。 剖析就是将原始代码转换成代码的笼统示意。
  2. 转换。 转换就是以这个笼统示意为基本,做编译器想做的任何事变。
  3. 代码天生。 代码天生就是将转换后的笼统示意变成新的代码。

2、 剖析

剖析一般分为两个阶段:词法剖析句法剖析

  1. 词法剖析。 词法剖析一般是运用一个标记器(或词法剖析器)将原始代码拆分红叫做标记的东西。而标记是一些细小的对象构成的数组,它们一般用来形貌一些伶仃的语法片断,它们可以是数字、标签、标点符号、操纵符等等。
  2. 语法剖析。 语法剖析将词法剖析获得的标记从新花样化为用于形貌语法的每一个部份及其相互关联的示意。 这被称为中心示意笼统语法树(AST)。笼统语法树(简称AST)是一个深度嵌套的对象,用于以一种既好用又能供应很多信息的情势表式代码。

关于下面的语法:

(add 2 (subtract 4 2))

标记可以长下面这个模样:

[
{ 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: ')' },
]

然后它对应的笼统语法树(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',
}]
}]
}]
}

3、 转换

在剖析以后,编译器的下一步鄹是转换。 一样,这不过就是将末了一步的笼统语法树(AST)拿过来对它做肯定的转变。这类转变多种多样,可以是在同一种言语中举行转变,也可以直接将笼统语法树转换成别的一种完整差别的新言语。

让我们来看看我们将怎样转换一个笼统语法树(AST)。

你可以已注意到,我们的笼统语法树内里有一些异常相似的元素。 这些元素对象有一个type属性。 这每一个对象元素都被称为一个AST节点。 这些节点上定义的属性用于形貌AST树上的一个自力部份。

我们可以为数字字面量(NumberLiteral)竖立一个节点:

{
type: 'NumberLiteral',
value: '2',
}

或许是为挪用表达式(CallExpression)建立一个节点:

{
type: 'CallExpression',
name: 'subtract',
params: [...nested nodes go here...],
}

当转换AST树的时刻,我们可以须要对它举行add、remove、replace等操纵。 我们可以增添新节点,删除节点或许我们完整可以将AST树搁一边不睬,然后基于它建立一个全新的AST。

因为我们这个编译器的目的是将lisp言语转换成C言语,所以我们会聚焦建立一个特地用于目的言语(在这里是C言语)的全新AST。

3.1 遍历

为了阅读一切这些节点,我们须要可以遍历它们。 这个遍历历程是对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'
}]
}]
}]
}

所以关于上面的AST,我们须要像如许走:

  1. Program – 从AST树的顶层最先。
  2. CallExpression (add) – 挪动到Program的body属性的第一个元素。
  3. NumberLiteral (2) – 挪动到CallExpression(add)的第一个参数。
  4. CallExpression (subtract) – 挪动到CallExpression(add)的第二个参数。
  5. NumberLiteral (4) – 挪动到CallExpression(subtract)的第一个参数。
  6. NumberLiteral (2) – 挪动到CallExpression(subtract)的第二个参数。

假如我们直接操纵这个AST而不是建立一个零丁的AST,我们可以须要在这里引入种种笼统观点。 然则我们正在尝试做的事变,只须要接见树中的每一个节点就足够了。

运用“接见”这个词的缘由是因为这个词可以很好的表达怎样在对象构造上操纵元素。

3.2 接见者

这里最基本的思绪就是我们建立一个接见者对象,这个对象具有一些要领,这些要领可以吸收差别的节点范例。

比方下面如许:

var visitor = {
NumberLiteral() {},
CallExpression() {},
};

当我们遍历AST的时刻,一旦我们碰到一个与指定范例相婚配的节点,我们就会挪用接见者对象上的要领。

为了让这个函数比较好用,我们给它通报了该节点以及它的父节点:

var visitor = {
NumberLiteral(node, parent) {},
CallExpression(node, parent) {},
};

但是,这里也会有可以出如今退出时挪用东西。 设想一下我们前面提到的树构造:

- Program
- CallExpression
- NumberLiteral
- CallExpression
- NumberLiteral
- NumberLiteral

当我们往下遍历的时刻,我们会碰到终究的分支。 当我们接见完一切的分支后我们退出。 所以向下遍历树,我们进入节点,然后向上回溯的时刻我们退出节点。

-> Program (enter)
-> CallExpression (enter)
-> Number Literal (enter)
<- Number Literal (exit)
-> Call Expression (enter)
-> Number Literal (enter)
<- Number Literal (exit)
-> Number Literal (enter)
<- Number Literal (exit)
<- CallExpression (exit)
<- CallExpression (exit)
<- Program (exit)

为了支撑这类体式格局,我们的接见者对象须要改成下面这个模样:

var visitor = {
NumberLiteral: {
enter(node, parent) {},
exit(node, parent) {},
}
};

4、 代码天生

编译器的末了一步是代码天生。有时刻编译器在这一步会反复做一些转换步鄹做过的事变。 然则对代码天生而言,它所做的大部份事变就是将我们的AST树stringify一下输出,也就是转换成字符串输出。

代码天生有多种事变体式格局,有一些编译器会反复应用前面天生的标记,另一些编译器会建立代码的零丁示意,以便线性地打印节点,然则据我说知,大多数编译器的战略是运用我们方才建立的谁人AST,这是我们将要关注的。

实际上,我们的代码天生器将晓得怎样打印AST的一切差别节点范例,而且它将递归地挪用自身来打印嵌套节点,直到将一切内容打印成一长串代码。

而就是如许! 这就是编译器的一切差别部份。

如今不是说每一个编译器看起来都和我在这里形貌的完整一样。 编译器有很多差别的用处,他们可以须要比我细致的更多的步骤。

然则如今您应当对大多数编译器的表面有一个整体的高层次的观点。

如今我已诠释了一切这些,你应当可以写好自身的编译器了是吧?

只是在开顽笑的啦,我会在这里继承供应协助,所以我们最先吧!

5、编译器的代码完成

前面说了,全部编译器或许可以分为三步:剖析、转换、代码天生。而剖析又可以分红两步:词法剖析和句法剖析。所以一共须要四个函数就可以完成了。我们来离别看一下:

5.1、 剖析的完成

5.1.1、 词法剖析之tokenizer完成

我们将从编译器的第一步——剖析——最先,应用tokenizer函数举行词法剖析。

我们将把字符串代码拆分红由标记构成的数组:

(add 2 (subtract 4 2)) => [{ type: 'paren', value: '(' }, ...]

我们的tokenizer吸收一个代码字符串, 然后接下来做两个事变:

function tokenizer(input) {
// 一个current变量,相似于游标,用于跟踪我们在代码字符串中的位置
let current = 0;
// 以及一个tockens数组,用于将我们剖析的标记寄存个中
let tokens = [];
// 我们建立一个while轮回,在这内里我们设置我们的current变量,这个变量会跟着轮回的深切而不停增添
//
// 这么做是因为tockens可以会是恣意长度
while (current // 我们还会存储变量current所在位置的字符
let char = input[current];
// 我们起首要搜检的是左括弧,这个将会在稍后用于CallExpression,然则此时我们只体贴左括弧字符
//
// 我们搜检看看有无左括弧:
if (char === '(') {
// 假如有,则竖立一个对象,其type属性是paren,值为左括弧, 然后我们将这个对象到场tokens数组
tokens.push({
type: 'paren',
value: '(',
});
// 接着我们增添current变量,也就是挪动游标
current++;
// 然后举行下一轮轮回.
continue;
}
// 接着我们搜检右括弧,我们依据前面的套路来做:搜检右括弧,新增一个标记,增添current, 举行下一轮轮回
if (char === ')') {
tokens.push({
type: 'paren',
value: ')',
});
current++;
continue;
}
// 接着,我们搜检空缺格。 这很风趣,因为我们关注空缺格是因为它将字符串分离隔,然则我们并不须要将空缺格存为标记,我们
// 可以直接抛弃它,所以这里我们仅仅搜检空缺格是不是存在,假如它存在我们就进入下一轮轮回
let WHITESPACE = /\s/;
if (WHITESPACE.test(char)) {
current++;
continue;
}
// 下一个范例的标记是数字,这和我们前面见到的差别,因为一个数字多是恣意个字符构成,而且我们须要捕捉全部字符序列作为一个标记
//
// (add 123 456)
// ^^^ ^^^
// 比方上面的就只有两个自力的数字标记
//
// 所以当我们碰到序列中的第一个数字的时刻最先进一步处置惩罚.
let NUMBERS = /[0-9]/;
if (NUMBERS.test(char)) {
// 我们在这内里建立了一个value字符,用于拼接数字字符
let value = '';
// 接下来我们遍历背面的每一个字符直到碰到一个非数字字符,将这些字符和前面的value变量拼接起来, 而且转变current游标
while (NUMBERS.test(char)) {
value += char;
char = input[++current];
}
// 这以后我们将建立数字标记并到场tokens数组
tokens.push({ type: 'number', value });
// 然后我们继承
continue;
}
// 我们也支撑字符串,字符串就是用双引号(")包裹的一段文本,比方
//
// (concat "foo" "bar")
// ^^^ ^^^ 字符串标记
//
// 我们先搜检左双引号:
if (char === '"') {
// 建立一个value变量用于保留字符串.
let value = '';
// 我们将疏忽双引号,因为我们体贴的是双引号包裹的文本.
char = input[++current];
// 然后我们遍历背面的字符串,直到我们碰到右双引号
while (char !== '"') {
value += char;
char = input[++current];
}
// 疏忽右双引号,同理,因为我们体贴的是双引号包裹的文本.
char = input[++current];
// 建立范例为string的标记,并放进tockens数组
tokens.push({ type: 'string', value });
continue;
}
// 末了一种范例的标记是name标记,这是一串字符而不是数字,也就是lisp语法中的函数名
//
// (add 2 4)
// ^^^
// name 标记
//
let LETTERS = /[a-z]/i;
if (LETTERS.test(char)) {
let value = '';
// 同理,我们遍历,并将它们拼接起来
while (LETTERS.test(char)) {
value += char;
char = input[++current];
}
// 而且建立一个范例为name的标记,存储于tokens数组
tokens.push({ type: 'name', value });
continue;
}
// 末了,假如我们到这里还没有婚配一个字符, 我们将抛出一个毛病然后退出
throw new TypeError('I dont know what this character is: ' + char);
}
// 在tokenizer函数的末端我们将tokens数组返回
return tokens;
}

举个例子,关于(add 123 456)这段lisp言语代码,tokenizer化以后获得的效果以下:

《手把手教你完成一个简朴的编译器》

5.1.2、 句法剖析之parser完成

句法剖析的目的就是将tokens数组转换成AST。也就是下面的历程:

[{ type: 'paren', value: '(' }, ...] => { type: 'Program', body: [...] }

所以,我们定义一个parse函数,吸收我们的tokens数组作为参数:

function parser(tokens) {
// 一样我们保持一个current变量用作游标
let current = 0;
// 然则此次我们运用递归而不是while轮回,所以我们定义了walk函数
function walk() {
// 在walk函数内部,我们起首拿到tokens数组中current索引处寄存的标记
let token = tokens[current];
// 我们将把每种范例的标记以别的一种构造关联存储,以表现句法关联
// 起首从数字token最先
//
// 我们搜检看有无数字token
if (token.type === 'number') {
// 假如有,我们挪动游标
current++;
// 而且我们会返回一个叫做“NumberLiteral”的新的AST节点而且将它的value属性设置为我们标记对象的value属性
return {
type: 'NumberLiteral',
value: token.value,
};
}
// 假如我们有string范例的标记,我们会和数字范例相似,建立一个叫做“StringLiteral”的AST节点
if (token.type === 'string') {
//一样挪动游标
current++;
return {
type: 'StringLiteral',
value: token.value,
};
}
// 接下来我们查找CallExpressions. 我们是经由过程左括弧来最先这个历程的
if (
token.type === 'paren' &&
token.value === '('
) {
// 我们将疏忽左括弧,因为在AST内里,AST就是有句法关联的,所以我们不体贴左括弧自身了
token = tokens[++current];
// 我们建立一个叫做CallExpression的基本节点,而且将节点的名字设置为当前标记的value属性,
// 因为左括弧标记的下一个标记就是函数名字
let node = {
type: 'CallExpression',
name: token.value,
params: [],
};
// 我们挪动游标,疏忽掉name标记,因为函数名已存起在CallExpression中了
token = tokens[++current];
// 然后如今我们遍历每一个标记,找到CallExpression的参数直至碰到右括弧
//
// 如今,这里就是递归进场的处所了,为了防止堕入无穷的嵌套节点剖析,我们采纳递归的体式格局来搞定这个事变
//
// 为了更好的诠释这个东西,我们以我们的Lisp代码举例,你可以看到,add的参数是一个数字以及一个嵌套的CallExpression,
// 这个嵌套的函数挪用包含它自身的数字参数
//
// (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函数来增添我们的游标
// 所以我们建立一个while轮回,这个while轮回将一向举行直到碰到一个范例是paren的标记而且这个标记的值是一个右括弧
while (
(token.type !== 'paren') ||
(token.type === 'paren' && token.value !== ')')
) {
// 我们将挪用walk函数,这个函数将返回一个节点, 我们将把这个返回的节点放到当前节点的params
// 数组中存储起来,如许嵌套关联再AST内里就表现出来了
node.params.push(walk());
token = tokens[current];
}
// 末了,我们须要末了一次挪动游标用于疏忽右括弧
current++;
// 而且返回节点
return node;
}
// 一样,假如我们没有辨认出标记的范例,我们也会抛出一个毛病
throw new TypeError(token.type);
}
// 如今walk函数已定义好了, 我们须要定义我们的AST树了,这个AST树有一个“Program”根节点:
let ast = {
type: 'Program',
body: [],
};
// 然后我们要启动我们的walk函数, 将AST节点放入根节点的body数组内里
//
// 我们在轮回内里做这个是因为,我们可以会碰到连着的多个函数挪用,比方说像如许的:
//
// (add 2 2)
// (subtract 4 2)
//启动walk
while (current ast.body.push(walk());
}
// 在剖析函数的末了,我们将返回天生的AST.
return ast;
}

任然之前面的例子举例,我们剖析后获得的AST以下:

《手把手教你完成一个简朴的编译器》

5.2、 转换的完成

如今我们已有了我们的AST,我们想要一个接见者可以接见差别的节点,不管什么时候婚配到对应的节点范例的时刻,我们都可以挪用接见者上的要领。
所以我们定义一个旅行者函数,这个函数吸收两个参数,第一个参数为AST树,第二个参数是一个接见者。这个接见者须要完成差别范例的AST节点须要挪用的一些要领:

traverse(ast, {
Program: {
enter(node, parent) {
// ...
},
exit(node, parent) {
// ...
},
},
CallExpression: {
enter(node, parent) {
// ...
},
exit(node, parent) {
// ...
},
},
NumberLiteral: {
enter(node, parent) {
// ...
},
exit(node, parent) {
// ...
},
},
});

5.2.1 、traverser函数完成

因而,我们的旅行者函数的完成以下,它吸收AST和一个接见者作为参数,而且在内里还定义了两个要领:

function traverser(ast, visitor) {
// 定义一个traverseArray函数,可以是我们迭代一个数组,然后挪用我们稍后定义的traverseNode函数
function traverseArray(array, parent) {
array.forEach(child => {
traverseNode(child, parent);
});
}
// traverseNode函数吸收一个AST节点以及它的父节点,所以它也可以通报给我们的接见者函数
function traverseNode(node, parent) {
// 我们起首搜检接见者婚配范例的要领
let methods = visitor[node.type];
// 假如该AST节点范例存在enter要领,我们将以当前node及其父节点作为参数挪用该要领
if (methods && methods.enter) {
methods.enter(node, parent);
}
// 接下来我们会依据节点范例来把事变分别开来
switch (node.type) {
// 起首我们从顶级节点Program最先,因为该顶级节点有一个叫做body的属性,这个属性中是一个AST节点构成的数组
// 我们将挪用traverseArray函数来递归它
//
// (记着traverseArray函数会反过来挪用traverseNode函数,所以我们让这个AST被递归的接见)
case 'Program':
traverseArray(node.body, node);
break;
// 接下来我们对CallExpression节点做一样的事变,而且接见它们的参数
case 'CallExpression':
traverseArray(node.params, node);
break;
// 关于数字节点以及字符串节点,他们没有任何的子节点,所以我们直接break.
case 'NumberLiteral':
case 'StringLiteral':
break;
// 而且再一次,假如没有辨认出对应的节点范例,就抛出毛病
default:
throw new TypeError(node.type);
}
// 假如接见者上有exit要领,我们将以该节点和它的父节点作为参数挪用exit要领
if (methods && methods.exit) {
methods.exit(node, parent);
}
}
// 末了,我们启动traverser,这是经由过程挪用traverseNode完成的,而且traverseNode第二个参数是null,因为定级节点自身就没有父节点.
traverseNode(ast, null);
}

5.2.2 、transformer函数完成

前面我们已写好了traverser函数,而traverser函数对节点的重要操纵都是经由过程它的第二个参数,也就是接见者来完成的,在上面,我们并没有定义接见者的详细完成,只是定义了enter和exit两个接口,实际上这两个接口所做的事变就是转换步鄹真正干的事变。为此我们定义transformer函数。

transformer函数吸收AST,将它通报给traverser函数,而且transformer函数内部还为traverser函数供应接见者。终究transformer函数返回一个新建的AST。

比方之前面谁人例子为例,获得的AST和转换后的AST以下:

----------------------------------------------------------------------------
Original AST | Transformed 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'
| }]
| }
| }
| }]
| }
----------------------------------------------------------------------------

所以我们的transformer函数的详细完成以下:

function transformer(ast) {
// 我们将建立一个新的AST(即newAst),它和我们本来的AST相似,有一个Program根节点
let newAst = {
type: 'Program',
body: [],
};
// 接下来,我们会做一些取巧的操纵,我们在父节点上定义一个\_context属性,
// 我们会将节点放入父节点的\_context属性中
// 一般你会有更好的笼统(或许会庞杂些),然则在这里我们如许做使得事变变得相对简朴
//
// 你仅仅须要记着的是,context是一个从老AST到新AST的援用
ast._cOntext= newAst.body;
// 我们以老ast和一个接见者作为参数挪用traverser函数
traverser(ast, {
// 第一个接见者的属性是用来处置惩罚NumberLiteral的
NumberLiteral: {
// 在enter要领中会对节点举行接见.
enter(node, parent) {
// 在这内里我们会建立一个新的AST节点,这个节点任然以NumberLiteral定名
// 我们会将这个节点放入该节点父亲的\_context属性中
parent._context.push({
type: 'NumberLiteral',
value: node.value,
});
},
},
// 接下来是StringLiteral
StringLiteral: {
enter(node, parent) {
parent._context.push({
type: 'StringLiteral',
value: node.value,
});
},
},
// 接下来是CallExpression
CallExpression: {
enter(node, parent) {
// 我们建立一个新的节点CallExpression,它有一个嵌套的标识符
let expression = {
type: 'CallExpression',
callee: {
type: 'Identifier',
name: node.name,
},
arguments: [],
};
// 接下来,我们在原始的CallExpression节点上定义一个新的context用于援用
// expression变量上的arguments属性
// 如许我们可以到场参数
node._cOntext= expression.arguments;
// 接着我们搜检父节点是不是是一个CallExpression节点
// 假如不是
if (parent.type !== 'CallExpression') {
// 我们将用一个ExpressionStatement节点包裹这个CallExpression节点
// 这么做是因为顶级CallExpression节点实际上就是statement
// 也就是说,假如某个CallExpression节点的父节点不是CallExpression节点
// 那末这个CallExpression节点应当就是函数声明
expression = {
type: 'ExpressionStatement',
expression: expression,
};
}
// 末了我们将这个新的CallExpression(可以被ExpressionStatement包裹)
// 放入parent._context
parent._context.push(expression);
},
}
});
// 在transformer函数的末了,我们把我们刚建立的新AST返回
return newAst;
}

我们一样之前面的例子来看一下新建立AST长什么模样:

《手把手教你完成一个简朴的编译器》

5.3、 代码天生的完成

如今让我们进入我们的末了一个步鄹:代码天生。我们的代码天生函数会递归的挪用自身用来打印它的节点到一个很大的字符串。也就是完成由newAST到代码的历程:

newAst => generator => output

5.3.1 codeGenerator的完成

function codeGenerator(node) {
// 我们会依据节点的type范例来将事变离别处置惩罚
switch (node.type) {
// 假如我们有一个Program节点,我们将遍历body中的每一个节点而且对每一个节点递挪用codeGenerator
// 函数,而且将它们的效果用一个换行符连接起来
case 'Program':
return node.body.map(codeGenerator)
.join('\n');
// 关于ExpressionStatement节点,我们将在节点的expression节点上挪用
// codeGenerator函数,然后我们会加上一个分号(即;)
case 'ExpressionStatement':
return (
codeGenerator(node.expression) +
';' // <<(...because we like to code the *correct* way)
);
// 关于CallExpression节点,我们会打印callee并最先一个做括弧
// 我们会遍历该节点的arguments属性,然后对每一个属性挪用codeGenerator要领,
// 将他们的效果用逗号分开,末了在背面加一个右括弧
case 'CallExpression':
return (
codeGenerator(node.callee) +
'(' +
node.arguments.map(codeGenerator)
.join(', ') +
')'
);
// 关于标识符,我们将返回节点的名字
case 'Identifier':
return node.name;
// 关于NumberLiteral节点,我们返回它的value属性
case 'NumberLiteral':
return node.value;
// 关于StringLiteral节点,我们用引号将它的value属性值包裹起来
case 'StringLiteral':
return '"' + node.value + '"';
// 假如没有辨认节点,我们将抛出毛病
default:
throw new TypeError(node.type);
}
}

一样以上面例子举例,它的输出效果如图:

《手把手教你完成一个简朴的编译器》

6、编译器(compiler)的完成

如今,编译器的三大步鄹的代码都已完成了,我们如今最先完成编译器,它的体式格局就是将三个步鄹链接起来,可以将这几个步鄹形貌以下:

1. input => tokenizer => tokens
2. tokens => parser => ast
3. ast => transformer => newAst
4. newAst => generator => output

我们的编译器代码以下:

function compiler(input) {
let tokens = tokenizer(input);
let ast = parser(tokens);
let newAst = transformer(ast);
let output = codeGenerator(newAst);
// and simply return the output!
return output;
}

末了作为一个模块,我们愿望别人去运用它,因为我们的每一个函数都是相对自力的一个功能模块,所以我们将这内里的每一个函数都导出:

module.exports = {
tokenizer,
parser,
traverser,
transformer,
codeGenerator,
compiler,
};

更多相干和无关内容迎接阅读Github和个人博客

书把手系列还包含:手把手教你完成一个简朴的Promise,手把手教你完成一个简朴的MVC形式,手把手教你完成一个简朴的MVP形式,手把手教你完成一个简朴的MVVM形式。


推荐阅读
  • 深入理解Java SE 8新特性:Lambda表达式与函数式编程
    本文作为‘Java SE 8新特性概览’系列的一部分,将详细探讨Lambda表达式。通过多种示例,我们将展示Lambda表达式的不同应用场景,并解释编译器如何处理这些表达式。 ... [详细]
  • H5技术实现经典游戏《贪吃蛇》
    本文将分享一个使用HTML5技术实现的经典小游戏——《贪吃蛇》。通过H5技术,我们将探讨如何构建这款游戏的两种主要玩法:积分闯关和无尽模式。 ... [详细]
  • 文章目录前言Program(程序)Identifier(标识符)Literal(字面量)Vari ... [详细]
  • 编译原理中的语法分析方法探讨
    本文探讨了在编译原理课程中遇到的复杂文法问题,特别是当使用SLR(1)文法时遇到的多重规约与移进冲突。文章讨论了可能的解决策略,包括递归下降解析、运算符优先级解析等,并提供了相关示例。 ... [详细]
  • Hadoop MapReduce 实战案例:手机流量使用统计分析
    本文通过一个具体的Hadoop MapReduce案例,详细介绍了如何利用MapReduce框架来统计和分析手机用户的流量使用情况,包括上行和下行流量的计算以及总流量的汇总。 ... [详细]
  • Java虚拟机及其发展历程
    Java虚拟机(JVM)是每个Java开发者日常工作中不可或缺的一部分,但其背后的运作机制却往往显得神秘莫测。本文将探讨Java及其虚拟机的发展历程,帮助读者深入了解这一关键技术。 ... [详细]
  • 本文详细探讨了在Java中如何将图像对象转换为文件和字节数组(Byte[])的技术。虽然网络上存在大量相关资料,但实际操作时仍需注意细节。本文通过使用JMSL 4.0库中的图表对象作为示例,提供了一种实用的方法。 ... [详细]
  • 字符串中特定模式出现次数的计算方法
    本文详细探讨了如何高效地计算字符串中特定模式(如'pat')的出现次数,通过实例分析与算法解析,帮助读者掌握解决此类问题的方法。 ... [详细]
  • MySQL InnoDB 存储引擎索引机制详解
    本文深入探讨了MySQL InnoDB存储引擎中的索引技术,包括索引的基本概念、数据结构与算法、B+树的特性及其在数据库中的应用,以及索引优化策略。 ... [详细]
  • 如何从BAM文件绘制ATAC-seq插入片段长度分布图?
    在ATAC-seq数据处理中,插入片段长度的分布图是一个重要的质量控制指标,它能反映出核小体的周期性排列。本文将详细介绍如何从BAM文件中提取并绘制这些数据。 ... [详细]
  • Android与JUnit集成测试实践
    本文探讨了如何在Android项目中集成JUnit进行单元测试,并详细介绍了修改AndroidManifest.xml文件以支持测试的方法。 ... [详细]
  • 流处理中的计数挑战与解决方案
    本文探讨了在流处理中进行计数的各种技术和挑战,并基于作者在2016年圣何塞举行的Hadoop World大会上的演讲进行了深入分析。文章不仅介绍了传统批处理和Lambda架构的局限性,还详细探讨了流处理架构的优势及其在现代大数据应用中的重要作用。 ... [详细]
  • 本文详细介绍了如何在 Node.js 环境中利用 Nodemailer 库实现邮件发送功能,包括环境配置、代码实现及常见问题解决方法。 ... [详细]
  • 本文介绍了一个使用Spring框架和Quartz调度器实现每周定时调用Web服务获取数据的小项目。通过详细配置Spring XML文件,展示了如何设置定时任务以及解决可能遇到的自动注入问题。 ... [详细]
  • 本文将深入探讨 Unreal Engine 4 (UE4) 中的距离场技术,包括其原理、实现细节以及在渲染中的应用。距离场技术在现代游戏引擎中用于提高光照和阴影的效果,尤其是在处理复杂几何形状时。文章将结合具体代码示例,帮助读者更好地理解和应用这一技术。 ... [详细]
author-avatar
再见要死不活的_454
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有