3、HDL词法分析:Verilog/VHDL的词法规则、Token生成原理、词法分析器实现
好,咱们今天聊聊词法分析。说白了,这就是编译器把源代码「切碎」的第一步。你写了一大段Verilog或VHDL,计算机怎么知道哪些是关键字、哪些是变量名、哪些是数字?全靠词法分析器来干这个脏活累活。
我刚开始接触AST那会儿,总觉得词法分析没啥技术含量。后来自己手写了一个简易的Verilog词法分析器,才发现这里面的坑还真不少。嗯,咱们一步步来拆解。
3.1 Verilog的词法规则
Verilog的词汇单元,我习惯把它们分成几大类。你想想看,写代码时用到的无非就是这些:
- 关键字:如
module、endmodule、always、assign、if、else等。这些是语言保留的,你不能拿来做变量名。 - 标识符:变量名、模块名、信号名。比如
clk、data_bus、counter_8bit。规则很简单:字母或下划线开头,后面跟字母、数字、下划线。 - 数字字面量:Verilog的数字表示很灵活。比如
8'hFF、4'b1010、123。格式是[位宽]'[进制][数值]。 - 运算符:
+、-、*、&、|、^、~等等。 - 分隔符:
;、,、(、)、[、]、{、}。 - 注释:
//单行注释,/* ... */多行注释。词法分析时直接跳过。
避坑指南:我曾经在解析Verilog数字时栽过跟头。比如 'hFF 这种省略位宽的写法,还有 8'bx 这种带不定态的。词法分析器必须能正确识别这些变体,否则后面语法分析会崩得一塌糊涂。
3.2 VHDL的词法规则
VHDL比Verilog啰嗦一些,但词法规则其实更规整。我个人觉得VHDL的词法设计更「学院派」,但用起来也挺顺手。
| 类别 | 示例 | 说明 |
|---|---|---|
| 关键字 | entity, architecture, signal, process |
不区分大小写,但习惯用小写 |
| 标识符 | clk, data_in, counter_8bit |
字母开头,可包含数字和下划线,不能连续两个下划线 |
| 数字字面量 | 10, 16#FF#, 2#1010# |
基于进制前缀,格式为 进制#数值# |
| 字符串 | "hello", "reset" |
双引号包裹,用于报告或属性 |
| 注释 | -- 这是注释 |
双横线开头,到行尾结束 |
VHDL有个特点:它不区分大小写。这意味着 SIGNAL 和 signal 是同一个关键字。我在做词法分析器时,统一转成小写再匹配,省事很多。
3.3 Token生成原理
词法分析的核心产出就是Token流。每个Token包含两个信息:类型 和 值。
举个例子,对于Verilog代码:
wire [7:0] data_bus;
词法分析器会生成这样的Token序列:
| Token类型 | Token值 | 说明 |
|---|---|---|
| KEYWORD | wire | 关键字 |
| LBRACKET | [ | 左方括号 |
| NUMBER | 7 | 数字 |
| COLON | : | 冒号 |
| NUMBER | 0 | 数字 |
| RBRACKET | ] | 右方括号 |
| IDENTIFIER | data_bus | 标识符 |
| SEMICOLON | ; | 分号 |
Token生成的过程,说白了就是「读字符 -> 匹配模式 -> 生成Token -> 继续读」。我习惯用有限状态机(FSM)来实现这个逻辑,每个状态对应一种词法模式。
小技巧:Token类型最好用枚举或常量定义,不要用字符串。比如 TOKEN_KEYWORD、TOKEN_IDENTIFIER。这样在语法分析阶段做模式匹配时,性能会好很多。
3.4 词法分析器实现
好,咱们来点实际的。我手写一个简易的Verilog词法分析器核心逻辑,用伪代码展示。你想想看,核心就是一个循环:
function tokenize(source_code):
tokens = []
pos = 0
while pos < len(source_code):
ch = source_code[pos]
// 跳过空白和注释
if ch is whitespace:
pos += 1
continue
if ch == '/' and source_code[pos+1] == '/':
skip_to_end_of_line()
continue
if ch == '/' and source_code[pos+1] == '*':
skip_to_end_of_comment()
continue
// 识别关键字或标识符
if ch is letter or ch == '_':
start = pos
while source_code[pos] is letter or digit or '_':
pos += 1
word = source_code[start:pos]
if word in keyword_set:
tokens.append(Token(KEYWORD, word))
else:
tokens.append(Token(IDENTIFIER, word))
continue
// 识别数字
if ch is digit:
// 处理Verilog的数字格式:位宽'进制数值
// 或者普通十进制数
start = pos
// ... 具体解析逻辑
tokens.append(Token(NUMBER, value))
continue
// 识别运算符和分隔符
if ch in operator_map:
tokens.append(Token(operator_map[ch], ch))
pos += 1
continue
// 遇到无法识别的字符,报错
error("Unexpected character: " + ch)
pos += 1
return tokens
这段代码看起来简单,但实际实现时有很多细节。比如Verilog的数字 8'hFF,你得先读 8,然后遇到 ' 知道后面是进制和数值。再比如 4'b1010 中的 b 表示二进制。
注意:VHDL的字符串和位字符串容易混淆。比如 "1010" 是字符串,而 B"1010" 是位字符串。词法分析器必须根据上下文区分。我建议在词法分析阶段先统一识别为字符串,等语法分析时再根据前导字符判断具体类型。
3.5 知识体系总览
下面这张图,是我自己总结的词法分析核心流程。你看一眼,基本就能把握住全章脉络。
这张图把整个流程串起来了。从源代码进去,经过词法分析器一顿操作,吐出一串Token。这些Token就是后面语法分析器要吃的「食材」。
3.6 实现中的几个坑
我做了这么多年EDA工具,词法分析器踩过的坑不少。挑几个典型的说说:
- 注释嵌套问题:Verilog不支持注释嵌套。你写
/* /* */ */,第一个*/就结束了注释。我见过有人写嵌套注释导致整个模块被注释掉的惨案。 - 字符串中的关键字:VHDL的字符串
"entity"里的entity不是关键字。词法分析器必须区分字符串上下文。 - 数字解析的边界情况:比如
'hFF省略位宽,或者8'bx带不定态。我建议在词法分析阶段只做初步解析,详细的语义检查留给语法分析。 - Unicode支持:虽然Verilog/VHDL标准没强制要求,但现代工具链最好支持UTF-8编码的注释。我遇到过客户用中文注释导致乱码的问题。
我的习惯:在实现词法分析器时,我会先写一个简单的版本,只支持核心语法。然后逐步添加对边界情况的支持。这样调试起来轻松很多,不会一开始就被各种奇葩输入搞崩溃。
好了,词法分析这块就聊到这儿。记住一句话:词法分析是编译器的「门卫」,它把好第一道关,后面的工作才能顺利进行。你把这个基础打牢了,后面学语法分析、AST构建就会轻松很多。