手写 compiler

看 babel doc 的时候,首页看到了一个链接,说是能从 high level 解释 babel 是怎么工作的 !!!

以下是babel官网提供的链接中的大致内容,有兴趣的戳这里看原文

简单说明

编译 是从 源代码(通常为高级语言) 到能直接被计算机编译器或虚拟机执行的 目标代码(通常为低级语言或机器语言) 的翻译过程.

如何用 JS 写一个超级小巧 (200行代码预定) 的编译器.

我们即将 (通过这个编译器) 把一些看着像 lisp 的代码, 编译成类 C 代码.
其实和把 ES6 转为 ES5, JSX/TS 转为 JS 一个套路.

具体的例子就是这个样子

                    LISP                    C
2 + 2               (add 2 2)               add(2, 2)
4 - 2               (subtract 4 2)          subtract(4 2)
2 + (4 - 2)         (add 2 (subtract 4 2))  add(2, subtract(4, 2))

非常简单吧?
虽然这既不是 LISP 语法也不是 C 语法. 但足够展示现代编译器的主要部分了.

背景

大多数编译器分为三个主要阶段:解析,转换,代码生成.

  1. 解析 : 将 原始代码 转换为 更抽象的表示
  2. 转换 : 将这个 抽象的表示 转化为编译器想要的东西.
  3. 代码生成 : 将转化后的表示 转化为新的代码.

解析

解析通常分为两个阶段:词法分析 和 句法分析

1 词法分析 (Lexical Analysis)
获取 原始代码 并通过 标记化器tokenizer(词法分析器lexer) 将其 拆分为 tokens

tokens 是一组非常小的 object,描述语法中的一个独立的部分。它们可能是数字,标签,标点符号,运算符, 等等等等。

2 语法分析 (Syntactic Analysis)
获取 token 并将其重新格式化为 描述语法的各个部分及其关系 的一种表现形式。这种表现形式称为 中间语言(intermediate representation) , 也叫 抽象语法树

比如:

(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: ')'        },
]

抽象语法树 Abstract Syntax Tree (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',
            }]
        }]
    }]
}

这样看起不爽, 其实 body 中的内容就是

        add
       /   \
     2    substract
         /      \
        4        2

转换

将 AST 转化为同样的语言, 或者其他完全不同的语言.

看看怎么转化 AST 吧.

你可能注意到了, 我们的 AST 中有一些元素, 看起来都差不多.
都有一个 type 属性. 这些我们把它叫做 AST 节点.
这些节点定义了自身的属性.

比如说这是一个数字:

{
    type: 'NumberLiteral',
    value: '2',
}

这是一个调用表达式:

{
    type: 'CallExpression',
    name: 'add',
    params: [ ...一些内嵌节点... ]
}

转换就是对 AST 节点的操作, 我们增加,删除,替换里面的属性.
也可以增加新的节点, 删除节点.
我们也可以把这个 AST 扔在这不操作, 而是基于它建一个新的出来.

因为我们这里, 要把代码转换为另一种语言(LIST to C), 我们后面就建一个新的 AST 好了.

遍历

为了浏览节点,我们需要能够遍历他们。
此遍历过程将使用 深度优先 来访问 AST 中的每个节点.

对于这个 AST.

        Program
          |
        add
       /   \
     2    substract
         /      \
        4        2

我们遍历的顺序是:

  1. Program
  2. CallExpression (add)
  3. NumberLiteral (2)
  4. CallExpression (substract)
  5. NumberLiteral (4)
  6. NumberLiteral (2)

如果我们要直接操作这棵树的话, 介绍一下不同的遍历方式还是有用的,
然而我们只是建一颗新的, 所以深度优先遍历足够了.

访客 vistor

基本思路是: 我们创建一个访客 vistor 对象, 它有不同方法能接受不同类型 (type) 的节点.

var visitor = {
    NumberLiteral() {},
    CallExpression() {},
};

遍历过程中, 当我们进入 (enter) AST 中每一个节点的时候, 我们都会根据节点 type 调用这个 vistor 的对应方法.

为了弄清楚类型, 这个函数需要接收当前节点 node
为了遍历, 这个函数需要知道它的父节点 parent

var visitor = {
    NumberLiteral(node, parent) {},
    CallExpression(node, parent) {},
};

不过, 我们可能还需要考虑一下 "退出 exit"
看一下树:

- Program
    - CallExpression
        - NumberLiteral
        - CallExpression
            - NumberLiteral
            - NumberLiteral

当我们遍历时, 可能走到没有子节点的分支, 我们就该 exit 了,
所以向下遍历我们叫做 "enter", 往回退我们叫做 "exit".

-> 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)

为了支持 enter 和 exit, 最终我们的 vistor 差不多是这样:

var visitor = {
    NumberLiteral: {
        enter(node, parent) {},
        exit(node, parent) {},
    }
};

代码生成

最后一步啦.

有一些 compiler 可能会把 transform 和这一步叠加在一起,
但是 代码生成(code generation) 这一步的主要目的是, 把我们的AST转为代码字符串(string-ify)。

代码生成有很多种做法, 大多数就是用我们刚才生成的 AST 来做的.

我们的 code generator 应该知道怎么打印出这些不同类型的 node (递归 nodes 是常见操作)

ta-da!

完毕, 这就是编译器的所有不同部分.

并不是说每个编译器看起来都像我在这里描述的那样.编译器有许多不同的用途,它们可能需要更多的步骤.
你现在应该大概知道它是怎么运作的.

代码时间

点这里 获取代码

(/ ^ ▽ ^ )/ tokenizer

我们将从解析第一阶段,词法分析 tokenizer (token 化)开始.
也就是把代码字符串, 转化为一组 token

