AST 抽象语法树

提起 AST 抽象语法树,大家可能并不感冒。但是提到它的使用场景,也许会让你大吃一惊。原来它一直在你左右与你相伴,而你却不知。

一、什么是抽象语法树

在计算机科学中,抽象语法树(abstract syntax tree 或者缩写为 AST),或者语法树(syntax tree),是源代码的抽象语法结构的树状表现形式,这里特指编程语言的源代码。树上的每个节点都表示源代码中的一种结构。

之所以说语法是「抽象」的,是因为这里的语法并不会表示出真实语法中出现的每个细节。

二、使用场景

  • JS 反编译,语法解析
  • Babel 编译 ES6 语法
  • 代码高亮
  • 关键字匹配
  • 作用域判断
  • 代码压缩

三、AST Explorer


我们来看一个 ES6 的解释器,声明如下的代码:

1 let tips = [
2   "Jartto‘s AST Demo"
3 ];

看看是如何解析的, JSON 格式如下:

 1 {
 2   "type": "Program",
 3   "start": 0,
 4   "end": 38,
 5   "body": [
 6     {
 7       "type": "VariableDeclaration",
 8       "start": 0,
 9       "end": 37,
10       "declarations": [
11         {
12           "type": "VariableDeclarator",
13           "start": 4,
14           "end": 36,
15           "id": {
16             "type": "Identifier",
17             "start": 4,
18             "end": 8,
19             "name": "tips"
20           },
21           "init": {
22             "type": "ArrayExpression",
23             "start": 11,
24             "end": 36,
25             "elements": [
26               {
27                 "type": "Literal",
28                 "start": 15,
29                 "end": 34,
30                 "value": "Jartto‘s AST Demo",
31                 "raw": "\"Jartto‘s AST Demo\""
32               }
33             ]
34           }
35         }
36       ],
37       "kind": "let"
38     }
39   ],
40   "sourceType": "module"
41 }

而它的语法树大概如此:

每个结构都看的清清楚楚,这时候我们会发现,这和 Dom 树真的差不了多少。再来看一个例子:

1 (1+2)*3

AST Tree:

我们删掉括号,看看规则是如何变化的?JSON 格式会一目了然:

 1 {
 2   "type": "Program",
 3   "start": 0,
 4   "end": 6,
 5   "body": [
 6     {
 7       "type": "ExpressionStatement",
 8       "start": 0,
 9       "end": 5,
10       "expression": {
11         "type": "BinaryExpression",
12         "start": 0,
13         "end": 5,
14         "left": {
15           "type": "Literal",
16           "start": 0,
17           "end": 1,
18           "value": 1,
19           "raw": "1"
20         },
21         "operator": "+",
22         "right": {
23           "type": "BinaryExpression",
24           "start": 2,
25           "end": 5,
26           "left": {
27             "type": "Literal",
28             "start": 2,
29             "end": 3,
30             "value": 2,
31             "raw": "2"
32           },
33           "operator": "*",
34           "right": {
35             "type": "Literal",
36             "start": 4,
37             "end": 5,
38             "value": 3,
39             "raw": "3"
40           }
41         }
42       }
43     }
44   ],
45   "sourceType": "module"
46 }

可以看出来,(1+2)*3 和 1+2*3,语法树是有差别的:
1.在确定类型为 ExpressionStatement 后,它会按照代码执行的先后顺序,将表达式 BinaryExpression 分为 Leftoperator 和 right 三块;
2.每块标明了类型,起止位置,值等信息;
3.操作符类型;

再来看看我们最常用的箭头函数:

1 const mytest = (a,b) => {
2   return a+b;
3 }

