第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重构工具中的位置,我画了张图:

Verilog代码重构工具 - 词法分析阶段 Verilog源代码 词法分析器 (Lexer) 正则匹配 → 切分 → 生成Token Token流 语法分析器 (Parser) 下一章内容 Token包含信息 • 类型 (Type) • 值 (Value) • 行号 (Line) • 列号 (Column) 示例: Token(KEYWORD, 'module', L1:1)

这张图展示了整个流程:源代码进入词法分析器,经过正则匹配和切分,输出结构化的Token流,然后交给下一阶段的语法分析器处理。

你想想看,没有词法分析这一步,后面的语法分析根本没法做。就像盖房子,词法分析就是打地基。地基不牢,房子盖得再漂亮也得塌。

好了,词法分析的基础就讲到这里。代码我已经给你了,建议你拿一段真实的Verilog代码跑一跑,看看Token流长什么样。遇到问题很正常,调试几次就熟了。


公众号:蓝海资料掘金营,微信deep3321