词法分析器开发(下):操作符与分隔符识别、错误处理机制、单元测试编写
好,咱们接着聊。上一节我们把标识符和关键字给拿下了,这一节要啃的,是操作符和分隔符。说实话,这部分看着简单,但坑不少。我当年第一次写词法分析器时,就在操作符的贪婪匹配上栽过跟头——<= 被拆成了 < 和 =,结果后面语法分析直接崩了。嗯,咱们今天就把这些坑一个个填平。
操作符识别:贪婪匹配与优先级
HDL里的操作符,有单字符的,比如 +、-、*;也有双字符的,比如 <=、=>、/=。你想想看,如果读到 <,你怎么知道后面有没有 =?
我的做法很简单:先看两个字符,能匹配双字符操作符就认双字符,不行再退回单字符。这就是所谓的「最长匹配」原则。
def _try_match_operator(self, pos):
# 先尝试双字符
if pos + 1 < len(self.source):
two_char = self.source[pos:pos+2]
if two_char in self.operator_map:
return self.operator_map[two_char], 2
# 再尝试单字符
one_char = self.source[pos]
if one_char in self.operator_map:
return self.operator_map[one_char], 1
return None, 0
** 幂运算符,加一行字典就行。
这里有个细节:-- 在 VHDL 里是注释开头,但在 Verilog 里不是。所以操作符表得根据语言来配。我见过有人把所有符号塞进一个表里,结果解析 VHDL 时把 -- 当成了两个减号……嗯,那画面太美。
分隔符识别:括号、逗号、分号
分隔符其实比操作符还简单,因为它们都是单字符。但简单归简单,容易漏。比如 ; 在 HDL 里是语句结束,你要是漏了,语法分析器会报一堆莫名其妙的错。
我一般把分隔符也放进一个字典里,跟操作符分开管理:
self.delimiter_map = {
'(': 'LPAREN',
')': 'RPAREN',
',': 'COMMA',
';': 'SEMICOLON',
':': 'COLON',
'.': 'DOT',
}
识别逻辑就更直接了——查表,命中就返回,没命中就报错。
: 在 VHDL 里也用于信号赋值 <= 的一部分,但单独出现时是分隔符。所以你得先处理双字符操作符,再处理单字符分隔符。顺序错了,解析就乱套。
错误处理机制:别让一个错卡死整个文件
词法分析器的错误处理,说白了就是「发现错误,记录错误,继续往下走」。你不能一遇到非法字符就抛异常退出,那样用户得疯——改一个错,跑一次,再改一个,再跑一次……
我设计的错误处理流程是这样的:
- 遇到非法字符:记录错误信息(行号、列号、非法字符内容)
- 跳过该字符:继续解析下一个
- 收集所有错误:最后一次性返回给调用方
def _report_error(self, message, line, column):
self.errors.append({
'message': message,
'line': line,
'column': column,
})
# 不抛异常,继续解析
我曾经在一个项目里遇到过这种情况:用户写了一个 @ 符号,这在 Verilog 里是非法的。如果我的词法分析器直接崩溃,用户得花半小时定位问题。但有了错误收集机制,我可以一次性告诉他:「第3行第5列有个非法字符 @,第12行第8列有个未闭合的字符串」。这才叫用户体验。
单元测试编写:把边界情况都测一遍
好,代码写完了,怎么保证它没问题?写单元测试。我个人的习惯是:先写测试,再写代码(TDD)。但如果你已经写完了代码,也别慌,补测试就行。
测试用例要覆盖哪些场景?我列了个清单:
| 测试类别 | 具体用例 | 预期结果 |
|---|---|---|
| 操作符 | <=、=>、/= |
识别为双字符操作符 |
| 操作符 | +、-、* |
识别为单字符操作符 |
| 分隔符 | (、)、; |
识别为对应分隔符 |
| 错误恢复 | @、# |
记录错误,继续解析 |
| 混合输入 | a <= b + c; |
正确识别所有 Token |
用 Python 的 unittest 框架写起来很顺手:
import unittest
class TestLexer(unittest.TestCase):
def test_operator_two_char(self):
lexer = Lexer("a <= b")
tokens = lexer.tokenize()
self.assertEqual(tokens[1].type, 'LE')
def test_error_recovery(self):
lexer = Lexer("a @ b")
tokens = lexer.tokenize()
self.assertGreater(len(lexer.errors), 0)
self.assertEqual(len(tokens), 2) # a 和 b 被正确识别
if __name__ == '__main__':
unittest.main()
知识体系总览
这一节的内容,我用一张 SVG 图来总结。你看一眼,就能把操作符识别、分隔符识别、错误处理、单元测试这四块的关系理清楚。
这张图把四个模块的关系画得很清楚。操作符和分隔符是「识别层」,错误处理是「容错层」,单元测试是「质量层」。三层叠加,就是一个靠谱的词法分析器。
好,这一节就到这儿。代码写完了,测试也补上了,接下来就可以放心地交给语法分析器去用了。你想想看,如果每个模块都这么扎实地做,整个工具链会稳得多。
公众号:蓝海资料掘金营,微信deep3321