关于编译器

71 阅读3分钟

编译器,是一种电脑程序,会将用某种编程语言写成的源代码,转换成另一种编程语言

最有特点的编译器:babel

编译器的基本思路(compiler)

  1. template 模板进行词法和语法分析,生成 AST (抽象语法树)
  2. AST 转换成附有 JS 语义的 JavaScript AST
  3. 解析 JavaScript AST 生成代码

解析 template 生成 AST

词法分析

将文本分割成一个个的“token”

let x =1;

//let、x、=、1、;、

词法分析生成token的办法有2种:

  1. 使用正则进行词法分析
    需要写大量的正则表达式,正则之间还有冲突需要处理,不容易维护,性能不高,所以正则只适合一些简单的模板语法,真正复杂的语言并不合适。 并且有的语言并不一定自带正则引擎。
  2. 使用有限状态自动机进行词法分析
    在给定有限个输入情况下,将状态转移到最终状态

语法分析

就是将token结合定义语言的语法规格来得到一颗抽象语法树

例子

一个简单的模版:

<template>
  <!-- 这是一段注释 -->  
  <p>{{ msg }}</p>
</template>

这个模板经过解析template生成的 AST 如下:

{  // type 字段,用来标记 AST 节点的类型
  "type": 0,  // 表示根节点
  "children": [  
    {    
      "type": 3,  // 表示注释节点   
      // 补充:若为 2 表示文本节点
      // 针对源代码起点位置的字符不是 < 或者 {{ 的,当做是 文本节点 处理
      "content": " 这是一段注释 ",      
      "loc": {   // loc 代表的是节点对应的代码相关信息,包括代码的起始位置等等  
        "start": {        
          "column": 3,          
          "line": 2,          
          "offset": 3        
        },        
        "end": {        
          "column": 18,          
          "line": 2,          
          "offset": 18        
        },        
        "source": "<!-- 这是一段注释 -->"      
      }    
    },    
    {    
      "type": 1,  // 表示元素节点 
      "ns": 0,      
      "tag": "p",      
      "tagType": 0,      
      "props": [],   // props 描述的是节点的属性
      "isSelfClosing": false,      
      "children": [      
        {        
          "type": 5,  //表示插值节点    
          // 因为当遇到字符串 {{msg}} 的时候,会把当前代码当做是 插值节点 来解析
          "content": {            
            "type": 4,  // 指表达式          
            "isStatic": false,            
            "constType": 0,            
            "content": "msg",            
            "loc": {            
              "start": {              
                "column": 9,                
                "line": 3,                
                "offset": 27              
              },              
              "end": {              
                "column": 12,                
                "line": 3,                
                "offset": 30              
              },              
              "source": "msg"            
            }          
          },          
          "loc": {          
            "start": {            
              "column": 6,              
              "line": 3,              
              "offset": 24            
            },            
            "end": {            
              "column": 15,              
              "line": 3,              
              "offset": 33            
            },            
            "source": "{{ msg }}"          
          }        
        }      
      ],      
      "loc": {      
        "start": {        
          "column": 3,          
          "line": 3,          
          "offset": 21        
        },        
        "end": {        
          "column": 19,          
          "line": 3,          
          "offset": 37        
        },        
        "source": "<p>{{ msg }}</p>"      
      }    
    }  
  ],  
  "helpers": [],  
  "components": [],  
  "directives": [],  
  "hoists": [],  
  "imports": [],  
  "cached": 0,  
  "temps": 0,  
  "loc": {  
    "start": {      
      "column": 1,      
      "line": 1,      
      "offset": 0    
    },    
    "end": {     
      "column": 1,      
      "line": 4,      
      "offset": 38    
    },    
    "source": "\n  <!-- 这是一段注释 -->\n  <p>{{ msg }}</p>\n"  
  }
}

代码转换(Transform)

在得到AST后,我们一般会先将AST转为另一种AST(eg:JS AST),目的是生成更符合预期的AST,这一步称为代码转换。

