第2章 Verilog语法解析基础:词法分析入门
好,咱们直接进入正题。
做HDL代码重构工具,第一步是什么?不是写复杂的逻辑综合算法,也不是搞什么高大上的AI。第一步,是让工具能「读懂」Verilog代码。说白了,就是得把一堆文本字符串,变成计算机能理解的结构化数据。
这个过程,就叫词法分析(Lexical Analysis)。
2.1 词法分析到底在干什么?
我打个比方你就明白了。你拿到一本英文小说,想统计里面每个单词出现的次数。你会怎么做?
你不会直接去分析语法结构,而是先把句子拆成一个个单词和标点符号。比如 "I love FPGA." 拆成 ["I", "love", "FPGA", "."]。
词法分析干的就是这个活。它把Verilog源代码,从头到尾扫描一遍,按照语法规则,切分成一个个有意义的「词法单元」(Token)。
核心概念:
词法分析器(Lexer)的输入是一串字符,输出是一串Token流。
举个例子,对于这段Verilog代码:
module counter(input clk, input rst_n);
reg [3:0] count;
endmodule
词法分析后,会得到类似这样的Token序列:
Token('KEYWORD', 'module')
Token('IDENTIFIER', 'counter')
Token('SYMBOL', '(')
Token('KEYWORD', 'input')
Token('IDENTIFIER', 'clk')
Token('SYMBOL', ',')
Token('KEYWORD', 'input')
Token('IDENTIFIER', 'rst_n')
Token('SYMBOL', ')')
Token('SYMBOL', ';')
Token('KEYWORD', 'reg')
Token('SYMBOL', '[')
Token('NUMBER', '3')
Token('SYMBOL', ':')
Token('NUMBER', '0')
Token('SYMBOL', ']')
Token('IDENTIFIER', 'count')
Token('SYMBOL', ';')
Token('KEYWORD', 'endmodule')
嗯,这里要注意:每个Token都包含两个信息——类型(Type)和值(Value)。类型告诉我们是关键字还是标识符,值就是具体的文本内容。
2.2 Token定义——给每个词贴上标签
Token怎么定义?我习惯用Python的枚举类来做。清晰,好扩展。
from enum import Enum, auto
class TokenType(Enum):
# 关键字
MODULE = auto()
ENDMODULE = auto()
INPUT = auto()
OUTPUT = auto()
INOUT = auto()
REG = auto()
WIRE = auto()
ASSIGN = auto()
ALWAYS = auto()
IF = auto()
ELSE = auto()
CASE = auto()
ENDCASE = auto()
BEGIN = auto()
END = auto()
POSEDGE = auto()
NEGEDGE = auto()
# 标识符
IDENTIFIER = auto()
# 字面量
NUMBER = auto()
STRING = auto()
# 运算符
PLUS = auto()
MINUS = auto()
ASSIGN_OP = auto() # <=
EQUAL = auto() # ==
# 分隔符
LPAREN = auto() # (
RPAREN = auto() # )
LBRACKET = auto() # [
RBRACKET = auto() # ]
SEMICOLON = auto() # ;
COMMA = auto() # ,
COLON = auto() # :
# 特殊
EOF = auto() # 文件结束
UNKNOWN = auto() # 无法识别的字符
Token类本身也很简单:
class Token:
def __init__(self, type: TokenType, value: str, line: int, column: int):
self.type = type
self.value = value
self.line = line # 行号,方便报错定位
self.column = column # 列号
def __repr__(self):
return f'Token({self.type.name}, {self.value!r}, L{self.line}:{self.column})'
个人经验: 一定要记录行号和列号!我在项目中遇到过,解析一个几千行的Verilog文件,报错只给个「语法错误」,你根本不知道错在哪。有了行号列号,调试效率翻倍。
2.3 正则表达式基础——词法分析的核心武器
词法分析怎么实现?最常用的方法就是正则表达式。
正则表达式,说白了就是一套「模式匹配」的语言。你告诉它「我要找这样的字符串」,它就能帮你从文本里揪出来。
Verilog词法分析中,常用的正则模式:
| Token类型 | 正则表达式 | 说明 |
|---|---|---|
| 关键字 | module|endmodule|input|output|... |
精确匹配关键字列表 |
| 标识符 | [a-zA-Z_][a-zA-Z0-9_$]* |
以字母或下划线开头,后跟字母数字下划线美元符 |
| 数字 | \d+ 或 \d'[bhod][01xz?]+ |
十进制数或Verilog进制数(如 8'hFF) |
| 注释 | //.* 或 /\*[\s\S]*?\*/ |
单行注释和多行注释 |
| 字符串 | "[^"]*" |
双引号括起来的字符串 |
| 运算符 | <= 或 == 或 \+ 等 |
注意多字符运算符要优先匹配 |
避坑指南: 我曾经在写词法分析器时,把 <=(非阻塞赋值)和 <(小于号)搞混了。正则匹配顺序很重要!一定要先匹配长的、特殊的模式,再匹配短的、通用的模式。否则 <= 会被拆成 < 和 = 两个Token。
2.4 Python re模块实战——手写一个简易词法分析器
理论说完了,咱们直接上代码。Python的 re 模块就是干这个的。
import re
class VerilogLexer:
def __init__(self, source_code: str):
self.source = source_code
self.pos = 0 # 当前扫描位置
self.line = 1
self.column = 1
self.tokens = []
# 定义Token模式,顺序很重要!
self.token_patterns = [
# 先匹配多字符运算符
('ASSIGN_OP', r'<='),
('EQUAL', r'=='),
('PLUS', r'\+'),
('MINUS', r'-'),
# 关键字(必须放在标识符前面)
('KEYWORD', r'\b(module|endmodule|input|output|inout|reg|wire|assign|always|if|else|case|endcase|begin|end|posedge|negedge)\b'),
# 标识符
('IDENTIFIER', r'[a-zA-Z_][a-zA-Z0-9_$]*'),
# 数字(支持Verilog进制数)
('NUMBER', r"\d+'[bhod][01xz?]+|\d+"),
# 字符串
('STRING', r'"[^"]*"'),
# 注释(直接跳过,不生成Token)
('COMMENT', r'//[^\n]*|/\*[\s\S]*?\*/'),
# 分隔符
('SYMBOL', r'[()\[\];,:]'),
# 空白(跳过)
('WHITESPACE', r'\s+'),
]
# 编译所有正则
self.compiled = [(name, re.compile(pattern)) for name, pattern in self.token_patterns]
def tokenize(self):
while self.pos < len(self.source):
match = None
for token_name, pattern in self.compiled:
match = pattern.match(self.source, self.pos)
if match:
text = match.group(0)
# 注释和空白直接跳过
if token_name in ('COMMENT', 'WHITESPACE'):
# 更新行号列号
self._update_position(text)
self.pos = match.end()
break
# 生成Token
token = Token(
type=self._map_token_type(token_name, text),
value=text,
line=self.line,
column=self.column
)
self.tokens.append(token)
self._update_position(text)
self.pos = match.end()
break
if not match:
# 无法识别的字符
raise SyntaxError(
f"无法识别的字符 '{self.source[self.pos]}' "
f"在第 {self.line} 行第 {self.column} 列"
)
# 添加文件结束Token
self.tokens.append(Token(TokenType.EOF, '', self.line, self.column))
return self.tokens
def _update_position(self, text):
for char in text:
if char == '\n':
self.line += 1
self.column = 1
else:
self.column += 1
def _map_token_type(self, name, text):
if name == 'KEYWORD':
return TokenType[text.upper()]
elif name == 'SYMBOL':
symbol_map = {
'(': TokenType.LPAREN,
')': TokenType.RPAREN,
'[': TokenType.LBRACKET,
']': TokenType.RBRACKET,
';': TokenType.SEMICOLON,
',': TokenType.COMMA,
':': TokenType.COLON,
}
return symbol_map.get(text, TokenType.UNKNOWN)
elif name == 'ASSIGN_OP':
return TokenType.ASSIGN_OP
elif name == 'EQUAL':
return TokenType.EQUAL
elif name == 'PLUS':
return TokenType.PLUS
elif name == 'MINUS':
return TokenType.MINUS
elif name == 'IDENTIFIER':
return TokenType.IDENTIFIER
elif name == 'NUMBER':
return TokenType.NUMBER
elif name == 'STRING':
return TokenType.STRING
return TokenType.UNKNOWN
使用起来很简单:
code = """
module counter(input clk, input rst_n);
reg [3:0] count;
always @(posedge clk or negedge rst_n) begin
if (!rst_n)
count <= 4'd0;
else
count <= count + 1'b1;
end
endmodule
"""
lexer = VerilogLexer(code)
tokens = lexer.tokenize()
for token in tokens:
print(token)
我建议: 实际项目中,别用纯手工写的正则词法分析器去解析大型工程。Verilog语法比想象中复杂,比如 `timescale、`include 这些编译指令,还有系统函数 $display,都需要特殊处理。但作为学习工具,理解原理足够了。
2.5 本章知识体系总览
为了让你更直观地理解词法分析在整个HDL重构工具中的位置,我画了张图:
这张图展示了整个流程:源代码进入词法分析器,经过正则匹配和切分,输出结构化的Token流,然后交给下一阶段的语法分析器处理。
你想想看,没有词法分析这一步,后面的语法分析根本没法做。就像盖房子,词法分析就是打地基。地基不牢,房子盖得再漂亮也得塌。
好了,词法分析的基础就讲到这里。代码我已经给你了,建议你拿一段真实的Verilog代码跑一跑,看看Token流长什么样。遇到问题很正常,调试几次就熟了。
公众号:蓝海资料掘金营,微信deep3321