第4章 语法分析入门:抽象语法树(AST)概念、Python ast模块使用、简单表达式解析

好,咱们今天聊聊语法分析。说白了,就是让计算机理解你写的代码。

你想想看,我们写RTL代码的时候,用的都是文本。但计算机要处理这些代码,不能像人一样“看”懂。它需要一种结构化的表示方式。这个方式,就是抽象语法树,简称AST。

4.1 什么是抽象语法树?

抽象语法树,英文叫Abstract Syntax Tree。我习惯叫它AST。

它是什么?就是把代码的语法结构,用树的形式表示出来。每个节点代表一个语法结构。比如一个加法表达式,就是一个节点。它的两个子节点,就是加号两边的操作数。

举个例子,表达式 3 + 4 * 5。人一看就知道先乘后加。但计算机怎么知道?AST就能清晰地表达这个优先级。

对应的AST大概是这样的:

    (+)
   /   \
 (3)   (*)
       /   \
     (4)   (5)

根节点是加法,左子节点是数字3,右子节点是乘法。乘法下面又有4和5。这样,计算顺序就一目了然了。

核心要点:AST是代码的骨架。它去掉了括号、分号这些细节,只保留语法结构。我们的RTL检查工具,就是在这个骨架上做文章。

我在项目中遇到过一个问题。有个同事写的代码,综合后面积特别大。我一看AST,发现他写了个很深的嵌套条件表达式。AST树长得跟圣诞树似的。这种结构,综合器很难优化。所以,看AST能帮你发现代码的“坏味道”。

4.2 为什么AST对RTL检查很重要?

咱们做RTL代码检查,说白了就是找茬。但找茬得有依据,不能瞎找。

AST给了我们一个标准化的视角。不管代码怎么写,最终都会变成一棵树。我们只需要在这棵树上做遍历、匹配、分析就行。

举个例子,你想检查所有的不完整敏感列表。在AST里,每个always块都是一个节点。它的敏感列表是它的子节点。你只需要遍历所有always节点,检查它们的敏感列表子节点是否完整。

这比用正则表达式去匹配文本,要靠谱得多。正则表达式很容易被注释、宏定义这些东西搞晕。但AST是纯语法结构,干净得很。

我的经验:刚开始做检查工具时,我也试过用正则。后来发现,维护正则表达式比写代码本身还累。改用AST后,世界清净了。

4.3 Python的ast模块

Python自带的ast模块,就是用来处理AST的。它能把Python代码解析成AST,也能把AST还原成代码。

咱们虽然检查的是RTL代码,但工具本身是用Python写的。所以,理解Python的ast模块,是基本功。

先看个最简单的例子:

import ast

code = "x = 3 + 4"
tree = ast.parse(code)
print(ast.dump(tree, indent=2))

运行后,你会看到这样的输出:

Module(
  body=[
    Assign(
      targets=[
        Name(id='x', ctx=Store())],
      value=BinOp(
        left=Constant(value=3),
        op=Add(),
        right=Constant(value=4)))],
  type_ignores=[])

嗯,看着有点乱。但仔细看,结构很清楚。最外层是Module,里面有个Assign(赋值)。Assign的targets是Name(变量名x),value是BinOp(二元操作)。BinOp里面又有left、op、right。

这就是AST的威力。它把代码拆成了一个个节点,每个节点都有明确的类型和属性。

注意:ast.parse()只能解析Python代码。要解析Verilog或VHDL,得用专门的解析器。但思路是一样的。

4.4 遍历AST的两种方式

拿到AST之后,怎么用?两种方式:手动遍历和使用NodeVisitor。

4.4.1 手动遍历

手动遍历,就是自己写递归函数。比如,你想找出代码里所有的数字常量:

def find_constants(node):
    if isinstance(node, ast.Constant):
        print(f"找到常量: {node.value}")
    for child in ast.iter_child_nodes(node):
        find_constants(child)

code = "a = 10 + 20"
tree = ast.parse(code)
find_constants(tree)

这种方法很灵活,但代码会变得很啰嗦。尤其是你要处理很多种节点类型的时候。

4.4.2 使用NodeVisitor

ast模块提供了NodeVisitor类。你只需要继承它,然后重写对应节点类型的visit方法。

class MyVisitor(ast.NodeVisitor):
    def visit_Constant(self, node):
        print(f"访问常量: {node.value}")
        self.generic_visit(node)  # 继续访问子节点

    def visit_BinOp(self, node):
        print(f"访问二元操作: {type(node.op).__name__}")
        self.generic_visit(node)

code = "x = 3 + 4 * 5"
tree = ast.parse(code)
visitor = MyVisitor()
visitor.visit(tree)

输出结果:

访问二元操作: Add
访问常量: 3
访问二元操作: Mult
访问常量: 4
访问常量: 5

你看,NodeVisitor会自动帮你遍历整棵树。你只需要关注你想处理的节点类型就行。

个人习惯:我一般用NodeVisitor。它让代码更干净,也更容易扩展。后来要加新的检查规则,只需要新增一个visit方法就行。

4.5 简单表达式解析实战

光说不练假把式。咱们来写个实际点的例子。

假设我们要检查RTL代码中的表达式,看它是不是用了“魔法数字”。所谓魔法数字,就是直接写在代码里的数字常量,比如 assign y = a + 5 里的5。

好的代码风格,应该用参数代替这些魔法数字。咱们的工具就要找出它们。

先写一个简单的Python脚本,模拟这个检查:

import ast

class MagicNumberChecker(ast.NodeVisitor):
    def __init__(self):
        self.magic_numbers = []

    def visit_Constant(self, node):
        if isinstance(node.value, int):
            # 排除0和1,它们通常不是魔法数字
            if node.value not in (0, 1):
                self.magic_numbers.append((node.lineno, node.value))
        self.generic_visit(node)

code = """
def calc(a, b):
    result = a * 100 + b * 50
    return result + 1
"""

tree = ast.parse(code)
checker = MagicNumberChecker()
checker.visit(tree)

for lineno, value in checker.magic_numbers:
    print(f"第{lineno}行: 发现魔法数字 {value}")

输出:

第3行: 发现魔法数字 100
第3行: 发现魔法数字 50

看到了吧?100和50被揪出来了。1被排除了,因为它太常见,不算魔法数字。

避坑指南:我曾经把0也排除了,结果漏掉了一个除以0的检查。后来我学乖了,排除规则要谨慎。最好做成可配置的。

4.6 AST的局限性

AST不是万能的。它只关心语法,不关心语义。

什么意思?比如 a + b,AST知道这是一个加法。但它不知道a和b是什么类型,也不知道这个加法会不会溢出。这些是语义分析的事。

另外,AST不包含注释。注释在解析时就被丢掉了。如果你想检查注释的规范性,得用别的方法。

还有,AST不包含宏定义展开后的内容。比如Verilog里的 `define,在AST里就是一个宏引用节点。你得自己处理宏展开。

重要提醒:AST只是第一步。完整的RTL检查工具,还需要符号表、类型系统、数据流分析等。但AST是基础,基础不牢,地动山摇。

4.7 本章小结

咱们今天聊了AST的概念,Python ast模块的基本用法,以及怎么用它做简单的表达式分析。

说白了,AST就是把代码变成树。我们在这棵树上做文章,就能实现各种检查规则。

下一章,咱们会深入RTL代码的AST表示。到时候,你会看到Verilog代码是怎么变成树的,以及我们怎么在这棵树上做更复杂的检查。

嗯,今天就到这儿。记住,多动手写代码,AST这东西,用多了就熟了。


公众号:蓝海资料掘金营,微信deep3321