(add 2 (subtract 4 2))   =>   [{ type: 'paren', value: '(' }, ...]
function tokenizer(input) {
    // current 标记我们现在在 code 的哪个位置, 其实就是 index
    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 });s
            continue;
        }
        
        // 也处理一下 string 字符串
        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);
    }
}

ヽ/❀o ل͜ o\ノ parser

接收一个 token 数组, 返回一个 AST

 [{ type: 'paren', value: '(' }, ...]   =>   { type: 'Program', body: [...] }
// 接收一个 token 数组
function parser(tokens) {
    let current = 0;
    
    // 递归 recursion
    function walk() {
        let token = tokens[current];
        
        if (token.type === 'number') {
            current++;
            // 数字节点, 直接返回
            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;
}

the token array

[
    { 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: ')'        }, //<<< Closing parenthesis
    { type: 'paren',  value: ')'        }, //<<< Closing parenthesis
]

the tree

            (
          /   
        param 
      /    |   \
    "add"  2    (
                |
               param
             /  |  \
         "sub"  4   2

AST DONE!!

⌒(❀>◞౪◟<❀)⌒ traverser

使用 visitor 访问不同 AST 节点, 在遇到不同 type 的节点 call 不同的方法.

函数结构:

traverse(ast, {
    Program: {
        enter(node, parent) {},
        exit(node, parent) {}
    },
 
    CallExpression: {
        enter(node, parent) {},
        exit(node, parent) {},
    },

    NumberLiteral: {
        enter(node, parent) {},
        exit(node, parent) {},
    },
})

定义一个 traverser 方法接收一个 AST 和 visitor. 内部有两个 functions

// 定义一个 traverser 方法接收一个 AST 和 visitor. 内部有两个 functions
function traverser(ast, visitor) {

  // `traverseArray` 可以让我们遍历数组, 并调用我们定义的另一个方法 `traverseNode`
  function traverseArray(array, parent) {
    array.forEach(child => {
      traverseNode(child, parent);
    });
  }

  // `traverseNode` 接收 `node` 和它的 `parent` 节点. 
  // 这样我们就能传 visitor 的两个 methods.
  function traverseNode(node, parent) {

    // 我们首先通过 type 看看 visitor 是否存在方法
    let methods = visitor[node.type];

    // 如果这个节点有 `enter` 方法, 我们就 enter 进入
    if (methods && methods.enter) {
      methods.enter(node, parent);
    }

    // 接下来,我们将按节点 type 拆分
    switch (node.type) {
      // 从 level `Program` 开始, 它的 body 属性的值是一个 node 的 array,     
      // 我们用上面的方法遍历
      // traverseArray 将依次调用 `traverseNode`,所以我们在通过递归对树进行遍历
      case 'Program':
        traverseArray(node.body, node);
        break;
      // 接下来, 我们对 CallExpression 的参数 params 做同样的事
      case 'CallExpression':
        traverseArray(node.params, node);
        break;
      // 遇到没有子节点的 `NumberLiteral` 和 `StringLiteral`,
      // 直接 break 就可以了
      case 'NumberLiteral':
      case 'StringLiteral':
        break;
      // 不认识的节点, 直接报错
      default:
        throw new TypeError(node.type);
    }

    // 如果这个节点有 `exit` 方法, 我们就 exit 回退
    if (methods && methods.exit) {
      methods.exit(node, parent);
    }
  }

  // 调用 traverseNode, 传入 AST, AST 的根节点没有 parent 传 null
  return traverseNode(ast, null);
}

⁽(◍˃̵͈̑ᴗ˂̵͈̑)⁽ the transformer !!!

接下来,transfromer。
我们的 transformer 将基于我们刚刚建立的 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接收 lisp 的 AST

function transformer(ast) {
  let newAst = {
    type: 'Program',
    body: [],
  };

  ast._context = newAst.body;

  // 用 AST 和 visitor 调用 traverser
  traverser(ast, {

    // 第一个 visitor 接收所有的 `NumberLiteral`
    NumberLiteral: {
      // 通过 enter 访问
      enter(node, parent) {
        // 我们会创建一个新的节点, 也叫做 NumberLiteral, 然后把它放入 parent 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` 并内嵌一个 `Identifier`.
        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`, 
        //因为 `CallExpression` 在 JS 中是真的语句
          expression = {
            type: 'ExpressionStatement',
            expression: expression,
          };
        }

        // 最后, 把 (包装过的) `CallExpression` 放入 parent 的 context.
        parent._context.push(expression);
      },
    }
  });

  // 返回刚刚生成的 new AST
  return newAst;
}

ヾ(〃^∇^)ノ♪ code generator

最后一步, Code Generator, 将以递归的方式打印上一步得到的 AST 的每个节点.

function codeGenerator(node) {

  // 通过node 的 type 属性, 把 AST 转为 code
  switch (node.type) {

    // 如果遇到 Program 节点, 就遍历 body 中的每个节点,
    case 'Program':
      return node.body.map(codeGenerator)
        .join('\n');

    // 对于 `ExpressionStatement`, 我们在内嵌表达式中递归的使用代码生成器, 结束后加上分号
    case 'ExpressionStatement':
      return (
        codeGenerator(node.expression) + ';'
      );

    // 如果遇到 `CallExpression`, 就打印 `callee` 和左括号,
    // 把 node 中的 arguments 映射成数组, 用逗号 join 一下, 最后打印一个右括号
    case 'CallExpression':
      return (
        codeGenerator(node.callee) + '(' +  node.arguments.map(codeGenerator).join(', ') + ')'
      );

    // 遇到 `Identifier` 直接返回 `node`'s 名字.
    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)