JSON 格式如下:

 1 {
 2   "type": "Program",
 3   "start": 0,
 4   "end": 42,
 5   "body": [
 6     {
 7       "type": "VariableDeclaration",
 8       "start": 0,
 9       "end": 41,
10       "declarations": [
11         {
12           "type": "VariableDeclarator",
13           "start": 6,
14           "end": 41,
15           "id": {
16             "type": "Identifier",
17             "start": 6,
18             "end": 12,
19             "name": "mytest"
20           },
21           "init": {
22             "type": "ArrowFunctionExpression",
23             "start": 15,
24             "end": 41,
25             "id": null,
26             "expression": false,
27             "generator": false,
28             "params": [
29               {
30                 "type": "Identifier",
31                 "start": 16,
32                 "end": 17,
33                 "name": "a"
34               },
35               {
36                 "type": "Identifier",
37                 "start": 18,
38                 "end": 19,
39                 "name": "b"
40               }
41             ],
42             "body": {
43               "type": "BlockStatement",
44               "start": 24,
45               "end": 41,
46               "body": [
47                 {
48                   "type": "ReturnStatement",
49                   "start": 28,
50                   "end": 39,
51                   "argument": {
52                     "type": "BinaryExpression",
53                     "start": 35,
54                     "end": 38,
55                     "left": {
56                       "type": "Identifier",
57                       "start": 35,
58                       "end": 36,
59                       "name": "a"
60                     },
61                     "operator": "+",
62                     "right": {
63                       "type": "Identifier",
64                       "start": 37,
65                       "end": 38,
66                       "name": "b"
67                     }
68                   }
69                 }
70               ]
71             }
72           }
73         }
74       ],
75       "kind": "const"
76     }
77   ],
78   "sourceType": "module"
79 }

AST Tree 结构如下图:

我们注意到了,增加了几个新的字眼:

  • ArrowFunctionExpression
  • BlockStatement
  • ReturnStatement

到这里,其实我们已经慢慢明白了:

抽象语法树其实就是将一类标签转化成通用标识符,从而结构出的一个类似于树形结构的语法树。

四、深入原理

可视化的工具可以让我们迅速有感官认识,那么具体内部是如何实现的呢?

继续使用上文的例子:

1 Function getAST(){}

JSON 也很简单:

 1 {
 2   "type": "Program",
 3   "start": 0,
 4   "end": 19,
 5   "body": [
 6     {
 7       "type": "FunctionDeclaration",
 8       "start": 0,
 9       "end": 19,
10       "id": {
11         "type": "Identifier",
12         "start": 9,
13         "end": 15,
14         "name": "getAST"
15       },
16       "expression": false,
17       "generator": false,
18       "params": [],
19       "body": {
20         "type": "BlockStatement",
21         "start": 17,
22         "end": 19,
23         "body": []
24       }
25     }
26   ],
27   "sourceType": "module"
28 }

怀着好奇的心态,我们来模拟一下用代码实现:

 1 const esprima = require(‘esprima‘); //解析js的语法的包
 2 const estraverse = require(‘estraverse‘); //遍历树的包
 3 const escodegen = require(‘escodegen‘); //生成新的树的包
 4 let code = `function getAST(){}`;
 5 //解析js的语法
 6 let tree = esprima.parseScript(code);
 7 //遍历树
 8 estraverse.traverse(tree, {
 9   enter(node) {
10     console.log(‘enter: ‘ + node.type);
11   },
12   leave(node) {
13     console.log(‘leave: ‘ + node.type);
14   }
15 });
16 //生成新的树
17 let r = escodegen.generate(tree);
18 console.log(r);

运行后,输出:

 1 enter: Program
 2 enter: FunctionDeclaration
 3 enter: Identifier
 4 leave: Identifier
 5 enter: BlockStatement
 6 leave: BlockStatement
 7 leave: FunctionDeclaration
 8 leave: Program
 9 function getAST() {
10 }

我们看到了遍历语法树的过程,这里应该是深度优先遍历。

稍作修改,我们来改变函数的名字 getAST => Jartto

 1 const esprima = require(‘esprima‘); //解析js的语法的包
 2 const estraverse = require(‘estraverse‘); //遍历树的包
 3 const escodegen = require(‘escodegen‘); //生成新的树的包
 4 let code = `function getAST(){}`;
 5 //解析js的语法
 6 let tree = esprima.parseScript(code);
 7 //遍历树
 8 estraverse.traverse(tree, {
 9   enter(node) {
10     console.log(‘enter: ‘ + node.type);
11     if (node.type === ‘Identifier‘) {
12       node.name = ‘Jartto‘;
13     }
14   }
15 });
16 //生成新的树
17 let r = escodegen.generate(tree);
18 console.log(r);

