3、HDL词法分析:Verilog/VHDL的词法规则、Token生成原理、词法分析器实现

好,咱们今天聊聊词法分析。说白了,这就是编译器把源代码「切碎」的第一步。你写了一大段Verilog或VHDL,计算机怎么知道哪些是关键字、哪些是变量名、哪些是数字?全靠词法分析器来干这个脏活累活。

我刚开始接触AST那会儿,总觉得词法分析没啥技术含量。后来自己手写了一个简易的Verilog词法分析器,才发现这里面的坑还真不少。嗯,咱们一步步来拆解。

3.1 Verilog的词法规则

Verilog的词汇单元,我习惯把它们分成几大类。你想想看,写代码时用到的无非就是这些:

  • 关键字:如 moduleendmodulealwaysassignifelse 等。这些是语言保留的,你不能拿来做变量名。
  • 标识符:变量名、模块名、信号名。比如 clkdata_buscounter_8bit。规则很简单:字母或下划线开头,后面跟字母、数字、下划线。
  • 数字字面量:Verilog的数字表示很灵活。比如 8'hFF4'b1010123。格式是 [位宽]'[进制][数值]
  • 运算符+-*&|^~ 等等。
  • 分隔符;,()[]{}
  • 注释// 单行注释,/* ... */ 多行注释。词法分析时直接跳过。

避坑指南:我曾经在解析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有个特点:它不区分大小写。这意味着 SIGNALsignal 是同一个关键字。我在做词法分析器时,统一转成小写再匹配,省事很多。

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_KEYWORDTOKEN_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 知识体系总览

下面这张图,是我自己总结的词法分析核心流程。你看一眼,基本就能把握住全章脉络。

HDL词法分析核心流程 源代码(Verilog/VHDL) 词法分析器(Lexer) 基于有限状态机(FSM) Token 流 输出到语法分析器(Parser) 关键规则 • 关键字匹配 • 标识符识别 • 数字字面量 • 运算符/分隔符 • 注释跳过 • 空白跳过 Token结构 • 类型(Type) • 值(Value) • 行号(Line) • 列号(Column) • 文件路径(可选)

这张图把整个流程串起来了。从源代码进去,经过词法分析器一顿操作,吐出一串Token。这些Token就是后面语法分析器要吃的「食材」。

3.6 实现中的几个坑

我做了这么多年EDA工具,词法分析器踩过的坑不少。挑几个典型的说说:

  • 注释嵌套问题:Verilog不支持注释嵌套。你写 /* /* */ */,第一个 */ 就结束了注释。我见过有人写嵌套注释导致整个模块被注释掉的惨案。
  • 字符串中的关键字:VHDL的字符串 "entity" 里的 entity 不是关键字。词法分析器必须区分字符串上下文。
  • 数字解析的边界情况:比如 'hFF 省略位宽,或者 8'bx 带不定态。我建议在词法分析阶段只做初步解析,详细的语义检查留给语法分析。
  • Unicode支持:虽然Verilog/VHDL标准没强制要求,但现代工具链最好支持UTF-8编码的注释。我遇到过客户用中文注释导致乱码的问题。

我的习惯:在实现词法分析器时,我会先写一个简单的版本,只支持核心语法。然后逐步添加对边界情况的支持。这样调试起来轻松很多,不会一开始就被各种奇葩输入搞崩溃。

好了,词法分析这块就聊到这儿。记住一句话:词法分析是编译器的「门卫」,它把好第一道关,后面的工作才能顺利进行。你把这个基础打牢了,后面学语法分析、AST构建就会轻松很多。


专注资料整理