闯关式学编译器

180 阅读5分钟

热身准备

在闯关之前,我们首先要知道每一关的 boss 是什么,这里我给大家罗列一下:

  1. 像往常一样第一关的 boss 比较简单,基本是有手就行,它就是 「词法分析」
  2. 第二关的 boss 稍微有些难度,它是「语法分析」
  3. 第三关比较开放,多了很多支线任务,我们为了快速通关,暂时做主线任务,boss 就是「转换」
  4. 第四关也到了末尾,这里比较轻松一些了,它是「代码生成」

上图就是整个的流程~~

好了,废话不多说,赶紧上车 🚌💨💨💨

第一关词法分析

正所谓知己知彼,百战百胜,接下来我们去了解对手。

什么是词法分析

下面是来自维基百科的解释

在计算机科学中,词法分析标记化是将字符序列(例如在计算机程序或网页中)转换为词法标记序列(具有特定含义并且可以被识别字符串)的过程。

都可以看懂是吧?[手动狗头]

实战

const obj = { name: 'gaoyuanyuan' };

这样一串 JavaScript 代码很简单吧,我们用人脑遵循一定的规则来进行词法分析:

  • const 关键字 Keyword
  • obj 标识符 Identifier
  • = 操作符 Punctuator
  • { 操作符 Punctuator
  • name 标识符 Identifier
  • : 操作符 Punctuator
  • ‘gaoyuanyuan’ 字符串 String
  • } 操作符 Punctuator
  • ; 操作符 Punctuator

最后用代码表示如下:

[    {        "type": "Keyword",        "value": "const"    },    {        "type": "Identifier",        "value": "obj"    },    {        "type": "Punctuator",        "value": "="    },    {        "type": "Punctuator",        "value": "{"    },    {        "type": "Identifier",        "value": "name"    },    {        "type": "Punctuator",        "value": ":"    },    {        "type": "String",        "value": "'gaoyuanyuan'"    },    {        "type": "Punctuator",        "value": "}"    },    {        "type": "Punctuator",        "value": ";"    }]

什么!!!知道答案还不会做题?那接下来一起看看如何实现吧:

function tokenizer(input) {
  let current = 0; // 指针,记录当前的位置
  let tokens = []; // 定义 tokens 容器存储产物
  const PUNCTUATOR = ['=', ':', '{', '}',';', "'"];
  const KEYWORD = ['const'];
  while(current < input.length) { // 依次分析每个字符
    let char = input[current];
    if (PUNCTUATOR.indexOf(char) > -1) { // 操作符
      if (char === "'") { // 遇到单引号开头直接跳过
        current++;
        continue;
      }
      tokens.push({
        type: 'Punctuator',
        value: char
      });
      current++;
      continue;
    }
    const WHITESPACE = /\s/;
    if (WHITESPACE.test(char)) { // 空格,跳过
      current++;
      continue;
    }
    const LETTERS = /[a-z]/i;
    if (LETTERS.test(char)) { // 字符,需要考虑到字符串的情况
      let value = '';
      while (LETTERS.test(char)) {
        value += char;
        char = input[++current];
      }
      if (KEYWORD.indexOf(value) > -1) {
        tokens.push({ type: 'Keyword', value });
        continue;
      }
      if (char === "'") { // 这里处理值为字符串的情况
        tokens.push({ type: 'String', value });
        continue;
      }
      tokens.push({ type: 'Identifier', value });
      continue;
    }
    // 如果是其他的类型,抛出错误
    throw new TypeError('I dont know what this character is: ' + char);
  }

  return tokens;
}

看到这里,恭喜你 🎉🎉🎉 ,第一关已经过关啦!!!

第二关语法分析

什么是语法分析

下面是来自维基百科的解释

语法分析器(parser)通常是作为编译器解释器的组件出现的,它的作用是进行语法检查、并构建由输入的单词组成的数据结构(一般是语法分析树抽象语法树等层次化的数据结构)。语法分析器通常使用一个独立的词法分析器从输入字符流中分离出一个个的“单词”,并将单词流作为其输入。实际开发中,语法分析器可以手工编写,也可以使用工具(半)自动生成。

实战

还是接着上面的例子,parse 后的结果:

{
  "type": "Program",
  "body": [
    {
      "type": "VariableDeclaration",
      "declarations": [
        {
          "type": "VariableDeclarator",
          "id": {
            "type": "Identifier",
            "name": "obj"
          },
          "init": {
            "type": "ObjectExpression",
            "properties": [
              {
                "type": "Property",
                "key": {
                  "type": "Identifier",
                  "name": "name"
                },
                "value": {
                  "type": "Literal",
                  "value": "gaoyuanyuan",
                  "raw": "'gaoyuanyuan'"
                }
              }
            ]
          }
        }
      ],
      "kind": "const"
    }
  ]
}