运行后,输出:

1 enter: Program
2 enter: FunctionDeclaration
3 enter: Identifier
4 enter: BlockStatement
5 function Jartto() {
6 }

可以看到,在我们的干预下,输出的结果发生了变化,方法名编译后方法名变成了 Jartto

这就是抽象语法树的强大之处,本质上通过编译,我们可以去改变任何输出结果。

补充一点:关于 node 类型,全集大致如下:

(parameter) node: Identifier | SimpleLiteral | RegExpLiteral | Program | FunctionDeclaration | FunctionExpression | ArrowFunctionExpression | SwitchCase | CatchClause | VariableDeclarator | ExpressionStatement | BlockStatement | EmptyStatement | DebuggerStatement | WithStatement | ReturnStatement | LabeledStatement | BreakStatement | ContinueStatement | IfStatement | SwitchStatement | ThrowStatement | TryStatement | WhileStatement | DoWhileStatement | ForStatement | ForInStatement | ForOfStatement | VariableDeclaration | ClassDeclaration | ThisExpression | ArrayExpression | ObjectExpression | YieldExpression | UnaryExpression | UpdateExpression | BinaryExpression | AssignmentExpression | LogicalExpression | MemberExpression | ConditionalExpression | SimpleCallExpression | NewExpression | SequenceExpression | TemplateLiteral | TaggedTemplateExpression | ClassExpression | MetaProperty | AwaitExpression | Property | AssignmentProperty | Super | TemplateElement | SpreadElement | ObjectPattern | ArrayPattern | RestElement | AssignmentPattern | ClassBody | MethodDefinition | ImportDeclaration | ExportNamedDeclaration | ExportDefaultDeclaration | ExportAllDeclaration | ImportSpecifier | ImportDefaultSpecifier | ImportNamespaceSpecifier | ExportSpecifier

说到这里,聪明的你,可能想到了 Babel,想到了 js 混淆,想到了更多背后的东西。接下来,我们要介绍介绍 Babel 是如何将 ES6 转成 ES5 的。

五、关于 Babel

由于 ES6 的兼容问题,很多情况下,我们都在使用 Babel 插件来进行编译,那么有没有想过 Babel 是如何工作的呢?先来看看:

1 let sum = (a, b)=>{return a+b};

AST 大概如此:

JSON 格式可能会看的清楚些:

 1 {
 2   "type": "Program",
 3   "start": 0,
 4   "end": 31,
 5   "body": [
 6     {
 7       "type": "VariableDeclaration",
 8       "start": 0,
 9       "end": 31,
10       "declarations": [
11         {
12           "type": "VariableDeclarator",
13           "start": 4,
14           "end": 30,
15           "id": {
16             "type": "Identifier",
17             "start": 4,
18             "end": 7,
19             "name": "sum"
20           },
21           "init": {
22             "type": "ArrowFunctionExpression",
23             "start": 10,
24             "end": 30,
25             "id": null,
26             "expression": false,
27             "generator": false,
28             "params": [
29               {
30                 "type": "Identifier",
31                 "start": 11,
32                 "end": 12,
33                 "name": "a"
34               },
35               {
36                 "type": "Identifier",
37                 "start": 14,
38                 "end": 15,
39                 "name": "b"
40               }
41             ],
42             "body": {
43               "type": "BlockStatement",
44               "start": 18,
45               "end": 30,
46               "body": [
47                 {
48                   "type": "ReturnStatement",
49                   "start": 19,
50                   "end": 29,
51                   "argument": {
52                     "type": "BinaryExpression",
53                     "start": 26,
54                     "end": 29,
55                     "left": {
56                       "type": "Identifier",
57                       "start": 26,
58                       "end": 27,
59                       "name": "a"
60                     },
61                     "operator": "+",
62                     "right": {
63                       "type": "Identifier",
64                       "start": 28,
65                       "end": 29,
66                       "name": "b"
67                     }
68                   }
69                 }
70               ]
71             }
72           }
73         }
74       ],
75       "kind": "let"
76     }
77   ],
78   "sourceType": "module"
79 }

