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

看到没?AST 把代码的层次结构清清楚楚地表达出来了。每个节点都有类型,有子节点。这就是计算机理解代码的方式。

1.2 AST 在编译器中的作用

AST 在编译器里扮演什么角色?我打个比方:

你写 HDL 代码,就像建筑师画图纸。编译器拿到图纸后,第一步不是直接去盖楼,而是先「读懂」图纸。AST 就是编译器读懂图纸后的「内部表示」。

编译器的工作流程大致是:

  1. 词法分析:把代码拆成一个个 token(单词)。比如 modulecounter( 这些都是 token。
  2. 语法分析:根据语法规则,把 token 组合成 AST。这一步会检查语法错误。
  3. 语义分析:在 AST 上做类型检查、作用域分析等。
  4. 中间代码生成:从 AST 生成更低级的表示,比如 RTL 网表。
  5. 优化:在 AST 或中间表示上做各种优化。
  6. 目标代码生成:最终生成布局布线工具能用的网表。

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 的结构,我画了一张图:

HDL代码 → AST 映射关系图 Verilog 代码 module adder( input [3:0] a, b, output [3:0] sum ); assign sum = a + b; endmodule 抽象语法树 (AST) SourceText └── ModuleDeclaration ├── Name: "adder" ├── PortList │ ├── a (input, [3:0]) │ ├── b (input, [3:0]) │ └── sum (output, [3:0]) └── ContinuousAssign └── sum = a + b AST 的核心特点 层次化结构:树形结构,根节点代表整个源文件,叶子节点代表标识符、常量等 抽象性:去掉了注释、空格、括号等语法细节,只保留语义结构 确定性:同一段代码有唯一的 AST 表示,便于分析和变换 可遍历性:可以深度优先或广度优先遍历所有节点,实现代码分析 可变换性:可以增删改节点,实现代码优化和重构

我个人觉得,理解 AST 的关键在于:别把它当成代码,把它当成「代码的骨架」。你写 HDL 代码时,脑子里想的是「我要一个加法器」。编译器看到 AST 时,它看到的是「一个 ModuleDeclaration,里面有一个 ContinuousAssign,赋值表达式是一个 BinaryExpression,运算符是 +」。

这种思维方式,是做代码优化工具的基础。后面我们会讲怎么在 AST 上做各种优化,比如常量折叠、死代码消除、资源共享等等。但前提是——你得先理解 AST。

一个小建议:如果你刚开始接触 AST,可以试试用 Python 的 pyverilog 或者 surelog 的 AST dump 功能。把一段简单的 HDL 代码输进去,看看生成的 AST 长什么样。亲手摸一摸,比看一百篇文章都管用。


专注资料整理