抽象语法树AST

如何对自定义脚本使用AST进行解析

Posted by Momoka7 on July 17, 2024

前言

是什么

抽象语法树(AST, Abstract Syntax Tree)是编译器和解释器中常用的数据结构,它表示代码的语法结构。AST 将代码分解成可以操作的节点,每个节点表示源代码中的一个结构。

有何用

要解析自定义的脚本,就可以通过构建 AST 来分析脚本的结构及其语法。

实现

依旧以if语句为例,但是 AST 可以解析包括condition、if body、else body等内容,且可以实现嵌套if语法。

步骤

要构建一个 AST,有如下流程:

  1. 定义语法规则:确定代码格式的语法结构,这里以if为例。
  2. 编写词法分析器(Tokenizer/Lexer):将源代码分解为标记(tokens)。
  3. 编写语法分析器(Parser):将标记序列转换为 AST。
  4. 操作和遍历 AST:编写函数来遍历和操作生成的 AST。

1. 语法规则

if语句的语法规则如下:

  1. if 关键字后跟一个条件表达式。
  2. 条件表达式后跟一个块语句。
  3. 可选的 else 关键字后跟一个块语句。

2. 词法分析器 Tokenizer

本质就是遍历源码字符串,提取关键字、字面量等;也可以正则提取tokens

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
function tokenize(input) {
  const tokens = [];
  let current = 0;

  while (current < input.length) {
    let char = input[current];

    if (/\s/.test(char)) {
      current++;
      continue;
    }

    if (char === "i" && input.slice(current, current + 2) === "if") {
      tokens.push({ type: "if", value: "if" });
      current += 2;
      continue;
    }

    if (char === "e" && input.slice(current, current + 4) === "else") {
      tokens.push({ type: "else", value: "else" });
      current += 4;
      continue;
    }

    if (char === "(") {
      tokens.push({ type: "paren", value: "(" });
      current++;
      continue;
    }

    if (char === ")") {
      tokens.push({ type: "paren", value: ")" });
      current++;
      continue;
    }

    if (char === "{") {
      tokens.push({ type: "brace", value: "{" });
      current++;
      continue;
    }

    if (char === "}") {
      tokens.push({ type: "brace", value: "}" });
      current++;
      continue;
    }

    if (/\d/.test(char)) {
      let value = "";

      while (/\d/.test(char)) {
        value += char;
        char = input[++current];
      }

      tokens.push({ type: "number", value });
      continue;
    }

    throw new TypeError("Unexpected character: " + char);
  }

  return tokens;
}

3. 语法分析器 Parser

tokens遍历,并按照token类型组织遍历的具体规则,比如if后面一个token必须是(,否则报错。

考虑嵌套if,这里的if-else body/consequentwalk()进行了递归调用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
function parse(tokens) {
  let current = 0;

  function walk() {
    let token = tokens[current];

    if (token.type === "number") {
      current++;
      return {
        type: "NumberLiteral",
        value: token.value,
      };
    }

    if (token.type === "if") {
      current++;
      token = tokens[current];

      if (token.type !== "paren" || token.value !== "(") {
        throw new TypeError("Expected ( after if");
      }
      current++;

      const test = walk();

      token = tokens[current];
      if (token.type !== "paren" || token.value !== ")") {
        throw new TypeError("Expected ) after condition");
      }
      current++;

      token = tokens[current];
      if (token.type !== "brace" || token.value !== "{") {
        throw new TypeError("Expected { after )");
      }
      current++;

      const consequent = [];
      while (
        tokens[current].type !== "brace" ||
        tokens[current].value !== "}"
      ) {
        consequent.push(walk());
      }
      current++;

      let alternate = null;
      token = tokens[current];
      if (token && token.type === "else") {
        current++;

        token = tokens[current];
        if (token.type !== "brace" || token.value !== "{") {
          throw new TypeError("Expected { after else");
        }
        current++;

        alternate = [];
        while (
          tokens[current].type !== "brace" ||
          tokens[current].value !== "}"
        ) {
          alternate.push(walk());
        }
        current++;
      }

      return {
        type: "IfStatement",
        test,
        consequent,
        alternate,
      };
    }

    throw new TypeError("Unexpected token type: " + token.type);
  }

  const ast = {
    type: "Program",
    body: [],
  };

  while (current < tokens.length) {
    ast.body.push(walk());
  }

  return ast;
}

4. 遍历和操作 AST

实际上有了 AST,你可以对其进行几乎任何你想要的操作了。

这里对 AST 的所有结点进行了遍历,并使用访问对象来打印遍历的情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
function traverse(node, visitor) {
  function traverseArray(array, parent) {
    array.forEach((child) => {
      traverseNode(child, parent);
    });
  }

  function traverseNode(node, parent) {
    let methods = visitor[node.type];

    if (methods && methods.enter) {
      methods.enter(node, parent);
    }

    switch (node.type) {
      case "NumberLiteral":
        break;
      case "IfStatement":
        traverseNode(node.test, node);
        traverseArray(node.consequent, node);
        if (node.alternate) {
          traverseArray(node.alternate, node);
        }
        break;
      case "Program":
        traverseArray(node.body, node);
        break;
      default:
        throw new TypeError(node.type);
    }

    if (methods && methods.leave) {
      methods.leave(node, parent);
    }
  }

  traverseNode(node, null);
}

其他

在实际的游戏脚本中,需要对token细化,此外也要实现具体的if-condition条件的判断,比如之前所发布的对condition的实现。