结构大概如此,那我们再用代码模拟一下:

 1 const babel = require(‘babel-core‘); //babel核心解析库
 2 const t = require(‘babel-types‘); //babel类型转化库
 3 let code = `let sum = (a, b)=>{return a+b}`;
 4 let ArrowPlugins = {
 5 //访问者模式
 6 visitor: {
 7   //捕获匹配的API
 8     ArrowFunctionExpression(path) {
 9       let { node } = path;
10       let body = node.body;
11       let params = node.params;
12       let r = t.functionExpression(null, params, body, false, false);
13       path.replaceWith(r);
14     }
15   }
16 }
17 let d = babel.transform(code, {
18   plugins: [
19     ArrowPlugins
20   ]
21 })
22 console.log(d.code);

记得安装 babel-corebabel-types 这俩插件,之后运行 babel.js,我们看到了这样的输出:

1 let sum = function (a, b) {
2   return a + b;
3 };

这里,我们完美的将箭头函数转换成了标准函数。

那么问题又来了,如果是简写呢,像这样,还能正常编译吗?

1 let sum = (a, b)=>a+b

Body 部分的结构发生了变化,所以,我们的 babel.js 运行就会报错了。

TypeError: unknown: Property body of FunctionExpression expected node to be of a type ["BlockStatement"] but instead got "BinaryExpression"

意思很明了,我们的 body 类型变成 BinaryExpression 不再是 BlockStatement,所以需要做一些修改:

 1 const babel = require(‘babel-core‘); //babel核心解析库
 2 const t = require(‘babel-types‘); //babel类型转化库
 3 let code = `let sum = (a, b)=> a+b`;
 4 let ArrowPlugins = {
 5 //访问者模式
 6   visitor: {
 7   //捕获匹配的API
 8     ArrowFunctionExpression(path) {
 9       let { node } = path;
10       let params = node.params;
11       let body = node.body;
12       if(!t.isBlockStatement(body)){
13         let returnStatement = t.returnStatement(body);
14         body = t.blockStatement([returnStatement]);
15       }
16       let r = t.functionExpression(null, params, body, false, false);
17       path.replaceWith(r);
18     }
19   }
20 }
21 let d = babel.transform(code, {
22   plugins: [
23     ArrowPlugins
24   ]
25 })
26 console.log(d.code);

看看输出结果:

1 let sum = function (a, b) {
2   return a + b;
3 };

看起来不错,堪称完美~

六、深入 Babel

当然,上文我们简单演示了 Babel 是如何来编译代码的,但是并非简单如此。

Babel 使用一个基于 ESTree 并修改过的 AST,它的内核说明文档可以在这里找到。

正如我们上面示例代码一样,Babel 的三个主要处理步骤分别是: 解析(parse),转换(transform),生成(generate)。

1.解析(parse):解析步骤接收代码并输出 AST。 这个步骤分为两个阶段:词法分析 Lexical Analysis 和语法分析Syntactic Analysis

  • 词法分析:词法分析阶段把字符串形式的代码转换为令牌(tokens) 流。你可以把令牌看作是一个扁平的语法片段数组:
  • n * n;
  • 例如上面的代码片段,解析结果如下:
    [
      { type: { ... }, value: "n", start: 0, end: 1, loc: { ... } },
      { type: { ... }, value: "*", start: 2, end: 3, loc: { ... } },
      { type: { ... }, value: "n", start: 4, end: 5, loc: { ... } },
      ...
    ]

    每一个 type 有一组属性来描述该令牌,和 AST 节点一样它们也有 startendloc 属性:

    {
      type: {
        label: ‘name‘,
        keyword: undefined,
        beforeExpr: false,
        startsExpr: true,
        rightAssociative: false,
        isLoop: false,
        isAssign: false,
        prefix: false,
        postfix: false,
        binop: null,
        updateContext: null
      },
      ...
    }
  • 语法分析:语法分析阶段会把一个令牌流转换成 AST 的形式。 这个阶段会使用令牌中的信息把它们转换成一个 AST 的表述结构,这样更易于后续的操作。

2.转换(transform):接收 AST 并对其进行遍历,在此过程中对节点进行添加、更新及移除等操作。 这是 Babel 或是其他编译器中最复杂的过程,同时也是插件将要介入工作的部分。

