从零开始构建一个超级小巧的编译器

尽管大多数开发人员在日常工作中不需要开发编译器,但编译器的概念贯穿我们使用的许多工具和技术。我们也是可以抽空了解一下其原理。在 GitHub 上发现一篇很不错的编译器的代码库,就想着和大家一起分享学习一下。

介绍

这份代码库用易于阅读的 JavaScript 编写的现代编译器的所有主要部分的超简化示例

通读指导代码将帮助我们了解大多数编译器从头到尾的工作方式

其实就简单的几个函数方法,用不了多长时间就能看完!它将一个简单的 Lisp 方言编译成对应的 AST(抽象语法树)并返回。

那就废话不多说,直接进入正题吧!

预习

前端开发中常见的编译器包括但不限于以下几种:

  1. Babel:用于将 ES6+ 代码转换为向后兼容的 JavaScript 代码,以便在当前和旧版本的浏览器中运行。
  2. TypeScript Compiler(tsc) :用于将 TypeScript 代码编译为 JavaScript 代码,使其可以在浏览器中执行。
  3. Sass / SCSS Compiler:用于将 Sass 或 SCSS 文件编译为普通的 CSS 文件,以便在浏览器中使用。
  4. Less Compiler:类似于 Sass 编译器,将 Less 文件编译为 CSS 文件。
  5. PostCSS:一个用 JavaScript 编写的工具,用于转换 CSS,并支持许多插件,可以进行自定义的 CSS 处理,例如添加浏览器前缀、压缩、提取变量等。
  6. Vue LoaderReact JSX Compiler:这些工具用于将 Vue 和 React 组件编译为浏览器可执行的 JavaScript 代码。

而这些大多数编译器通常经历三个主要阶段:解析、转换、代码生成

  1. 解析:将原始代码解析为更抽象的代码表示形式。
  2. 转换:使用抽象表示形式进行操作,执行编译器期望的转换。
  3. 代码生成:采用转换后的代码表示形式,并将其转换为新的代码形式。

首先,解析 通常包括词法分析和句法分析两个阶段:

  1. 词法分析:通过词法分析器(或称为 lexer),将原始代码拆分为一系列令牌(tokens)。这些令牌可以是数字、标识符、标点符号、运算符等。
  2. 句法分析:通过句法分析器,将这些令牌重新组织成描述代码语法结构的中间表示,也称为抽象语法树(Abstract Syntax Tree,AST)。AST 是一个嵌套的对象结构,用于表示代码的语法信息,以便进一步的处理和分析。

然后,转换阶段涉及对抽象语法树(AST)的修改。

  • 在转换阶段,可以操作同一语言的 AST,也可以将其转换为全新的语言。
  • 在 AST 中,有一些非常相似的元素,它们都具有类型属性。每个这样的对象被称为 AST 节点,它们定义了树中各个节点的属性,描述了树的各个部分。
  • 需要按照深度优先的方式遍历 AST 中的每个节点。以达到遍历所有节点。
  • 在转换 AST 时,我们可以进行以下操作:
    • 添加、删除或替换节点的属性;
    • 添加新节点或删除节点;
    • 放弃现有的 AST,创建一个全新的 AST,以满足特定于目标语言的需求。

最后,大部分代码生成只是将我们的 AST 转换回字符串形式的代码。

  • 代码生成器将知道如何 "打印" 所有不同类型的 AST 节点,并且它将递归地调用自身来打印嵌套节点,直到所有内容都打印成一个长的字符串。

源码学习

Let's begin...

tokenizer 词法分析器

js 复制代码
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;
    }

    // 字符串的情况
    if (char === '"') {
      let value = '';
      // 跳过开始的双引号
      char = input[++current];
      while (char !== '"') {
        value += char;
        char = input[++current];
      }
      // 跳过结束的双引号
      char = input[++current];
      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];
      }
      tokens.push({ type: 'name', value });
      continue;
    }

    // 错误处理
    throw new TypeError('I dont know what this character is: ' + char);
  }

  return tokens;
}

parser 解析器

