编译原理(1-编译器工作流)


1.需求分析

  • 实现JSX语法转成JS语法的编译器
  • 需求:将一段JSX语法的代码生成一个AST,并支持遍历和修改这个AST,将AST重新生成JS语法的代码

JSX代码

<h1 id="title"><span>hello</span>world</h1>

JS代码

React.createElement("h1", {
  id: "title"
},React.createElement("span", null, "hello"), "world");

2.编译器工作流

  • 解析(Parsing) 解析是将最初原始的代码转换为一种更为抽象的表示(AST抽象语法树)
  • 转换(Transformation)转换时将对这个对象的表示(AST)做一些处理,让它能做到编译器期望它做的事情
  • 代码生成(Code Generation)接收处理之后的代码表示,然后将它转换成新的代码

2.1解析

解析一般分为两个阶段,词法分析(Lexical Analysis)和语法分析 (Syntactic Analysis)

  • 词法分析 接收原始的代码,然后把他分割成一些被称为token的片段,这个过程是再词法分析器中完成的(Tokenizer或Lexer),使用状态机分词
  • Token是一个数组,由一些代码语句的碎片组成,它们可以是数字、标签、标点符号、运算符或其他东西组成
  • 语法分析接收之前生成的token,把他们转换成一种抽象的表示,这种表述描述了代码语句中的每一个片段以及他们之间的关系。这被称为中间表示,或者抽象语法树(Abatract Syntas Tree,缩写AST)
  • 抽象语法树是一个嵌套程度很深的对象,用一种更容易处理的方式代表了代码本身,也能给我们更多信息

原始 jsx 代码

<h1 id="title"><span>hello</span>world</h1>

AST和tokens

Module {
  type: 'Program',
  body: [
    ExpressionStatement {
      type: 'ExpressionStatement',
      expression: [JSXElement]
    }
  ],
  sourceType: 'module',
  tokens: [
    { type: 'Punctuator', value: '<' },
    { type: 'JSXIdentifier', value: 'h1' },
    { type: 'JSXIdentifier', value: 'id' },
    { type: 'Punctuator', value: '=' },
    { type: 'String', value: '"title"' },
    { type: 'Punctuator', value: '>' },
    { type: 'Punctuator', value: '<' },
    { type: 'JSXIdentifier', value: 'span' },
    { type: 'Punctuator', value: '>' },
    { type: 'JSXText', value: 'hello' },
    { type: 'Punctuator', value: '<' },
    { type: 'Punctuator', value: '/' },
    { type: 'JSXIdentifier', value: 'span' },
    { type: 'Punctuator', value: '>' },
    { type: 'JSXText', value: 'world' },
    { type: 'Punctuator', value: '<' },
    { type: 'Punctuator', value: '/' },
    { type: 'JSXIdentifier', value: 'h1' },
    { type: 'Punctuator', value: '>' }
  ]
}

2.2 遍历(Traversal)

  • 为了能处理所有的结点,我们需要遍历它们,使用的是深度优先遍历
  • 对于上面的 AST 的遍历流程是这样的

代码实现

let esprima = require('esprima'); // AST转换工具
let estraverse = require('estraverse-fb'); // 深度优先遍历工具
let sourceCode = `<h1 id="title"><span>hello</span>world</h1>`;
// jsx解析为AST,支持jsx,带token
let ast = esprima.parseModule(sourceCode,{jsx:true,tokens:true}); 
console.log(ast);

let ident = 0; // 缩进
function padding(){
    return ' '.repeat(ident);
}
//visitor访问者 访问器  深度优先遍历
estraverse.traverse(ast,{  // 访问器
  enter(node){
    console.log(padding()+node.type+'进入');
    ident+=2;
  },
  leave(node){
    ident-=2;
    console.log(padding()+node.type+'离开');
  }
});

访问器 运行代码输出结果

Program进入
  ExpressionStatement进入
    JSXElement进入
      JSXOpeningElement进入
        JSXIdentifier进入
        JSXIdentifier离开
        JSXAttribute进入
          JSXIdentifier进入
          JSXIdentifier离开
          Literal进入
          Literal离开
        JSXAttribute离开
      JSXOpeningElement离开
      JSXClosingElement进入
        JSXIdentifier进入
        JSXIdentifier离开
      JSXClosingElement离开
      JSXElement进入
        JSXOpeningElement进入
          JSXIdentifier进入
          JSXIdentifier离开
        JSXOpeningElement离开
        JSXClosingElement进入
          JSXIdentifier进入
          JSXIdentifier离开
        JSXClosingElement离开
        JSXText进入
        JSXText离开
      JSXElement离开
      JSXText进入
      JSXText离开
    JSXElement离开
  ExpressionStatement离开
Program离开

2.3 转换(Transformation)

  • 编译器的下一步就是转换,它只是把 AST 拿过来然后对它做一些修改.它可以在同种语言下操作 AST,也可以把 AST 翻译成全新的语言
  • 你或许注意到了我们的 AST 中有很多相似的元素,这些元素都有type 属性,它们被称为 AST结点。这些结点含有若干属性,可以用于描述 AST 的部分信息
  • 当转换 AST 的时候我们可以添加、移动、替代这些结点,也可以根据现有的 AST 生成一个全新的 AST
  • 既然我们编译器的目标是把输入的代码转换为一种新的语言,所以我们将会着重于产生一个针对新语言的全新的 AST

2.4 代码生成(Code Generation)

  • 编译器的最后一个阶段是代码生成,这个阶段做的事情有时候会和转换(transformation)重叠,但是代码生成最主要的部分还是根据 AST 来输出代码
  • 代码生成有几种不同的工作方式,有些编译器将会重用之前生成的 token,有些会创建独立的代码表示,以便于线性地输出代码。但是接下来我们还是着重于使用之前生成好的 AST
  • 我们的代码生成器需要知道如何打印AST 中所有类型的结点,然后它会递归地调用自身,直到所有代码都被打印到一个很长的字符串中

文章作者: 何不去高处
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 何不去高处 !