从0到1,构建抽象代码树

从0到1,构建抽象代码树

1.前言

参考 babel 工具链对 JavaScript 代码的静态分析过程,笔者实现了一套简易的抽象语法树构造过程,由于抽象语法树相关的中文文档较少,故有此文以此记录。

2.关键词:抽象语法树

抽象语法树(Abstract Syntax Tree,以下简称 AST)是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。之所以说语法是“抽象”的,是因为这里的语法并不会表示出真实语法中出现的每个细节。比如,嵌套括号被隐含在树的结构中,并没有以节点的形式呈现。以上是搜索引擎也能给到的介绍,下面就讲讲文档较少覆盖的 AST 生成过程和一种特定的AST使用方式。

3.AST生成过程

3.1 词法分析:生成 token 对象数组。

词法分析将读取字符串形式的代码,将代码按照规则解析、生成由一个个 token 组成的 tokens 数组(令牌流),同时,它会移除空白、注释等。在代码中拆解出 token 对象的常用步骤如下:

1.确定 token 类型,如数字、字符串、关键词、变量等
2.确定 token 的匹配方法:tokenizer 函数。函数读取代码时,按照代码字符串中字符的下标递增进行迭代,递归执行 tokenizer 函数,根据 token 类型,函数以对应的正则表达式、强等于等方式匹配字符。
3.生成 token 对象:token 对象的属性常包括 token 的类型和代码内容。根据实际需要,token 对象也可以携带自身在编辑器中的坐标等辅助信息。
复制代码

一段自定义语法规则的代码“if A 5 then B 8”转化而成的 tokens 令牌流如下 。

let tokens = [
  {
    token: "if",
    type: "Identifier",
  },
  {
    token: "A",
    type: "identifier",
  },
  {
    token: "5",
    type: "number",
  },
  {
    token: "then",
    type: "identifier",
  },
  {
    token: "B",
    type: "identifier",
  },
  {
    token: "8",
    type: "number",
  },
];
复制代码

3.2 语法分析:生成 AST

语法分析将每个 token 对象按照一定形式解析,形成树形结构,树的每一层结构称为节点,节点们共同作用于程序代码的静态分析,同时验证语法,抛出语法错误信息。

3.2.1 节点构造

语义本身就代表了一个值的节点是字面量节点,在树形结构担任“叶子”角色。由于每一个被解析出的 token 都携带 type(类型)属性,我们很容易通过type属性匹配得到对应的字面量节点,如:type属性为“number”的 token,其本身的语义就代表了一个 Number 类型值,作为叶子,在树结构中没有其他子节点可被其包含,因此可以由其生成一个字面量节点,为其设置 type 属性为“NumberLiteral”,义为 Number 类型字面量。字符串、布尔类型值、正则表达式等亦然。详细的节点命名可参照 AST 对象文档。

Number 类型字面量节点的生成函数如下。

if (curToken.type === 'number') {
    // 数字或字符串,说明此处是个常量表达式
    const literal = {
      type: 'NumberLiteral',
      value: eval(curToken.value),
    };
    // 将current指向下一个token对象
    nextToken(); 
    return literal;
 }
复制代码

AST 利用“枝干”节点可以将多个节点组合成为更庞大的语法枝干,它们将单个或多个叶子节点、枝干节点组合成为复杂的程序语法。if 语句节点、块状域节点、函数表达式节点等,都可以看做语法树的枝干节点,下面举个例子。
if 语句节点常带有三个属性:test、consequent、alternate。
1.test 属性表示一个条件表达式节点。
2.consequent 属性表示条件为 true 时的执行语句,通常是一个块状域节点。
3.alternate 属性表示 else 后跟随的语句节点,通常是一个块状域节点或 null,也可能是新的 if 语句节点,这种情况下的语法结构形似:if(a){ … }else if(b){ … }

