AST简介

108 阅读5分钟

1.需求分析

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

JXS代码

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

JS代码

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

2.编译器工作流

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

2.1 解析(Parsing)

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

原始jsx代码

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

生成的tokens

Punctuator 标点

JSXIdentifier jsx标识符

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

可以通过AST explorer

可以看到是AST就是一颗带有各种标记的树,type,attributes,name,value等属性组成
{
  "type": "Program",
  "body": [
    {
      "type": "ExpressionStatement",
      "expression": {
        "type": "JSXElement",
        "openingElement": {
          "type": "JSXOpeningElement",
          "name": {
            "type": "JSXIdentifier",
            "name": "h1",
            "range": [
              180,
              182
            ]
          },
          "selfClosing": false,
          "attributes": [
            {
              "type": "JSXAttribute",
              "name": {
                "type": "JSXIdentifier",
                "name": "id",
                "range": [
                  183,
                  185
                ]
              },
              "value": {
                "type": "Literal",
                "value": "title",
                "raw": "'title'",
                "range": [
                  186,
                  193
                ]
              },
              "range": [
                183,
                193
              ]
            }
          ],
          "range": [
            179,
            194
          ]
        },
        "children": [
          {
            "type": "JSXElement",
            "openingElement": {
              "type": "JSXOpeningElement",
              "name": {
                "type": "JSXIdentifier",
                "name": "span",
                "range": [
                  195,
                  199
                ]
              },
              "selfClosing": false,
              "attributes": [],
              "range": [
                194,
                200
              ]
            },
       ------------------
}

示例代码,根据esprima和estraverse来遍历生成AST树

let esprima = require('esprima')
let estraverse = require('estraverse-fb')
let sourceCode = `<h1 id='title'><span>hello</span>world</h1>`
let ast = esprima.parseScript(sourceCode, { jsx: true, tokens: true })
console.log(ast.tokens)
let ident = 0

function padding() {
    return ' '.repeat(ident)
}

estraverse.traverse(ast, {
    enter(node) {
        console.log(padding() + node.type + '进入')
        ident += 2
    },
    leave(node) {
        ident -= 2
        console.log(padding() + node.type + '离开')
    }
})

2.2 转换

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

2.3 代码生成

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

3.实现编译器

3.1 词法分析器

  • 我们只是接受代码组成的字符串,然后把他们分割成token组成的数组
  • 实现思路,根据不同的状态,然后调用不同的方法
3.1.1 tokenizer.js

const LETTERS = /[a-zA-Z1-6]/;  // 匹配标签元素 span h1等

let sourceCode = '<h1 id="title"><span>hello</span>world</h1>'
let tokens = []
let currentToken = { type: '', value: '' }
function emit(token) {
    tokens.push(token)
    currentToken = { type: '', value: '' }
}
function EOF() {
    if (currentToken.value.length > 0) {
        emit(currentToken);
    }
}
const token = tokenize(sourceCode)
console.log("🚀 ~ token:", token)
function tokenize(input) {
    let state = start // 当前状态是什么,是文本还是标签,还是<等
    for (const char of input) {
        if (state) state = state(char)
    }
    EOF();  // 到了终止状态,判断currentToken是否还有内容
    return tokens
}
function start(char) {
    if (char === '<') { //开始状态
        emit({ type: 'LeftParenthese', value: '<' })
        return foundLeftParentheses //当前找到了<,那么进入左尖括号状态
    }

    throw new Error('第一个字符必须是<')
}

function foundLeftParentheses(char) {
    if (LETTERS.test(char)) { //匹配到了开始标签h a s等标签元素开头
        currentToken.type = 'Identifier'
        currentToken.value += char
        return identifier
    } else { //匹配到了结束标签
        emit({ type: 'BackSlash', value: '/' })
        return foundLeftParentheses
    }
}

function attribute(char) {
    if (LETTERS.test(char)) { //不考虑多个空格的情况
        currentToken.type = 'AttributeKey'
        currentToken.value += char
        return attributeKey
    }
}
function identifier(char) {
    if (LETTERS.test(char)) {
        currentToken.value += char
        return identifier
    } else if (char === ' ') { //标签获取完毕,下一个可能是attribute
        emit(currentToken)
        return attribute
    } else if (char === '>') {
        emit(currentToken)
        emit({ type: "RightParentheses", value: '>' })
        return foundRightParentheses
    }
}

function attributeKey(char) {
    if (LETTERS.test(char)) {
        currentToken.value += char
        return attributeKey
    } else if (char === '=') {
        emit(currentToken) //判断key走完了,现在走value
        return attributeValue
    }
}

function attributeValue(char) {
    if (char === '"' || char === "'") {
        currentToken.type = 'AttributeValue'
        currentToken.value = ""
        return attributeStringValue //将引号拿到,去匹配属性值
    }
    throw new Error('属性值没有用引号包裹')
}

function attributeStringValue(char) {
    if (LETTERS.test(char)) {
        currentToken.value += char
        return attributeStringValue
    } else if (char === '"' || char === '"') { //读完属性了
        emit(currentToken)
        return tryLeaveAttribute //离开属性
    }
}


function tryLeaveAttribute(char) {
    if (char === ' ') { //说明后面是新属性 
        return attribute
    } else if (char === '>') { //说明读完了属性
        emit({ type: 'RightParentheses', value: ">" })
        return foundRightParentheses
    } else {
        throw new Error('必须以>结束')
    }
}

function foundRightParentheses(char) {
    if (char === '<') { //可能是下一个标签的开始
        emit({ type: 'LeftParenthese', value: "<" })
        return foundLeftParentheses
    } else { //如果不是下一个标签开始,那么就当成文本处理
        currentToken.type = 'Text'
        currentToken.value += char
        return text

    }
}

function text(char) {
    if (char === '<') {
        emit(currentToken)
        emit({ type: "LeftParenthese", value: "<" });
        return foundLeftParentheses;
    } else {
        currentToken.value += char;
        return text
    }
}