the-super-tiny-compiler编译分析

505 阅读3分钟

编译原理

推荐在学习webpack前可以先学习the-super-tiny-compiler,主要是它的代码比较简短容易理解。webpack主要做的是将我们写的相关语法糖(JSX/es6等)转换成浏览器引擎能执行的代码,webpack中babel做事的原理同“the-super-tiny-compiler”,通过学习the-super-tiny-compiler,了解AST和编译原理。如果我们输入如下代码块

    const input = '(add 2 (subtract 4 2))'

期望转换输出为

   const output = 'add(2, subtract(4, 2));'

通过学习研究“the-super-tiny-compiler”, 你会了解上面的input通过那几步能输出output的结果

词法分析tokenizer

这里tokenizer主要是将输入的语法糖转换成tokens结构{type: 'type', value: 'value'}

function tokenizer(input) {

  // current变量用于记录代码的位置,类似光标的概念
  let current = 0;

  // tokens是记录输入input转化后的结果
  let tokens = [];
  
  // 开始对字符串input进行遍历
  while (current < input.length) {
    // char记录当前遍历到current位置的字符
    let char = input[current];
    // 检查是否是左括号‘(’
    if (char === '(') {
      // 左括号的类型用'paren'标示
      tokens.push({
        type: 'paren',
        value: '(',
      });
      // 指针+1
      current++;
      continue;
    }

    // 这里处理右括号‘(’,处理方式同上面左括号
    if (char === ')') {
      tokens.push({
        type: 'paren',
        value: ')',
      });
      current++;
      continue;
    }

    // 处理空格,对于空格不需要存入到token中,移动current位置即可
    let WHITESPACE = /\s/;
    if (WHITESPACE.test(char)) {
      current++;
      continue;
    }

    // 处理数据
    //   (add 123 456)
    //        ^^^ ^^^
  
    let NUMBERS = /[0-9]/;
    if (NUMBERS.test(char)) {
      // 记录数字
      let value = '';
      // 通过current后移,判断下一个位置上的char是否是数字,是就用value拼接记录
      while (NUMBERS.test(char)) {
        value += char;
        char = input[++current];
      }
      // 将连续数字存入tokens中,type是number类型
      tokens.push({ type: 'number', value });
      continue;
    }

    // 处理字符串
    //   (concat "foo" "bar")
    //            ^^^   ^^^ string tokens
    // 处理方式同上面number大致相同,不同的地方是通过双引号来判断开始和结束
  
    if (char === '"') {
      let value = '';
      char = input[++current];
      while (char !== '"') {
        value += char;
        char = input[++current];
      }
      char = input[++current];
      tokens.push({ type: 'string', value });
      continue;
    }

    // 处理方法名,处理方式同上面的字符串,区别是上面字符串可理解为变量,需要双引号包裹,这里的字符串是函数名,没有引号包裹
    // Name token
    //   (add 2 4)
    //    ^^^

    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;
}

执行tokens=tokenizer('(add 2 (subtract 4 2))')后获取到的tokens结果如下所示

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

语法分析parser

parser它的主要功能是将tokens转换成ast结构

function parser(tokens) {

  // current依然是记录当前指针的位置
  let current = 0;
  
  // 这里的walk主要是后面需要进行递归处理
  function walk() {
    let token = tokens[current];
    
    // 处理type是number的token
    if (token.type === 'number') {
      current++;
      // ast中number类型的type用NumberLiteral记录
      return {
        type: 'NumberLiteral',
        value: token.value,
      };
    }

    //处理type是string的token
    if (token.type === 'string') {
      current++;
      // ast中string类型的type用StringLiteral记录
      return {
        type: 'StringLiteral',
        value: token.value,
      };
    }

    // 处理表达式,type: 'paren', 以左括号开始,并继续查找知道以右括号结束 ast中是以CallExpressions记录type,其中这里的node节点多了params参数
    if (
      token.type === 'paren' &&
      token.value === '('
    ) {

      token = tokens[++current];
   
      let node = {
        type: 'CallExpression',
        name: token.value,
        params: [],
      };

      token = tokens[++current];
      // 查找type是paren,且以右括号结束的token,如果不是一直循环,
      while (
        (token.type !== 'paren') ||
        (token.type === 'paren' && token.value !== ')')
      ) {
        //对非结束token进行递归处理
        node.params.push(walk());
        token = tokens[current];
      }
      current++;
      return node;
    }

    // 主要处理了number/string/paren类型的type, 对于未识别的type异常处理
    throw new TypeError(token.type);
  }

  // 创建ast的根节点,type是Program
  let ast = {
    type: 'Program',
    body: [],
  };

  // ast.body内容就是将token递归执行walk的过程
  while (current < tokens.length) {
    ast.body.push(walk());
  }
  return ast;
}

这里执行ast = parser(token)后获取的就是ast的数据,其结果如下所示

  const 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'
        }]
      }]
    }]
  };

traverser

