浅谈抽象语法树

36 阅读2分钟

抽象语法树,简称:AST,全称:Abstract Asytax Tree
地址:https://astexplorer.net/

1680095684321.jpg

在上述的编译器中编写一个方法,对应的可以看见它生成的一个树状结构的数据,我们称之为抽象语法树,当然这只是JavaScript的AST。

在前端开发过程,应该听说过或者使用babel,eslint等,本文在这里就不过多赘述。本文的在这边介绍一款简易的AST;
github地址:https://github.com/jamiebuilds/the-super-tiny-compiler
当然有一定知识储备的兄弟,还是建议去看源码。

1680139418646.jpg

(add 2 (subtract 4 2))为例:

  • [ 词法分析 ]
[
      { 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: ')'        },
]
  • [ 语法分析 ]
{
    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',
         }]
       }]
     }]
}

语法分析之后就已经生成一个简易的AST了。但是还是没有结束,如果我想在分析完成之后,把4转化成xxx怎么办?所以还有下一步

  • [ 转换处理 ]
var visitor = {
     NumberLiteral(node, parent) {
         if(node.value==='4'){
             //处理xxx
         }
     },
     CallExpression(node, parent) {},
};

它的进出方式走完一个节点,进入下一个节点,一进一出的方式

  -> Program (enter)
     -> CallExpression (enter)
       -> Number Literal (enter)
       <- Number Literal (exit)
       -> Call Expression (enter)
          -> Number Literal (enter)
          <- Number Literal (exit)
          -> Number Literal (enter)
          <- Number Literal (exit)
       <- CallExpression (exit)
     <- CallExpression (exit)
   <- Program (exit)
*   Original AST                     |   Transformed AST
* ----------------------------------------------------------------------------
*   {                                |   {
*     type: 'Program',               |     type: 'Program',
*     body: [{                       |     body: [{
*       type: 'CallExpression',      |       type: 'ExpressionStatement',
*       name: 'add',                 |       expression: {
*       params: [{                   |         type: 'CallExpression',
*         type: 'NumberLiteral',     |         callee: {
*         value: '2'                 |           type: 'Identifier',
*       }, {                         |           name: 'add'
*         type: 'CallExpression',    |         },
*         name: 'subtract',          |         arguments: [{
*         params: [{                 |           type: 'NumberLiteral',
*           type: 'NumberLiteral',   |           value: '2'
*           value: '4'               |         }, {
*         }, {                       |           type: 'CallExpression',
*           type: 'NumberLiteral',   |           callee: {
*           value: '2'               |             type: 'Identifier',
*         }]                         |             name: 'subtract'
*       }]                           |           },
*     }]                             |           arguments: [{
*   }                                |             type: 'NumberLiteral',
*                                    |             value: '4'
* ---------------------------------- |           }, {
*                                    |             type: 'NumberLiteral',
*                                    |             value: '2'
*                                    |           }]
*  (sorry the other one is longer.)  |         }
*                                    |       }
*                                    |     }]
*                                    |   }

对比处理前和处理后,发现AST的内容进行丰富

  • [ 代码生成 ]
    输出的结果是预期可运行的结构

其实babel的大体结构也是如此,遍历解析(词法分析、语法分析),遍历转换,最后代码生成

graph TD
Parse -->  Transform --> Generator

总结一下,其实我们写的代码,严格意义上只是字符串,各种框架脚本之所以能够运行,是通过既定的规则解析,把它转化为AST,最后根据宿主环境生成可运行的代码。前端的跨端也是如此。