3.生成(generate):代码生成步骤把最终(经过一系列转换之后)的 AST 转换成字符串形式的代码,同时还会创建源码映射(source maps)。

代码生成其实很简单:深度优先遍历整个 AST,然后构建可以表示转换后代码的字符串。

了解这这些过程,我们回头再来参悟一下之前的示例代码:

 1 const babel = require(‘babel-core‘); //babel核心解析库
 2 const t = require(‘babel-types‘); //babel类型转化库
 3 let code = `let sum = (a, b)=>{return a+b}`;
 4 let ArrowPlugins = {
 5 //访问者模式
 6   visitor: {
 7   //捕获匹配的API
 8     ArrowFunctionExpression(path) {
 9       let { node } = path;
10       let body = node.body;
11       let params = node.params;
12       let r = t.functionExpression(null, params, body, false, false);
13       path.replaceWith(r);
14     }
15   }
16 }
17 let d = babel.transform(code, {
18   plugins: [
19     ArrowPlugins
20   ]
21 })
22 console.log(d.code);

是不是发现突然简单易懂了。

七、关于遍历

想要转换 AST 你需要进行递归的树形遍历

比方说我们有一个 FunctionDeclaration 类型。它有几个属性:idparams,和 body,每一个都有一些内嵌节点。

 1 {
 2   type: "FunctionDeclaration",
 3   id: {
 4     type: "Identifier",
 5     name: "square"
 6   },
 7   params: [{
 8     type: "Identifier",
 9     name: "n"
10   }],
11   body: {
12     type: "BlockStatement",
13     body: [{
14       type: "ReturnStatement",
15       argument: {
16         type: "BinaryExpression",
17         operator: "*",
18         left: {
19           type: "Identifier",
20           name: "n"
21         },
22         right: {
23           type: "Identifier",
24           name: "n"
25         }
26       }
27     }]
28   }
29 }

按照上面的代码结构,我们来说一下具体流程:
1.首先我们从 FunctionDeclaration 开始并且我们知道它的内部属性(即:idparamsbody),所以我们依次访问每一个属性及它们的子节点;

2.然后我们来到 id,它是一个 IdentifierIdentifier 没有任何子节点属性,所以我们继续;

3.紧接着是 params,由于它是一个数组节点所以我们访问其中的每一个,它们都是 Identifier 类型的单一节点,然后我们继续;

4.此时我们来到了 body,这是一个 BlockStatement 并且也有一个 body 节点,而且也是一个数组节点,我们深入访问其中的每一个;

5.这里唯一的一个属性是 ReturnStatement 节点,它有一个 argument,我们访问 argument 就找到了 BinaryExpression

6.BinaryExpression 有一个 operator,一个 left,和一个 right。 Operator 不是一个节点,它只是一个值。因此我们不用继续向内遍历,我们只需要访问 left 和 right

Babel 的转换步骤基本都是是这样的遍历过程。

八、具体语法树

看到抽象语法树,我们脑海中会出现这样一个疑问:有没有具体语法树呢?

和抽象语法树相对的是具体语法树(通常称作分析树)。一般的,在源代码的翻译和编译过程中,语法分析器创建出分析树。一旦AST 被创建出来,在后续的处理过程中,比如语义分析阶段,会添加一些信息。

文章首发于 Jartto‘s blog

原文地址:https://www.cnblogs.com/wind-alex/p/10375236.html

时间: 2024-10-27 17:24:20

AST 抽象语法树的相关文章

使用PHP-Parser生成AST抽象语法树

0.前言 最近项目的流程逐渐清晰,但是很多关键性的技术没有掌握,也只能一步一步摸索. 由于要做基于数据流分析的静态代码分析,所以前端的工作如:词法分析.语法分析必不可少.Yacc和Lex什么的就不再考虑了,查了一天的资料,发现两款比较适合,一款是Java下的ANTLR,另一款是专门做PHP AST生成的PHP-Parser. ANTLR是编译原理领域比较著名的工具了,相对于Yacc和Lex,更加实用.但是对PHP的语法文件只有一个,折腾了半天才生成调通,发现不太适合,对于"$a=1"生

