手把手教你实现一个简单的编译器

2023-05-16

手把手教你实现一个简单的编译器

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 < input.length) {

    // 我们还会存储变量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 < tokens.length) {
    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模式。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

手把手教你实现一个简单的编译器 的相关文章

  • 用预训练的densenet121模型训练cifar10数据集

    densenet121采用pytorch预训练模型 xff0c 这里用cifar10作为数据集 import torchvision models as models import ssl ssl create default https
  • Ubuntu环境下安装DBoW2

    简介 DBoW2 is an improved version of the DBow library an open source C 43 43 library for indexing and converting images in
  • java如何将二进制转换为十进制

    1 使用java内部提供的方法 xff0c 直接进行api的调用 public static void binaryTodecimal2 int n String res 61 Integer toBinaryString n System
  • Spring Cloud Feign 请求动态URL

    2019独角兽企业重金招聘Python工程师标准 gt gt gt 1 FeignClient 中不要写url 使用 64 RequestLine 修饰方法 2 调用地方必须引入 FeignClientConfiguration 必须有De
  • 折半查找:查找成功的最少/多次数、平均次数,查找不成功的最少/多次数、平均次数...

    2019独角兽企业重金招聘Python工程师标准 gt gt gt 最方面的方法是建立一个判定树 现在有11个数 xff1a xff08 第1行是索引 xff0c 第2行是数 xff09 0 1 2 3 4 5 6 7 8 9 10 7 1
  • 关于maven打包 “程序包com.sun.deploy.net不存在” 的问题

    2019独角兽企业重金招聘Python工程师标准 gt gt gt 关于maven打包 程序包com sun deploy net不存在 的问题 遇到问题如下 xff1a INFO payGateway 1 0 SNAPSHOT SUCCE
  • 印度理工学院有多难考?

    http app myzaker com news article php pk 61 599546401bc8e08604000085 印度理工学院有多难考 xff1f 何赟08 17 原文是六月高考季时给公众号 34 中印对话 34 x
  • iOS系统下 的手机屏幕尺寸 分辨率 及系统版本 总结

    今天 我对iOS系统下 的手机屏幕尺寸 分辨率 及系统版本做了一次系统总结 供大家参考 首先 是系统 xff1a 随着iOS 系统不断升级 xff0c 现在已经到iOS7 0了 xff0c 并且TA有了很多新变化 xff0c 最震撼的就是
  • android ViewFlipper的使用

    屏幕切换指的是在同一个Activity内屏幕见的切换 xff0c 最长见的情况就是在一个FrameLayout内有多个页面 xff0c 比如一个系统设置页面 xff1b 一个个性化设置页面 通过查看 OPhone API文档可以发现 xff
  • Linux下路由配置梳理

    在日常运维作业中 xff0c 经常会碰到路由表的操作 下面就linux运维中的路由操作做一梳理 xff1a 先说一些关于路由的基础知识 xff1a 1 xff09 路由概念 路由 xff1a 跨越从源主机到目标主机的一个互联网络来转发数据包
  • ASP.NET成员角色系列(一)--验证与授权入门

    在当今的信息世界里 无论是门户网站 电子商务 社区论坛 都有一个共性 它们通常都需要验证当前用户的身份并根据验证结果判断用户所具有的权限 例如博客园 它允许未注册的匿名用户可能查看帖子 但是不允许他们发表帖子 为了能够发表帖子 匿名用户必须
  • 老赵谈IL(4):什么时候应该学IL,该怎么学IL

    又是一个拖了半年的系列 xff0c 可能是前几篇主要以事实为准 xff0c 举例子的文章总是比较容易写的 xff0c 因此十分顺畅 而最后一篇打算做一个总结 xff0c 以讲道理为主 却发现该将的似乎都已经讲完了 不过做事要有始有终 xff
  • FreeRTOS的第一个任务是怎么跑起来的

    一 一般在程序末尾会有一个vTaskStartSheduler 函数 span class hljs keyword int span main span class hljs keyword void span BSP INIT Bina
  • STM32-正弦波可调(50HZ~20KHZ可调、峰峰值0~3.3V可调)

    1 原理 通过定时器每隔一段时间触发一次DAC转换 然后通过DMA发送正玄波码表值给DAC 当需要改变频率HZ 时 只需要修改定时器频率 即可 最高只能达到20KHz 当需要改变 正玄波的正峰峰值 负峰峰值 时 只需要修改正玄波码表 即可
  • .Net ASP.NET 打开指定文件夹

    比如要打开指定的文件夹 xff0c 而不是弹出对话框 System Diagnostics Process Start 64 34 D 34 这样就打开了D盘 和正常打开D盘是一样的
  • 几种更新(Update语句)查询的方法

    正 文 数据库更新就一种方法Update xff0c 其标准格式 xff1a Update 表名 set 字段 61 值 where 条件 只是依据数据的来源不同 xff0c 还是有所差别的 xff1a 1 从外部输入 这样的比較简单 例
  • mysql8.0.13 cmd 登陆报错

    今天打算配置一个php运行环境 xff0c 将php mysql apache依次下载好 xff0c 我首先安装的是mysql xff0c 安装过程很顺利 xff0c 在cmd输入mysql uroot p的时候 xff0c 我靠 xff0
  • vue移动端的自适应布局的两种解决方案

    目标 前端开发移动端及H5时候 xff0c 不需要再关心移动设备的大小 xff0c 只需要按照固定 设计稿的px值布局 基础知识 dpr xff08 设备像素比 xff09 css的像素px不等于设备像素 分辨率 各种值 xff0c css
  • 对单片机数码管显示段选位选的理解

    在51单片机的数码管的应用开发中一些小的细节还是应该注意到的 其中位选信号应该在段选之前打开 xff0c 下面是一段示例代码 xff08 我用的是国信长天开发板 xff09 xff1a include lt reg51 h gt 包含51单

随机推荐

  • http请求中get请求可以缓存和post请求不可缓存

    2019独角兽企业重金招聘Python工程师标准 gt gt gt GET请求后退 刷新无害 xff0c POST后退 刷新则会导致重新提交数据 GET书签可被收藏 POST为书签不可收藏 GET能被缓存 POST不能被缓存 GET编码类型
  • VMWare中虚拟机(CentOS)如何开启虚拟化功能

    2019独角兽企业重金招聘Python工程师标准 gt gt gt 需求说明 xff1a VMware版本如下示 xff0c 在此VMware上创建了虚拟机并安装了CentOS6 5系统 现在需要在此客户机 xff08 VM xff09 上
  • C# 常见的错误类型

    Exception 应用程序执行期间发生错误 SystemException 系统异常 所有Exception的基类 ArgumentException 当方法提供的任意一个参数无效时 xff0c 引发此异常 ArithmeticExcep
  • 字典树 trie树 学习

    一字典树 字典树 xff0c 又称单词查找树 xff0c Trie树 xff0c 是一种树形结构 xff0c 哈希表的一个变种 二 性质 根节点不包含字符 xff0c 除根节点以外的每一个节点都只包含一个字符 xff1b 从根节点到某一节点
  • 《Linux 内核完全注释》阅读笔记

    在阅读源代码之前 xff0c 有必要对Linux内核的体系结构 源代码的目录结构有个宏观地了解 xff0c Linux内核完全注释 非常详细地介绍了这方面的内容 xff0c 所以 这里仅仅进行概述性的讨论 xff0c 以便让所有的笔记构成一
  • 抽象工厂模式(C++)

    define win 0 define mac 1 include lt iostream gt using namespace std class button public button virtual button virtual v
  • 大智慧显示切换服务器,大智慧怎么改界面 大智慧改界面教程

    很多软件的界面都可以根据每个用户不同的需求进行定制 xff0c 大智慧炒股软件也是如此 在大智慧的版面设计功能中 xff0c 用户可以将几十种不同功能的窗口自由组合摆放 xff0c 直到配置出满意的界面 大智慧的版面设计可以建立分析功能窗口
  • threadx将linux作为进程,如何在Windows操作系统上模拟ThreadX应用程序

    是的 xff0c 你可以的 xff0c 如果你愿意投入的工作 首先观察到每个线程系统调用都有一个等价的posix调用 xff0c 除了事件 因此 xff0c 您的线程程序可以使用posix线程 xff0c 互斥锁等作为单个进程运行 事件可以
  • STL"源码"剖析-重点知识总结

    STL是C 43 43 重要的组件之一 xff0c 大学时看过 STL源码剖析 这本书 xff0c 这几天复习了一下 xff0c 总结出以下LZ认为比较重要的知识点 xff0c 内容有点略多 1 STL概述 STL提供六大组件 xff0c
  • inter处理器(CPU)的分类

    对于台式机和笔记本电脑 xff0c 最常见的是酷睿 奔腾和赛扬系列 xff0c 同代产品中他们的性能依次减弱 xff0c 酷睿最强 xff0c 奔腾次之 xff0c 赛扬最弱 xff08 酷睿 gt 奔腾 gt 赛扬 xff09 对于智能手
  • 利用iftop查看网络带宽使用情况

    2019独角兽企业重金招聘Python工程师标准 gt gt gt 利用iftop查看服务器实时流量 yum install y gcc flex byacc libpcap ncurses ncurses devel libpcap de
  • matlab程序改为m文件名,在MATLAB中,程序文件的扩展名为.m,所以程序文件也称为M文件...

    在MATLAB中 xff0c 程序文件的扩展名为 m xff0c 所以程序文件也称为M文件 答 xff1a 磷酸果糖激酶 2催化6 磷酸果糖生成的产物是 答 xff1a 2 xff0c 6 二磷酸果糖 人类行为的经济学分析 的作者是 答 x
  • 学习ASP.NET Core Razor 编程系列十八——并发解决方案

    学习ASP NET Core Razor 编程系列目录 学习ASP NET Core Razor 编程系列一 学习ASP NET Core Razor 编程系列二 添加一个实体 学习ASP NET Core Razor 编程系列三 创建数据
  • Kubernetes运行监控-使用Helm快速部署Prometheus和Grafana

    2019独角兽企业重金招聘Python工程师标准 gt gt gt Kubernetes运行监控 使用Helm快速部署Prometheus和Grafana 使用Helm快速部署Pormetheus和Grafana非常方便 xff0c 很多手
  • linux交叉编译c++

    下载g 43 43 交叉编译工具链 sudo apt install g 43 43 arm linux gnueabihf 测试程序 include lt iostream gt using namespace std int main
  • 因子分析factor analysis_spss运用_python建模(推荐AAA)

    sklearn实战 乳腺癌细胞数据挖掘 xff08 博主亲自录制视频 xff09 https study 163 com course introduction htm courseId 61 1005269003 amp utm camp
  • 清除ListBox的列表项(删除所有项目)

    如何清除ListBox的列表项 删除所有项目 xff0c 今天开发程序时 xff0c 有尝试使用此功能 一开始并不是很顺利 循环所有item去做remove时 xff0c 需要执行两次才可以完成清除 debug进行步进跟踪 xff0c 发现
  • SVN查看所有日志提交记录

    1 svn默认显示最近一周的文件提交和修改记录 xff0c 怎么查看更长时间的日志记录呢 xff1f 2 TortoiseSVN 3 点击show all 或者NEXT 100 xff0c 就可显示更长时间的文件提交记录
  • Nearest neighbor graph | 近邻图

    最近在开发一套自己的单细胞分析方法 xff0c 所以copy paste事业有所停顿 实例 xff1a R eNetIt v0 1 1 data ralu site Saturated spatial graph sat graph lt
  • 手把手教你实现一个简单的编译器

    手把手教你实现一个简单的编译器 1 概述 今天我们将学习开发一个编译器 xff0c 但是呢 xff0c 这个编译器并不是说什么都能都编译 xff0c 它只是一个超级小的编译器 xff0c 主要用于说明编译器的一些基本的原理 我们这个编译器可