这里的遍历主要是在transformer中调用,先提前了解它的原理。接受2个参数ast和visitor,其中参数ast其结构如上面所示,visitor大概结构如下所示

    visitor = {
     NumberLiteral: {enter(node, parent) {}},
     StringLiteral: {enter(node, parent) {}},
     CallExpression: {enter(node, parent) {}},
   }

traverser方法主要是对旧ast进行遍历,根据相应的type执行visitor传过来的enter方法

function traverser(ast, visitor) {

  // 对array数组依次执行traverseNode方法
  function traverseArray(array, parent) {
    array.forEach(child => {
      traverseNode(child, parent);
    });
  }

  function traverseNode(node, parent) {

    // methods主要是获取visitor对应type的方法
    let methods = visitor[node.type];

    // 执行methods方法
    if (methods && methods.enter) {
      methods.enter(node, parent);
    }

  
    //  traverseArray依次处理各类型的子节点
    switch (node.type) {
      // 根节点
      case 'Program':
        traverseArray(node.body, node);
        break;
      // 表达式节点
      case 'CallExpression':
        traverseArray(node.params, node);
        break;
      // Number/String没有子节点,不用处理
      case 'NumberLiteral':
      case 'StringLiteral':
        break;
      default:
        throw new TypeError(node.type);
    }

    if (methods && methods.exit) {
      methods.exit(node, parent);
    }
  }
  traverseNode(ast, null);
}

转换transformer

transformer主要是将ast中的数据进行处理获取到新的ast

function transformer(ast) {

  // 创建新ast的根节点
  let newAst = {
    type: 'Program',
    body: [],
  };

  // 将新的ast挂在旧ast的_context上(此方式修改了旧ast结构,存在数据污染行为),这里有点绕的点是下面的操作node._context操作,看着像是操作旧的ast,其实这里_context已经指向新的ast,所以newAst的代码会改变
  ast._context = newAst.body;

  // 执行上面的traverser方法
  traverser(ast, {
  
    NumberLiteral: {
      enter(node, parent) {
        //将number类型的子节点插入parent的_context属性上
        parent._context.push({
          type: 'NumberLiteral',
          value: node.value,
        });
      },
    },

    StringLiteral: {
      enter(node, parent) {
        //将string类型的子节点插入parent的_context属性上
        parent._context.push({
          type: 'StringLiteral',
          value: node.value,
        });
      },
    },

    CallExpression: {
      enter(node, parent) {

        // 这里添加了callee来记录name/type, 
        let expression = {
          type: 'CallExpression',
          callee: {
            type: 'Identifier',
            name: node.name,
          },
          arguments: [],
        };
      
        node._context = expression.arguments;
     
        if (parent.type !== 'CallExpression') {
          // 这里新增了一个type, 它表示的是表达式语句
          expression = {
            type: 'ExpressionStatement',
            expression: expression,
          };
        }
        parent._context.push(expression);
      },
    }
  });
  return newAst;
}

其中newAst = transformer(ast),其结果如下所示,CallExpression的结构有变化,同时多了ExpressionStatement/Identifier类型

 const newAst = {
    type: 'Program',
    body: [{
      type: 'ExpressionStatement',
      expression: {
        type: 'CallExpression',
        callee: {
          type: 'Identifier',
          name: 'add'
        },
        arguments: [{
          type: 'NumberLiteral',
          value: '2'
        }, {
          type: 'CallExpression',
          callee: {
            type: 'Identifier',
            name: 'subtract'
          },
          arguments: [{
            type: 'NumberLiteral',
            value: '4'
          }, {
            type: 'NumberLiteral',
            value: '2'
          }]
        }]
      }
    }]
  };

生成代码

最后是遍历新的newAst,将递归处理里面的类型,并转换成最后机器等能识别或期望的类型结果

function codeGenerator(node) {

  switch (node.type) {

    case 'Program':
      return node.body.map(codeGenerator)
        .join('\n');

    // 表达式添加分号
    case 'ExpressionStatement':
      return (
        codeGenerator(node.expression) +
        ';'
      );
    
    case 'CallExpression':
      return (
        codeGenerator(node.callee) +
        '(' +
        node.arguments.map(codeGenerator)
          .join(', ') +
        ')'
      );
 
    case 'Identifier':
      return node.name;

    case 'NumberLiteral':
      return node.value;

    case 'StringLiteral':
      return '"' + node.value + '"';

    default:
      throw new TypeError(node.type);
  }
}

最后这里的输出结果就是output = add(2, subtract(4, 2)),同时,在代码中能看到的compiler方法

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;
}

其实,它主要就是分如下四部去完成编译的

  1. 根据词法分析将输入字符串变成tokens
  2. 根据语法分析将tokens转换成简单的ast结构
  3. 根据期望的visitor来修改ast结构,生成新的newAst,这里生成的结构和ast网站的结构更相符
  4. 最后将newAst转换成

参考链接

  1. the-super-tiny-compiler(github.com/jamiebuilds…)
  2. ast(astexplorer.net/#)
  3. 编译原理(www.ayqy.net/blog/the-su…