AST抽象语法树 Javascript版

在javascript世界中,你可以认为抽象语法树(AST)是最底层. 再往下,就是关于转换和编译的"黑魔法"领域了. 现在,我们拆解一个简单的add函数 function add(a, b) { return a + b } 首先,我们拿到的这个语法块,是一个FunctionDeclaration(函数定义)对象. 用力拆开,它成了三块: 一个id,就是它的名字,即add 两个params,就是它的参数,即[a, b] 一块body,也就是大括号内的一堆东西 add没办法继续拆下去了

用python演示一个简单的AST(抽象语法树)

假设对'a + 3 * b'进行解释,其中a=2,b=5 代码很简单,就不再进行详细的解释了. Num = lambda env, n: n Var = lambda env, x: env[x] Add = lambda env, a, b:_eval(env, a) + _eval(env, b) Mul = lambda env, a, b:_eval(env, a) * _eval(env, b) _eval = lambda env, expr:expr[0](env, *expr[1

Babel(抽象语法树,又称AST)

文章:https://juejin.im/post/5a9315e46fb9a0633a711f25 https://github.com/jamiebuilds/babel-handbook/blob/master/translations/zh-Hans/plugin-handbook.md 你了解过Babel吗? 了解过抽象语法树,又称AST,有学习过,也写过一个基于AST的乞丐版模板引擎,先是词法解析token,然后生产抽象语法树,然后更改抽象语法树,当然这是插件做的事情,最后根据新的A

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

这是专门探索 JavaScript 及其所构建的组件的系列文章的第 14 篇. 如果你错过了前面的章节,可以在这里找到它们: JavaScript 是如何工作的:引擎,运行时和调用堆栈的概述! JavaScript 是如何工作的:深入V8引擎&编写优化代码的5个技巧! JavaScript 是如何工作的:内存管理+如何处理4个常见的内存泄漏 ! JavaScript 是如何工作的:事件循环和异步编程的崛起+ 5种使用 async/await 更好地编码方式! JavaScript 是如何工作的:

五分钟了解抽象语法树(AST)babel是如何转换的?

抽象语法树 什么是抽象语法树? It is a hierarchical program representation that presents source code structure according to the grammar of a programming language, each AST node corresponds to an item of a source code. 抽象语法树是源代码语法结构的一种抽象表示.它以树状的形式表现编程语言的语法结构,树上的每个节点

复杂网络,抽象语法树

近期看了一些软件抽象为复杂网络,以及软件抽象成静态语法树的文章.做一个小总结. 1.复杂网络是由大量的边和点组成的,边点都可以有类型,加权值,边还可以有方向.如何计算边和点的权值是一个关键点,如何在不执行代码的情况下确定边的方向,目前不确定是否已经解决. 有许多工具,可以直接扫描软件源代码,抽象为复杂网络.然而我还没亲身实践,且做个记录. Dependency Finder分析编译后的java代码,能够提取依赖图. Doxygen是使用c++开发的基于源代码注释的文档生成工具.但是这个注释,是人

解释抽象语法树

创建了抽象语法树之后,有两个选择:解释或编译.解释,简单地说,就是遍历树,同时执行操作:编译,就是改变成其他形式,对于机器执行来说可能更简单,通常可能更快.这一小节先讨论如何解释结果,下面一小节再讨论编译的内容,最后,再讨论何时应该用解释,何时应该用编译的问题. 下面的例子是一个很小解释器,解释抽象语法树的主要工作由函数interpret 完成,它遍历树,并同时执行需要的动作.逻辑相当简单,如果发现一个文字值或标识符,就返回相应值: | Ident (s) ->variableDict.[s]

抽象语法树(Abstract Syntax Tree)

抽象语法树(AST)表示组成程序的结构,可以让程序员更容易使用,F# 适宜这种开发的一个原因就是它的联合类型.这种类型非常适合表示语言,因为它可以用来表示相关而结构不相同的项目.下面就是抽象语法树的例子: type Ast = | Ident of string | Val of System.Double | Multi of Ast * Ast | Div of Ast * Ast | Plus of Ast * Ast | Minus of Ast * Ast 树非常简单,只包含一种类型: