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

尽管大多数开发人员在日常工作中不需要开发编译器,但编译器的概念贯穿我们使用的许多工具和技术。我们也是可以抽空了解一下其原理。在 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...

相关推荐
腾讯TNTWeb前端团队1 小时前
helux v5 发布了,像pinia一样优雅地管理你的react状态吧
前端·javascript·react.js
范文杰5 小时前
AI 时代如何更高效开发前端组件?21st.dev 给了一种答案
前端·ai编程
拉不动的猪5 小时前
刷刷题50(常见的js数据通信与渲染问题)
前端·javascript·面试
拉不动的猪5 小时前
JS多线程Webworks中的几种实战场景演示
前端·javascript·面试
FreeCultureBoy6 小时前
macOS 命令行 原生挂载 webdav 方法
前端
uhakadotcom6 小时前
Astro 框架:快速构建内容驱动型网站的利器
前端·javascript·面试
uhakadotcom6 小时前
了解Nest.js和Next.js:如何选择合适的框架
前端·javascript·面试
uhakadotcom7 小时前
React与Next.js:基础知识及应用场景
前端·面试·github
uhakadotcom7 小时前
Remix 框架:性能与易用性的完美结合
前端·javascript·面试
uhakadotcom7 小时前
Node.js 包管理器:npm vs pnpm
前端·javascript·面试