js 复制代码
// 把令牌数组转换成 AST
function parser(tokens) {
  let current = 0;

  // 创建一个递归函数
  function walk() {
    let token = tokens[current];
    // 把每种类型的令牌分成不同的代码路径
    
    // 数字
    if (token.type === 'number') {
      current++;
      // 返回新的 AST 节点
      return {
        type: 'NumberLiteral',
        value: token.value,
      };
    }

    // 字符串,类似 数字 处理
    if (token.type === 'string') {
      current++;
      return {
        type: 'StringLiteral',
        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);
  }

  // 创建 AST
  let ast = {
    type: 'Program',
    body: [],
  };
  while (current < tokens.length) {
    ast.body.push(walk());
  }

  return ast;
}

traverser 遍历器

js 复制代码
function traverser(ast, visitor) {
  // 迭代数组,并调用 traverseNode 函数
  function traverseArray(array, parent) {
    array.forEach(child => {
      traverseNode(child, parent);
    });
  }

  // 接受 节点 参数和 父节点,都传给 visitor 方法
  function traverseNode(node, parent) {
    let methods = visitor[node.type];
    if (methods && methods.enter) {
      methods.enter(node, parent);
    }

    switch (node.type) {
      // 先从顶层 `Program` 开始
      case 'Program':
        traverseArray(node.body, node);
        break;
      case 'CallExpression':
        traverseArray(node.params, node);
        break;
      // 在 `NumberLiteral` 和 `StringLiteral` 的情况下,没有任何子节点可访问,所以将中断。
      case 'NumberLiteral':
      case 'StringLiteral':
        break;
      default:
        throw new TypeError(node.type);
    }

    if (methods && methods.exit) {
      methods.exit(node, parent);
    }
  }
  // 启动遍历器
  traverseNode(ast, null);
}

transformer 转换器

js 复制代码
// 转换新的 AST
function transformer(ast) {
  let newAst = {
    type: 'Program',
    body: [],
  };
  // 将在父节点上使用一个名为 `context` 的属性,将节点推送到其父节点的 `context` 中
  // 这算是一种小技巧
  ast._context = newAst.body;

  // 用 ast 和 一个 visitor 方法触发遍历器
  traverser(ast, {
    // 第一个 visitor 方法
    NumberLiteral: {
      // enter 方法
      enter(node, parent) {
        // 创建一个名为"NumberLiteral"的新节点,并将其推送到父上下文。
        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) {

        // 创建一个带有嵌套 "Identifier" 的新节点 "CallExpression"。
        let expression = {
          type: 'CallExpression',
          callee: {
            type: 'Identifier',
            name: node.name,
          },
          arguments: [],
        };

        // 在原始的 "CallExpression" 节点上定义一个`_context`,
        // 将引用 "expression" 的参数,以便可以推送参数。
        node._context = expression.arguments;

        // 父级节点不是 CallExpression 
        if (parent.type !== 'CallExpression') {
          // 将用一个 `ExpressionStatement` 来包裹我们的 `CallExpression` 节点。
          // 这样做是因为 JavaScript 中顶层的 `CallExpression` 实际上是语句
          expression = {
            type: 'ExpressionStatement',
            expression: expression,
          };
        }

        // 将被包装过的 `CallExpression` 推送到 `parent` 的 `context` 中
        parent._context.push(expression);
      },
    }
  });

  // 返回新的 AST
  return newAst;
}

codeGenerator 代码生成器

js 复制代码
// 代码生成器将递归调用自身,将树中的每个节点打印成一个巨大的字符串。
function codeGenerator(node) {
  // 按照节点的`type`来拆分处理
  switch (node.type) {
    // 如果有一个`Program`节点,将遍历`body`中的每个节点,
    // 并将它们通过代码生成器运行,然后用换行符连接它们。
    case 'Program':
      return node.body.map(codeGenerator)
        .join('\n');

    // 对于 `ExpressionStatement`,将调用嵌套表达式的代码生成器,并添加一个分号...
    case 'ExpressionStatement':
      return (
        codeGenerator(node.expression) +
        ';' // << (...because we like to code the *correct* way)
      );

    // 对于 `CallExpression`,将打印 `callee`,添加一个左括号,
    // 然后将对 `arguments` 数组中的每个节点运行代码生成器,用逗号连接它们,
    // 最后添加一个右括号。
    case 'CallExpression':
      return (
        codeGenerator(node.callee) +
        '(' +
        node.arguments.map(codeGenerator)
          .join(', ') +
        ')'
      );

    // 对于 `Identifier`,返回 `node` 的名称。
    case 'Identifier':
      return node.name;

    // 对于 `NumberLiteral`,返回 `node` 的值。
    case 'NumberLiteral':
      return node.value;

    // 对于 `StringLiteral`,将在 `node` 的值周围添加引号。
    case 'StringLiteral':
      return '"' + node.value + '"';

    // 如果没有识别节点,将抛出一个错误。
    default:
      throw new TypeError(node.type);
  }
}

compiler 编译器

最后!将创建 compiler 函数。在这里,将连接管道的每个部分。

  1. 输入 => 词法分析器 => 令牌
  2. 令牌 => 解析器 => 抽象语法树
  3. 抽象语法树 => 转换器 => 新的抽象语法树
  4. 新的抽象语法树 => 生成器 => 输出
js 复制代码
function compiler(input) {
  let tokens = tokenizer(input);
  let ast    = parser(tokens);
  let newAst = transformer(ast);
  let output = codeGenerator(newAst);

  return output;
}

YOU MADE IT!

小结

这个编译器实现了一个非常简单的编译流程,可以将输入的代码字符串转换成目标代码字符串。虽然它非常简单,但展示了编译器的基本原理和流程。

我们是不是可以在此原理的基础上大刀阔斧的去创造了?!大家加油!

参考资料

github 源码:github.com/jamiebuilds...

相关推荐
Domain-zhuo3 分钟前
Git和SVN有什么区别?
前端·javascript·vue.js·git·svn·webpack·node.js
雪球不会消失了8 分钟前
SpringMVC中的拦截器
java·开发语言·前端
李云龙I19 分钟前
解锁高效布局:Tab组件最佳实践指南
前端
m0_7482370523 分钟前
Monorepo pnpm 模式管理多个 web 项目
大数据·前端·elasticsearch
JinSoooo25 分钟前
pnpm monorepo 联调方案
前端·pnpm·monorepo
m0_7482449634 分钟前
【AI系统】LLVM 前端和优化层
前端·状态模式
明弟有理想34 分钟前
Chrome RCE 漏洞复现
前端·chrome·漏洞·复现
平行线也会相交35 分钟前
云图库平台(二)前端项目初始化
前端·vue.js·云图库平台
shimmer00836 分钟前
抖音小程序登录(前端通过tt.login获取code换取openId)
前端·小程序·状态模式
嘤嘤怪呆呆狗1 小时前
【开发问题记录】使用 Docker+Jenkins+Jenkins + gitee 实现自动化部署前端项目 CI/CD(centos7为例)
前端·vue.js·ci/cd·docker·gitee·自动化·jenkins