3.3 手写词法器与表驱动词法器
定义词法单元(token)规则之后,还要实现扫描器(scanner)。两类实现风格反复出现:手写词法分析器(lexer)和表驱动/生成式词法分析器。两者都可以很优秀。真正的选择取决于语言复杂度、诊断要求、团队偏好、性能约束和工具链目标。
手写词法分析器是普通代码,会调用 peek、advance、match、identifier、number、string 等辅助函数。表驱动(table-driven)词法分析器把 DFA 表示成数据。它分类每个字符,查表得到下一状态,记录接受状态,并输出最长词法单元。词法分析器生成器(lexer generator)介于两者之间:开发者写 regex 规则,工具生成表或源码。
手写词法分析器
需要定制诊断或特殊词法规则的编译器经常采用手写扫描器。它可以用普通调试器(debugger)单步调试,因为控制流就是源码。它也容易产生针对性消息,例如 “numeric literal requires digits after .”,因为扫描器知道到底是哪条分支失败。
一个典型手写扫描器循环像这样:
scanToken():
skipWhitespaceAndComments()
start = position
c = advance()
if isLetter(c): return identifierOrKeyword(start)
if isDigit(c): return number(start)
switch c:
case '=': return match('=') ? EQEQ : EQ
case '"': return string(start)
default: return errorToken(start, "unexpected character")危险在于规则漂移。如果词法单元规则分散在很多分支里,实现会变得难以审计。你需要系统测试每个运算符前缀、字面量边界情况、关键字冲突和错误恢复路径。
表驱动词法分析器
表驱动词法分析器把自动机执行变成数据查询。扫描器维护一个状态,并把每个字符分类成 letter、digit、space、= 或 other 等字符类(character class)。然后它询问:
next = transition[currentState][charClass]scanner 还会记录最近一次接受状态和位置。这就是 DFA 中最长匹配的实现方式。它可能会多读一个字符,发现无法继续匹配,然后从最近接受点发出词法单元,并从那里恢复扫描。
优势是规则性。一个紧凑循环可以扫描很多词法单元种类。生成式词法分析器可以压缩转移表(transition table),合并等价字符类,并最小化自动机。代价是自定义诊断可能更难,除非生成器支持语义动作(semantic action)或错误标签。
生成式词法分析器
词法分析器生成器会自动完成从词法单元 regex 到可执行扫描器的路线。生成器可以合并词法单元 NFA,转换成 DFA,最小化或压缩它,并输出代码。这减少了手工维护扫描器逻辑的成本,也让实现更接近形式化的词法单元规范。
生成器不能替代语言设计。你仍然要决定规则优先级、跳过通道、关键字处理、Unicode 策略、字面量规则、错误消息和源码跨度约定。你也仍然需要金标准测试(golden test),因为简洁的 regex 规范也可能编码了错误语言。
按变化速度选择实现形状
小语言若有缩进(indentation)、嵌套注释(nested comment)、插值(interpolation)或特殊诊断,手写词法分析器通常最清楚:使用一个游标抽象(cursor abstraction)、scanNumber/scanString 等命名辅助函数,并禁止未记录的回退。表驱动/生成式词法分析器则适合大量正则规则、声明式文法和性能敏感场景;无论实现来源,语法分析器看到的词法单元/源码跨度 API 必须一致。
Hybrid 很常见
许多真实词法分析器用 DFA 处理标识符、数字、运算符,再以小型手写模式处理字符串、插值、指令(directive)、缩进。不要先优化表结构;先用金标准语料验证两个实现对每个文件给出相同的种类、payload、跨度和诊断。