1. AST基础概念:什么是抽象语法树
抽象语法树,英文叫 Abstract Syntax Tree,简称 AST。说白了,就是把代码的「骨架」拎出来,去掉那些花里胡哨的语法糖。
我刚开始接触 AST 的时候,觉得这东西挺玄乎。后来做编译器工具链,才真正体会到它的价值。你想想看,我们写的 HDL 代码,比如 Verilog 或 VHDL,本质上是一堆文本。计算机要理解这些文本,就得先把它变成一种结构化的数据——这就是 AST。
1.1 什么是抽象语法树
AST 是一棵树。树的每个节点代表代码中的一个结构。比如一个 always 块、一个 assign 语句、一个 if 条件,甚至一个简单的加法运算。
举个例子,下面这段简单的 Verilog 代码:
module counter(
input clk,
input rst_n,
output reg [3:0] count
);
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= 4'b0;
else
count <= count + 1'b1;
end
endmodule
这段代码对应的 AST 大概长这样:
AST 结构示意(简化版):
- Root: SourceText
- ModuleDeclaration
- ModuleName: "counter"
- PortList
- Port: clk (input, wire)
- Port: rst_n (input, wire)
- Port: count (output, reg, [3:0])
- ModuleBody
- AlwaysBlock
- SensitivityList: posedge clk, negedge rst_n
- IfStatement
- Condition: !rst_n
- ThenBranch: count <= 4'b0
- ElseBranch: count <= count + 1'b1
- AlwaysBlock
- ModuleDeclaration
看到没?AST 把代码的层次结构清清楚楚地表达出来了。每个节点都有类型,有子节点。这就是计算机理解代码的方式。
1.2 AST 在编译器中的作用
AST 在编译器里扮演什么角色?我打个比方:
你写 HDL 代码,就像建筑师画图纸。编译器拿到图纸后,第一步不是直接去盖楼,而是先「读懂」图纸。AST 就是编译器读懂图纸后的「内部表示」。
编译器的工作流程大致是:
- 词法分析:把代码拆成一个个 token(单词)。比如
module、counter、(这些都是 token。 - 语法分析:根据语法规则,把 token 组合成 AST。这一步会检查语法错误。
- 语义分析:在 AST 上做类型检查、作用域分析等。
- 中间代码生成:从 AST 生成更低级的表示,比如 RTL 网表。
- 优化:在 AST 或中间表示上做各种优化。
- 目标代码生成:最终生成布局布线工具能用的网表。
AST 在第三步到第五步之间特别重要。我做过一个项目,需要在综合前对代码做自动优化。直接在 AST 上操作,比在文本上做正则替换靠谱一万倍。
我的经验:AST 是编译器的「中枢神经系统」。所有高级分析和变换,几乎都在 AST 上完成。你如果要做代码优化工具,AST 是你绕不开的第一道坎。
1.3 HDL 代码与 AST 的映射关系
HDL 代码和 AST 之间是一一对应的映射关系。什么意思?就是每段代码都能唯一地映射到 AST 上的一个子树。
我整理了一个对照表,方便你理解:
| HDL 代码结构 | AST 节点类型 | 说明 |
|---|---|---|
module ... endmodule |
ModuleDeclaration | 模块声明,包含端口和内部逻辑 |
input clk |
PortDeclaration | 端口声明,包含方向、类型、位宽 |
reg [3:0] count |
VariableDeclaration | 变量声明,包含类型和位宽 |
always @(...) |
AlwaysBlock | 过程块,包含敏感列表和语句体 |
if (cond) ... else ... |
IfStatement | 条件分支,包含条件、真分支、假分支 |
a <= b + c |
BlockingAssign / NonblockingAssign | 赋值语句,区分阻塞和非阻塞 |
a + b |
BinaryExpression | 二元运算,包含运算符和左右操作数 |
4'b1010 |
NumberLiteral | 数值字面量,包含进制和位宽 |
嗯,这里要注意一点:AST 不是简单的「代码的另一种写法」。它去掉了代码中的注释、空格、换行这些无关紧要的东西。只保留「语义」相关的结构。
举个例子,下面两段代码在文本上完全不同,但 AST 完全一样:
// 写法一
assign sum = a + b;
// 写法二
assign
sum =
a + b;
AST 里都是:
ContinuousAssign
├── Target: sum
└── Expression: BinaryExpression(+)
├── Left: a
└── Right: b
这就是「抽象」二字的含义——只关心结构,不关心格式。
我曾经踩过的坑:刚开始做 AST 工具时,我以为 AST 能保留所有代码信息。后来发现,像 `generate` 块、`macro` 展开这些,在 AST 里可能已经被预处理掉了。如果你需要保留这些信息,得在词法分析阶段就做特殊处理。
为了让你更直观地理解 AST 的结构,我画了一张图:
我个人觉得,理解 AST 的关键在于:别把它当成代码,把它当成「代码的骨架」。你写 HDL 代码时,脑子里想的是「我要一个加法器」。编译器看到 AST 时,它看到的是「一个 ModuleDeclaration,里面有一个 ContinuousAssign,赋值表达式是一个 BinaryExpression,运算符是 +」。
这种思维方式,是做代码优化工具的基础。后面我们会讲怎么在 AST 上做各种优化,比如常量折叠、死代码消除、资源共享等等。但前提是——你得先理解 AST。
一个小建议:如果你刚开始接触 AST,可以试试用 Python 的 pyverilog 或者 surelog 的 AST dump 功能。把一段简单的 HDL 代码输进去,看看生成的 AST 长什么样。亲手摸一摸,比看一百篇文章都管用。