词法分析基础:正则表达式入门、Python re模块详解、Token定义与分类

各位同学,今天我们来聊聊词法分析。说白了,这就是RTL代码检查工具的第一步——把文本变成有意义的“单词”。我刚开始做EDA工具开发时,总觉得这步很简单,不就是字符串匹配嘛。后来踩了不少坑才明白,词法分析做不好,后面语法分析全是白搭。

正则表达式入门

正则表达式,嗯,就是用来描述字符串模式的工具。你想想看,我们要从Verilog代码里找出所有的module关键字、wire声明、always块,总不能一行行手写if判断吧?正则就是干这个的。

我个人习惯把正则分成三个层次来理解:

  • 字面字符:比如module就匹配"module"这个字符串
  • 元字符.匹配任意字符,*匹配0次或多次,+匹配1次或多次
  • 字符类[a-zA-Z]匹配字母,\d匹配数字

我在项目中遇到过一个问题:用.*匹配注释时,把整个文件都吞掉了。为什么?因为.*是贪婪匹配,它会尽可能多地匹配字符。后来我改用.*?非贪婪模式才解决。

核心要点:正则匹配默认是贪婪的。写RTL检查工具时,一定要小心这个特性。

Python re模块详解

Python的re模块,说白了就是正则引擎的封装。我常用的就四个函数:

函数 作用 适用场景
re.match() 从字符串开头匹配 检查行首关键字
re.search() 搜索整个字符串 查找任意位置的模式
re.findall() 返回所有匹配 提取所有标识符
re.sub() 替换匹配内容 代码预处理

举个例子,我们要从Verilog代码里提取所有wire声明的信号名:

import re

code = "wire clk, rst_n; wire [7:0] data_bus;"
pattern = r'wire\s+([a-zA-Z_]\w*)'
matches = re.findall(pattern, code)
print(matches)  # 输出: ['clk', 'rst_n']

等等,这里有个坑。上面的正则只能匹配单个信号名,遇到wire [7:0] data_bus这种带位宽的就不行了。我曾经在这个问题上卡了半天,后来改成这样:

pattern = r'wire\s*(?:\[\d+:\d+\])?\s*([a-zA-Z_]\w*)'

小技巧:写正则时,先用re.compile()编译,能提高重复匹配的效率。我习惯把所有正则放在一个字典里统一管理。

Token定义与分类

Token是什么?就是把源代码拆成一个个有意义的“单词”。比如always @(posedge clk),拆成Token就是:

  • ALWAYS 关键字
  • @ 操作符
  • ( 左括号
  • posedge 关键字
  • clk 标识符
  • ) 右括号

我一般把Token分成这几类:

Token类型 示例 说明
关键字 module, always, wire Verilog保留字
标识符 clk, rst_n, data_bus 用户定义的名称
操作符 +, -, &&, || 运算符号
分隔符 ;, (, ), {, } 代码结构符号
字面量 1'b0, 32'hFF, 100 数值常量
注释 // ... 或 /* ... */ 代码注释

定义Token时,我建议用枚举类:

from enum import Enum, auto

class TokenType(Enum):
    KEYWORD = auto()
    IDENTIFIER = auto()
    OPERATOR = auto()
    DELIMITER = auto()
    LITERAL = auto()
    COMMENT = auto()
    WHITESPACE = auto()
    UNKNOWN = auto()

注意:Token的顺序很重要。比如always既是关键字,也可能被误匹配为标识符。我习惯把关键字放在前面匹配,标识符放在后面。

知识体系总览

下面这张图,是我做RTL检查工具时总结的词法分析流程。你仔细看看,每个环节都有讲究:

词法分析知识体系 RTL源代码 预处理(去注释) 正则表达式匹配 关键字 标识符 Token序列输出

这张图展示了词法分析的完整流程。从源代码开始,先做预处理去掉注释,然后用正则匹配各类Token,最后输出Token序列。每个环节都有坑,我一个个说。

实战中的避坑指南

我曾经在写Verilog检查工具时,遇到一个诡异的问题:reg [3:0] data这个声明,正则匹配总是漏掉data。排查了半天,发现是[3:0]里的方括号被正则解释成了字符类。后来我加了转义才解决。

经验总结:写正则时,一定要考虑Verilog语法中的特殊字符。方括号、花括号、反斜杠这些,该转义就转义。

另一个常见问题是注释处理。比如wire /* comment */ clk,如果先做正则匹配,注释里的内容会干扰结果。我建议先做预处理,把注释替换成空格,再开始词法分析。

嗯,这里要注意:替换注释时,字符串里的注释不能动。比如assign str = "// not a comment";,这里的//是字符串内容,不是注释。这个边界情况,我当年调试了整整一个下午。

最后说一句,Token定义不是越细越好。我见过有人把+-都单独定义成不同Token,结果后面语法分析时代码量翻倍。我的建议是:同类操作符合并,比如算术运算符、逻辑运算符各归一类。