3.2 最长匹配、关键字、标识符、字面量与注释
词法分析器(lexer)经常遇到多个规则都能匹配同一源码前缀的情况。扫描器必须有确定性策略来选择一个词法单元(token)。多数编程语言使用 最长匹配(maximal munch),也叫 longest match:从当前位置开始,选择任意词法单元规则能接受的最长前缀。如果多个规则匹配了同样长的最长前缀,再使用规则优先级或关键字表。
这个策略不是小细节。它决定 ifx 是一个标识符(identifier),还是 IF 后跟 IDENT(x)。它决定 == 是一个相等运算符(equality operator),还是两个赋值运算符(assignment operator)。它也决定数字和字符串边界如何表现。语法分析器(parser)假设词法分析器已经稳定地做完这些决定。
先最长匹配,再优先级
假设我们有:
IF = if
IDENT = [A-Za-z_][A-Za-z0-9_]*
EQEQ = ==
EQ = =对于输入 ifx,IF 和 IDENT 都能从当前位置开始匹配,但 IDENT 匹配长度 3,而 IF 匹配长度 2。最长匹配选择 IDENT(ifx)。对于输入 if,IF 和 IDENT 都匹配长度 2,此时由优先级(priority)决定。很多词法分析器会把关键字规则放在 IDENT 前面,或者先扫描 IDENT,再通过关键字表(keyword table)改写种类。
优先级规则应该在语言实现中显式写清楚。隐藏的优先级 bug 很痛苦,因为词法化错误会制造误导性的语法分析错误。如果 ifx 被错误拆开,语法分析器可能抱怨 if 后面出现了意外标识符,但真正的 bug 在词法分析器。
关键字与标识符
关键字是最常见的词法冲突例子之一。常见策略有两种。
第一种是让关键字模式(pattern)拥有更高优先级:
IF = if
ELSE = else
RETURN = return
IDENT = [A-Za-z_][A-Za-z0-9_]*第二种是把所有关键字形状的单词都先扫描成 IDENT,再查关键字表:
kind = keywords.get(lexeme) ?? IDENTtable 方式容易扩展,也避免给自动机写大量单独 accepting label。优先级方式在词法分析器生成器(lexer generator)中很自然。两种都合理,关键测试是一样的:if 应该成为关键字,而 ifx、if_、if2 应该保持为标识符。
字面量与注释是有状态边界
数值字面量(numeric literal)、字符串字面量(string literal)和注释仍然属于词法结构,但它们往往需要更细致的状态行为。
数字扫描器必须决定:
2.到底是浮点字面量、整数后跟 dot,还是非法小数。字符串扫描器必须处理转义(escape)、结束引号和换行。注释扫描器必须知道 // 是否到行尾结束、是否存在 /* */、是否允许嵌套块注释。
这些决定会直接影响诊断。未闭合字符串不只是“非法文本”。编译器通常应该指向开始引号,展示扫描范围,并说明字符串是在换行前还是 EOF 前没有关闭。未闭合块注释可能吞掉文件剩余部分,所以恢复策略必须保守。
边界情况才真正定义语言
有十进制(decimal)与成员访问(member access)的语言必须预先决定 1.、.5、1..2、a.b、1e+ 的含义:1. 是浮点还是 INT 后接 dot?.5 合法吗?1..2 是否是区间(range)?最长匹配只会在你明确允许的模式中选最长,不能替语言设计做决定。
字符串同样要规定转义、原始/处理(raw/cooked)、换行、Unicode 转义与非法转义的恢复方式。块注释是否嵌套也必须写清;当要给开始分隔符(opening delimiter)定位错误时,用类似 /* .*? */ 的直觉并不是足够实现模型。
测试矩阵
- 运算符:
=、==、===、>>、>>=。
- 关键字边界:
if、ifx、_if、if2。
- 字面量失败:错误数字、不完整指数、错误转义、换行/EOF 在引号之前。
- 注释邻接:
/、//x、/*x*/y、未闭合块注释。