ast编译原理-1

Posted coderlin_

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ast编译原理-1相关的知识,希望对你有一定的参考价值。

编译原理

  • 实现jsx语法转成js语法的编译器。
    如将
<h1 id=“title”><span>hello</span>world</h1>

转成

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

步骤差不多就是,jsx代码->ast -> 处理转换ast -> ast生成js代码 -> js代码

编译器工作流

  • 解析(parse),将原始代码转为更加抽象的抽象语法树(AST)
  • 转换(transform), 对抽象语法树机进行处理
  • 代码生成(code generation)接收处理之后的代码表示,把他转为新的代码。

解析

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

  • 词法分析:接收源代码,将他分割成为token,这个过程在词法分析器完成。
  • token是一个数组,由一些代码语句的语句组成,可以是数字,标签,标点符号,运算符等等。
  • 语法分析接受token,转成更加抽象的表示,这种抽象的表示描述了代码语句中的每一个片段和他们之间的关系,称为AST,抽象语法树。
  • 抽象语法树是一个嵌套成都很深的对象,用一种容易处理的方式代表了代码本身。

比如下面的jsx代码

<h1 id=“title”><span>hello</span>world</h1>

转成tokens就差不多是

[
	type: "Punctuator', value: '<',
	type: 'JSXIdentifier', value: 'h1',
	.....
]

语法树就差不多是

一个树级结构。

使用esprima看看


打印结果

ast 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: '>' 
  ]

expirma内部要得到ast,

  • 1 需要将源代码进行分析,得到tokens数组。
  • 2 接着才能将tokens数组转成抽象语法树。

基本把js拆开了。

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

遍历

  • 对于能处理的所有节点,需要遍历他们,使用深度优先遍历。

使用estraverse遍历

const estraverse = require('estraverse-fb')

let indent = 0
function padding()
    return ' '.repeat(indent)


//深度优先遍历
estraverse.traverse(ast, 
    enter(node)
        console.log(padding() + node.type + '进入');
        indent+=2
    ,
    leave(node)
        console.log(padding() + node.type + '离开');
        indent-=2
    
)

结果

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离开

转换

当我们转化ast并且可以遍历后,下一步就是转换了。

  • 转换就是对ast,进行遍历并且修改,操作ast,可以在同种语言下操作ast,也可以把ast翻译成全新的语言。
  • ast有很多相似的元素,每个元素都有type属性,称为ast节点,他们都有若干属性,用来描述ast节点的信息,如
  • 当转换ast的时候,可以添加,移动,删除这些节点。
  • 最终将ast转换成我们想要的样式

代码生成

  • 编译器的最后一个阶段就是代码生成,就是将转换后的ast生成代代码。

这就是编译器的工作流:解析-》转换-》代码生成。

有限状态机

有限状态机( FSM),是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。

  • 每一个状态都是一个机器,每个机器都可以接收输入和计算输出
  • 机器本身没有状态,每一个机器会根据输入决定下一个状态

通过状态机进行分词操作

分词 10+20
代码

/**
 * 分词
 * 状态机实现
 */

let tokens = [];

let NUMBERS = /[0-9]/;
//类型
const types = 
  Numeric: "Numeric",
  Punctuator: "Punctuator",
;


/**
 * start表示开始状态函数
 * 接受一个字符,返回下一个状态函数
 */
let currentToken;
function start(char) 
  //第一个是1
  if (NUMBERS.test(char)) 
    //如果是数字,就生成新的token
    currentToken =  type: types.Numeric, value: "" ;
  
  //进入下一个状态,收集number状态
  return number(char);


/**
 * number表示number状态的函数
 */
function number(char) 
  if (NUMBERS.test(char)) 
    // 当前char是number
    currentToken.value += char;
    //推断下一个还是Number状态
    return number;
   else if (char === "+" || char === "-") 
    emit(currentToken);
    //将这个+/-也收集起来
    emit( type: types.Punctuator, value: char );
    currentToken =  type: types.Numeric, value: "" ;
    return number; //继续准备收集number
  


/**
 *
 */
function emit(token) 
  //当遇到+,-的时候,就表示一个token结束了,该收集了。
  tokens.push(token);
  currentToken =  type: "", value: "" ;


function tokenizer(input) 
  let state = start;
  for (let char of input) 
    state = state(char);
  
  if(currentToken.value.length > 0)
      emit(currentToken)
  


tokenizer("10+20+30-10");
console.log(tokens);

// 打印结果是
[
   type: 'Numeric', value: '10' ,
   type: 'Punctuator', value: '+' ,
   type: 'Numeric', value: '20' ,
   type: 'Punctuator', value: '+' ,
   type: 'Numeric', value: '30' ,
   type: 'Punctuator', value: '-' ,
   type: 'Numeric', value: '10' 
]

成功分词,这就是状态机的原理。比如一开始的start函数,然后第一个char是number,下一个运行的函数就变成了number函数。当前的状态根据当前的char决定下一个机器怎么处理。

实现编译器

转换ast

第一步,词法分析器

以上是关于ast编译原理-1的主要内容,如果未能解决你的问题,请参考以下文章

[Vue源码]一起来学Vue模板编译原理-AST生成Render字符串

JavaScript的工作原理:解析抽象语法树(AST)+ 提升编译速度5个技巧

golang编译原理

Vue.js 模板解析器原理

编译原理构建一个简单的解释器(Let’s Build A Simple Interpreter. Part 7.)(笔记)解释器 interpreter 解析器 parser 抽象语法树AST(代码片

iOS面试:编译原理