上述简化了一些参数,但总体还是遵循规则 这里我们一起来分析下 body 的结果:

  • 数组只有一个对象
  • type 为 'VariableDeclaration' ,表示为一段变量赋值语法,必须有两个子项:
    • kind: "var" | "let" | "const"
    • declarations: Array<VariableDeclarator>
  • type 为 'VariableDeclarator'
    • id: LVal
    • init: Expression
  • **LVal **表示一个节点值
  • type 为 'ObjectExpression'
    • properties: Array<ObjectProperty>
  • type 为 'ObjectProperty'
    • key: Identifier
    • value: PatternLike
  • type 为 'PatternLike'
    • type: "Literal"
    • value: "gaoyuanyuan"
    • raw: "'gaoyuanyuan'"

上图为结构简化版

function parser(tokens) {
  // 声明一个全时指针,它会一直存在
  let current = 0;
  // 指针指向的当前token
  let token = tokens[current];

  const parseDeclarations = () => {
      // 如果字符为'const'可见是一个声明
      if (token.type === 'Keyword' && token.value === 'const') {
          const VariableDeclaration = {
              type: 'VariableDeclaration',
              kind: token.value,
              declarations: []
          };

          next();
          // const 后面要跟变量的,如果不是则报错
          if (token.type !== 'Identifier') {
              throw new Error('Expected Variable after const');
          }
          const VariableDeclarator = {
            type: 'VariableDeclarator',
            id: {
              type: 'Identifier',
              name: token.value,
            },
            init: {}
          }
          next();
          if (token.type === 'Punctuator' && token.value === '=') {
            VariableDeclarator.init = parseExpression();
          }
          next();
          if (token.type === 'Identifier' && token.value === ';') {
            next();
          }
          VariableDeclaration.declarations.push(VariableDeclarator);
          return VariableDeclaration;
      }
  };

  const parseExpression = () => {
    next();
    switch(token.type) {
      case 'Punctuator':
        if (token.value === '{') {
          return parseObjectExpression()
        }
      default:
        throw new TypeError('I dont know what this type is: ' + token.type);
    }
  }

  const parseObjectExpression = () => {
      next();
      const init = {
        type: 'ObjectExpression',
        properties: []
      };
      while (token.value !== '}' && token.value !== ',') {
        init.properties.push(parseProperty());
        next();
      }

      return init;
  };

  const parseProperty = () => {

    let properties;
    if (token.type === 'Identifier') {
      properties = {
        type: 'Property',
        key: {
          type: 'Identifier',
          value: token.value
        },
        value: {}
      }
    }
    next();
    if (token.type !== 'Punctuator' || token.value !== ':') {
      throw new Error('Expected Operater after object');
    }
    next();
    properties.value = {
      type: 'Literal',
      value: token.value.substring(1, token.value.length-1),
      raw: token.value
    }
    return properties;
  }

  // 指针后移的函数
  const next = () => {
    current++;
    if (tokens[current]) {
      token = tokens[current];
    } else {
      token = { type: 'eof', value: '' };
    }
  };

  const ast = {
      type: 'Program',
      body: []
  };

  while (current < tokens.length) {
      const statement = parseDeclarations();
      if (!statement) {
          break;
      }
      ast.body.push(statement);
  }
  return ast;
}

这部分代码只是一个最小化实现,很多逻辑写的不是很完善,但是大体思想是正确的。

阅读建议:这部分代码相对也比较长,如果是阅读文章的话建议先跳过,如果感兴趣可以后续自己实现一次

坚持到这里的同学,恭喜你,已经超越全国 ***(手动打码) 的人。

到这里我们第二关也算是坎坎坷坷的闯过了 🎉🎉🎉

第三关转换

上面提到过,这关比较开放,而且也很好理解,就是将解析后的结构进行增/删/替换等的操作然后组成新的 AST 树,比如:我们想做语法的兼容性处理,可以把 const 替换成 var 这关比较简单,需要注意的是如何遍历树。这关就不上实战了,大家懂了思想可以自己尝试去实现。

我们继续下一关吧~

第四关代码生成

废话不多说,直接上代码:

function generator(node) {
  switch (node.type) {
  // 如果是 `Program` 节点,那么我们会遍历它的 `body` 属性中的每一个节点,并且进行递归
  // 对这些节点再次调用 generator,再把结果打印进入新的一行中。
  case 'Program':
    return node.body.map(generator).join('\n');

  // 如果是 VariableDeclaration 将关键字与变量拼接起来
  case 'VariableDeclaration':
    return node.kind + ' ' + node.declarations.map(generator);
  
  case 'VariableDeclarator':
    return generator(node.id) + ' = ' + generator(node.init)

  case 'Identifier':
    return node.name;
  
  case 'Literal':
    return node.raw;
  
  case 'Property':
    return generator(node.key) + ': ' + generator(node.value);

  case 'ObjectExpression':
    return '{ ' + node.properties.map(generator) + ' };';
  
  // 没有符合的则报错
  default:
      throw new TypeError(node.type);
  }
}

恭喜你 🎉🎉🎉 ,通关完成!!!

至此,我们的小型编译器就完成啦~

大家可以参考这个仓库来实现自己的小型编译器哦~

总结

众所周知,js 是不需要开发者关心编译代码的,直接放到浏览器中就可以运行,所以对于编译相关的知识了解甚少,希望这篇文章可以让大家对于编译有一定的了解。

最后,希望大家帮忙点点赞,你的点赞是对我创作莫大的支持~

感谢阅读❤️