transform 的目标是为了生成 JavaScript AST。因为渲染函数是一堆 js 代码构成的,编译器最终产物就是渲染函数,所以理想中的 AST 应该是用来描述渲染函数的 JS 代码。

代码生成

编译器的最后阶段是代码生成。有时编译器会做与转换重叠的东⻄,但大部分是代码生成只是意味着取出我们的 AST 和字符串化代码。

实现一个简单的编译器(基础的compiler)

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

  1. 解析 将原始代码转化为更抽象的代码
  2. 转换 采用这种抽象表示(见如下示例)并进行操作
  3. 代码生成 采用转换后的代码表示,并将其转换为新代码。

示例:

LISPC
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 的过程

词法分析

//compiler.js
function tokenizer(input){
  //其实我们的输入就是一段字符串,那我们要拿到token的话,遍历字符串就好
  int current = 0; // 下标
  int 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]; // 获取下一个char
      }
      //这时就得到一个完整的数值
      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 LEFTERS = /[a-z]/i;
    if(LEFTERS.test(char)){
      let value = '';
      //获取操作符 add、subtract...
      while(LEFTERS.test(char)){
        value += char;
        char = input[++current];
      }
      tokens.push({
        type: 'name',
        value,
      });
      continue;
    }
    
    throe new TypeError('I dont know what this character is: ' + char);
  }
  return tokens;
}

moudle.exports = {
  tokenizer,
}

语法分析

//compiler.js
function parser(tokens){
  let current = 0;
  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); 
  }
  let ast = {
    type: 'Program', 
    body: [], 
  };
  
  while (current < tokens.length) { 
    ast.body.push(walk());
  }
  return ast;
}

moudle.exports = {
  tokenizer,
  parser,
}

代码转换

//compiler.js
//visitor模式——观察者模式
function traverser(ast, visitor) {
  function traverseArray(array, parent) { 
    array.forEach(child => {
      traverseNode(child, parent); 
    }); 
  }
  
  function traverseNode(node, parent) {
    let methods = visitor[node.type];
    
    if (methods && methods.enter) { 
      methods.enter(node, parent); 
    }
    
    switch (node.type) {
      case 'Program':
        traverseArray(node.body, node);
        break;
      case 'CallExpression': 
        traverseArray(node.params, node);
        break; 
      case 'NumberLiteral':
      case 'StringLiteral': 
        break;
      
      default:
        throw new TypeError(node.type); 
    } 
    if (methods && methods.exit) { 
      methods.exit(node, parent); 
    }
  }
  traverseNode(ast, null);
}

function transformer(ast) {
  let newAst = {
    type: 'Program', 
    body: [], 
  };
  
  ast._context = newAst.body;
  
  traverser(ast, { 
    NumberLiteral: { 
      enter(node, parent) { 
        parent._context.push({ 
          type: 'NumberLiteral', 
          value: node.value, 
        });
      },
    },
  
    StringLiteral: { 
      enter(node, parent) { 
        parent._context.push({
          type: 'StringLiteral',
          value: node.value, 
        });
      },
    },
  
    CallExpression: { 
      enter(node, parent) {
        let expression = { 
          type: 'CallExpression', 
          callee: { 
            type: 'Identifier',
            name: node.name,
          },
          arguments: [], 
        };
      
        node._context = expression.arguments;
        if (parent.type !== 'CallExpression') {
          expression = {
            type: 'ExpressionStatement',
            expression: expression, 
          }; 
        } 
      
        parent._context.push(expression); 
      },
    },
  });
  return newAst;
}

moudle.exports = {
  tokenizer,
  parser,
  transformer,
}

代码生成

//compiler.js
function codeGenerator(node) { 
  switch (node.type) {
    case 'Program':
      return node.body.map(codeGenerator).join('\n'); 
    case 'ExpressionStatement': 
      return ( 
        codeGenerator(node.expression) + ';' // << (...because we like tocode the *correct* way) 
      ); 
    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); 
  }
}