第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