热门标签 | 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形式。


推荐阅读
  • 2023年京东Android面试真题解析与经验分享
    本文由一位拥有6年Android开发经验的工程师撰写,详细解析了京东面试中常见的技术问题。涵盖引用传递、Handler机制、ListView优化、多线程控制及ANR处理等核心知识点。 ... [详细]
  • 汇编语言等号伪指令解析:探究其陡峭的学习曲线
    汇编语言以其独特的特性和复杂的语法结构,一直被认为是编程领域中学习难度较高的语言之一。本文将探讨汇编语言中的等号伪指令及其对初学者带来的挑战,并结合社区反馈分析其学习曲线。 ... [详细]
  • 作者:守望者1028链接:https:www.nowcoder.comdiscuss55353来源:牛客网面试高频题:校招过程中参考过牛客诸位大佬的面经,但是具体哪一块是参考谁的我 ... [详细]
  • 本文探讨了如何在编程中正确处理包含空数组的 JSON 对象,提供了详细的代码示例和解决方案。 ... [详细]
  • 本文探讨了 C++ 中普通数组和标准库类型 vector 的初始化方法。普通数组具有固定长度,而 vector 是一种可扩展的容器,允许动态调整大小。文章详细介绍了不同初始化方式及其应用场景,并提供了代码示例以加深理解。 ... [详细]
  • 本文汇总了在正式宴会上常用的寒暄语句,包括欢迎词、感谢词及日常问候,适用于各种正式场合。这些语句不仅有助于提升交际礼仪,还能增进彼此之间的友好关系。 ... [详细]
  • 解决Element UI中Select组件创建条目为空时报错的问题
    本文介绍如何在Element UI的Select组件中使用allow-create属性创建新条目,并处理创建条目为空时出现的错误。我们将详细说明filterable属性的必要性,以及default-first-option属性的作用。 ... [详细]
  • 本教程涵盖OpenGL基础操作及直线光栅化技术,包括点的绘制、简单图形绘制、直线绘制以及DDA和中点画线算法。通过逐步实践,帮助读者掌握OpenGL的基本使用方法。 ... [详细]
  • 图数据库中的知识表示与推理机制
    本文探讨了图数据库及其技术生态系统在知识表示和推理问题上的应用。通过理解图数据结构,尤其是属性图的特性,可以为复杂的数据关系提供高效且优雅的解决方案。我们将详细介绍属性图的基本概念、对象建模、概念建模以及自动推理的过程,并结合实际代码示例进行说明。 ... [详细]
  • Codeforces Round #566 (Div. 2) A~F个人题解
    Dashboard-CodeforcesRound#566(Div.2)-CodeforcesA.FillingShapes题意:给你一个的表格,你 ... [详细]
  • 使用GDI的一些AIP函数我们可以轻易的绘制出简 ... [详细]
  • 本题探讨如何通过最大流算法解决农场排水系统的设计问题。题目要求计算从水源点到汇合点的最大水流速率,使用经典的EK(Edmonds-Karp)和Dinic算法进行求解。 ... [详细]
  • 本文详细介绍如何在VSCode中配置自定义代码片段,使其具备与IDEA相似的代码生成快捷键功能。通过具体的Java和HTML代码片段示例,展示配置步骤及效果。 ... [详细]
  • 本文详细介绍了Java中的输入输出(IO)流,包括其基本概念、分类及应用。IO流是用于在程序和外部资源之间传输数据的一套API。根据数据流动的方向,可以分为输入流(从外部流向程序)和输出流(从程序流向外部)。此外,还涵盖了字节流和字符流的区别及其具体实现。 ... [详细]
  • 本文介绍了一种解决二元可满足性(2-SAT)问题的方法。通过具体实例,详细解释了如何构建模型、应用算法,并提供了编程实现的细节和优化建议。 ... [详细]
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社区 版权所有