if 语句树形节点的生成函数如下:

 if (curToken.type === 'identifier' && curToken.value === 'if') {
   // 解析 if 语句
   const statement = {
     type: 'IfStatement',
   };
   // if 后面必须紧跟着 (
   nextToken();
   if (curToken.type !== 'parens' || curToken.value !== '(') {
     throw new Error('Expected ( after if');
 
   // 后续的一个表达式是 if 的判断条件
   statement.test = nextExpre
   // 判断条件之后必须是 )
   nextToken();
   if (curToken.type !== 'parens' || curToken.value !== ')') {
     throw new Error('Expected ) after if test expression');
 
   // 下一个语句是 if 成立时执行的语句
   statement.consequent = nextStat
   // 如果下一个符号是 else 就说明还存在 if 不成立时的逻辑
   if (curToken === 'identifier' && curToken.value === 'else') {
     statement.alternative = nextStatement();
   } else {
     statement.alternative = null;
   }
   return statement;
 }
复制代码
3.2.2 简单示例

当我们以3.2.1小节的方法解构、定义出更多枝干节点的内部结构后,就能根据这些节点的结构限制来进行 AST 的构造。参照if语句的内部结构为每个 token 所对应的节点类型定义其树形结构,根据这些节点的结构限制,设计出可递归的 AST 构造函数,使函数实现递归形式的执行流程。执行这样的函数,逐步生成枝干结构,直至完整的树形结构构造完成。一段简单javaScript代码的AST生成示例如下:

1.javaScript 代码:

                            if ( 1 > 0 ) { alert( 'aa' ) }
复制代码

2.生成 tokens 令牌流

[
  {type: "identifier", value: "if"},
  {type: "parens", value: "("},
  {type: "number", value: "1"},
  {type: "operator", value: ">"},
  {type: "number", value: "0"},
  {type: "parens", value: ")"},
  {type: "brace", value: "{"},
  {type: "identifier", value: "alert"},
  {type: "parens", value: "("},
  {type: "string", value: "'aa'"},
  {type: "parens", value: ")"},
  {type: "brace", value: "}"},
]
复制代码

3.生成 AST

{
  "type": "Program",
  "body": [
    {
      "type": "IfStatement",
      "test": {
        "type": "BinaryExpression",
        "left": {
          "type": "Literal",
          "value": 1
        },
        "right": {
          "type": "Literal",
          "value": 0
        }
      },
      "consequent": {
        "type": "BlockStatement",
        "body": [
          {
            "type": "ExpressionStatement",
            "expression": {
              "type": "CallExpression",
              "caller": {
                "type": "Identifier",
                "value": "alert"
              },
              "arguments": [
                {
                  "type": "Literal",
                  "value": "aa"
                }
              ]
            }
          }
        ]
      },
      "alternative": null
    }
  ]
}
复制代码

4.AST的一种应用介绍:Babel工具链

AST 在 Babel 工具链中的作用是将 ECMAScript 2015+ 版本的代码转换为向后兼容的 JavaScript 语法,其历经了生成 AST,再将 AST 转换、生成目标代码的过程。

1.设计 visitor,visitor 是一个对象,visitor 的每个 key 是 AST 的每个节点的类型命名,对象的值是一个函数,这个函数需要描述 AST 的转换过程。
2.设计函数,遍历一个树的所有节点,对节点进行转换,生成转换后的一个新的 AST。遍历至枝干节点一般以递归形式遍历其子节点,叶子节点则直接执行其转换函数。
3.生成目标代码。根据新的 AST 和目标语言的语法规范,设计出每个节点对应的代码生成方式,层层遍历节点,为原始代码赋予新的语法。
复制代码

5.结语

本文主要对AST的生成过程和Babel对AST的一种应用方式进行介绍,在生成过程中小节中描述了AST生成过程中的词法解析和语法解析两个步骤;在AST的应用中介绍了Babel工具链对Javascript语言的语法转换过程。除Babel的应用方式外,我们可以对AST做很多操作,如代码执行、语法报警、自定义打包方案等,在面临此类需求时,AST往往能提供重要的能力,希望本文在届时对您有所帮助。

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享