3. 词法分析器实现:手写一个Verilog词法分析器
好,咱们直接进入正题。词法分析器,说白了就是编译器的“眼睛”。它负责把Verilog源码那长长的一串字符,拆成一个个有意义的“单词”——也就是Token。我当年第一次写这东西的时候,觉得不就是字符串匹配嘛,有啥难的?结果被一堆边界条件折磨得够呛。今天咱们就手撸一个简单的,把坑都踩一遍。
3.1 词法分析器到底在干啥?
先看个例子:
module counter(input clk, output reg [3:0] q);
人眼一看就知道:module是关键字,counter是标识符,input是关键字,clk是标识符……但计算机不懂啊。词法分析器的工作就是把这些“单词”一个个识别出来,并打上标签。
我习惯把词法分析器想象成一个流水线工人:
- 从字符流里一个一个读字符
- 根据当前字符决定下一步怎么走
- 凑成一个完整的Token后,打包送出去
- 然后继续读下一个
核心原则:最长匹配。比如 input 不能只匹配到 in 就停,必须读完整个关键字。
3.2 整体架构:状态机思维
词法分析器本质上是一个有限状态自动机(FSA)。你想想看,每读一个字符,状态就变一次。我画了一张图,帮你理清思路:
这张图看着简单,但实际写代码时,每个分支里还有子状态。比如数字字面量,你得区分十进制、十六进制、二进制……嗯,咱们后面细说。
3.3 关键字与标识符:最基础的部分
Verilog的关键字大概有100多个,比如 module、endmodule、input、output、wire、reg、always、assign 等等。识别逻辑其实很简单:
- 如果当前字符是字母或下划线,开始收集
- 持续收集直到遇到非字母、非数字、非下划线的字符
- 查表:如果收集到的字符串在关键字表里,就是关键字;否则就是标识符
我的小技巧:关键字表用哈希表存,查找O(1)。我早期用线性查找,编译大文件时明显感觉慢。后来改成哈希表,速度提升很明显。
看一段核心代码(C语言风格伪代码):
// 识别标识符或关键字
if (is_alpha(ch) || ch == '_') {
char buf[256];
int i = 0;
buf[i++] = ch;
while (is_alnum(ch = next_char()) || ch == '_') {
buf[i++] = ch;
}
buf[i] = '\0';
unget_char(ch); // 多读的字符还回去
// 查关键字表
TokenType type = lookup_keyword(buf);
if (type == TOK_KEYWORD) {
return make_token(TOK_KEYWORD, buf);
} else {
return make_token(TOK_IDENTIFIER, buf);
}
}
这里有个细节:回退字符。你读到了非标识符字符,说明标识符结束了,但那个字符属于下一个Token,得“还回去”。我刚开始写的时候忘了这茬,结果下一个Token的第一个字符丢了,调试了半天。
3.4 数字字面量:坑最多的地方
Verilog的数字表示比C语言复杂多了。它支持:
| 格式 | 示例 | 说明 |
|---|---|---|
| 十进制 | 123 |
普通整数 |
| 带位宽 | 8'd255 |
8位十进制数255 |
| 十六进制 | 16'hFF00 |
16位十六进制数 |
| 二进制 | 4'b1010 |
4位二进制数 |
| 八进制 | 12'o777 |
12位八进制数 |
| 实数 | 3.14159 |
浮点数 |
解析带位宽的数字时,格式是 [位宽]'[进制][数值]。比如 8'd255:
- 先读到
8,别急着当数字处理 - 看到
',知道后面跟进制标识 d表示十进制,h表示十六进制,b表示二进制,o表示八进制- 然后解析后面的数值部分
我曾经踩过的坑:数字里允许出现下划线作为分隔符,比如 8'd255 也可以写成 8'd2_55。下划线要忽略掉,但不能把它当成非法字符。还有,'d 前面的位宽可以省略,比如 'd255 也是合法的。这些边界情况,测试用例一定要覆盖全。
3.5 字符串与注释
字符串:以双引号开头,遇到下一个双引号结束。中间支持转义字符,比如 \n、\t、\\、\" 等。我习惯用一个标志位记录是否在转义模式中。
注释:Verilog有两种注释:
- 单行注释:
//开头,读到换行符结束 - 多行注释:
/*开头,*/结束
多行注释有个坑:注释不能嵌套。比如 /* /* */,第一个 */ 就结束了注释,后面的 */ 就成了非法字符。我见过有人写嵌套注释然后编译报错,一脸懵。
3.6 运算符与分隔符
Verilog的运算符有单字符的(如 +、-、*),也有多字符的(如 <=、>=、==、!=、&&、||)。解析时要遵循最长匹配原则:
// 伪代码示例
if (ch == '&') {
char next = peek_char();
if (next == '&') {
consume(); // 吃掉第二个 &
return make_token(TOK_LOGICAL_AND, "&&");
} else if (next == '=') {
// &=
} else {
return make_token(TOK_BITWISE_AND, "&");
}
}
分隔符包括括号、花括号、方括号、分号、逗号、点号等。这些比较简单,每个字符对应一个Token就行。
3.7 实战建议:从简单到复杂
如果你要自己实现一个词法分析器,我建议按这个顺序来:
- 先搞定标识符和关键字(最简单)
- 再处理数字字面量(注意各种进制)
- 然后处理运算符和分隔符
- 最后处理字符串和注释
- 别忘了空白字符和换行——直接跳过就行
测试驱动开发:每写完一个模块,立刻写测试用例。我习惯准备一个小的Verilog文件,里面包含所有语法元素,然后跑一遍看输出对不对。特别是边界情况,比如空文件、只有注释的文件、数字后面紧跟标识符的情况。
嗯,词法分析器这部分就讲到这里。记住一个原则:读一个字符,判断,决定下一步。状态机思维贯穿始终。代码写完后,多跑几个真实的设计文件,你会发现很多意想不到的情况——这就是经验积累的过程。