3.4 词法错误、源码位置与诊断
词法错误往往是用户第一次感受到编译器质量的地方。弱词法分析器(lexer)只会说 “invalid token”。有用的词法分析器会说明哪段文本非法、它从哪里开始、为什么扫描失败、用户可以怎样修复。诊断不是编译过程上方的一层装饰;它是编译器契约的一部分。
词法分析器对字符级位置负有独特责任。后续阶段可能知道某个语句格式不对,但词法分析器知道字符串字面量从哪里开始、坏转义(escape)出现在哪里、未知字符在哪里打断扫描。
源码位置与范围
源码位置通常包含偏移量(offset)、行(line)和列(column)。偏移量对工具方便,因为它能直接索引源码缓冲区。行和列对人方便。跨度(span)是一对位置或偏移量,用来覆盖一段源码范围。
对于输入:
let bad = @value;一个好的词法诊断可能是:
error[LEX001]: unexpected character '@'
--> example.lang:1:11
1 | let bad = @value;
| ^
= hint: remove it or use a valid operator主要跨度(primary span)应该覆盖最小但有用的范围。非法单字符就是一个字符。未闭合字符串的跨度可能从开始引号延伸到换行或 EOF。非法转义的跨度可以只覆盖该转义序列。
未闭合结构
未闭合字符串和注释很特殊,因为它们会影响同步。如果词法分析器看到:
let s = "hello
let x = 1;它必须决定字符串错误在换行处结束,还是吞掉整个文件剩余部分。很多语言不允许普通字符串跨行,所以词法分析器会报告开始引号,并在下一行恢复。块注释不同:
/* open comment
let x = 1;如果块注释允许跨行,未闭合块注释通常会一直消费到 EOF。报告一个清晰诊断,比对原本处于注释尝试内部的文本产生几十个假语法分析错误更好。
恢复策略
恢复是在真实错误之后继续前进,同时不欺骗用户。词法分析器可以跳过一个坏字符、跳到行边界、合成词法单元(token)、发出 error token,或者停止扫描。每种选择都有成本。
跳过一个字符保留最多上下文,但如果原问题是未闭合字符串,可能造成级联错误。跳到下一行能快速重新同步,但会丢掉当前语句余下部分。合成词法单元可以帮助语法分析器继续,但诊断必须明确说明该词法单元是编译器补出来的。发出 error token 让语法分析器看见词法失败,但会让语法恢复更复杂。
策略应该被文档化并测试。对于教学语言,一个合理策略可能是:
illegal character -> report, skip one character
unterminated string -> report, recover at newline
invalid escape -> report, continue inside string
unterminated block comment -> report once, recover at EOFSource Position 需要精确模型
选择一个内部坐标,通常是 UTF-8 字节偏移量,再通过行首表(line-start table)按需得到显示行/列。Unicode 码点(code point)、UTF-16 码元(code unit)、终端显示列可能不同;IDE 协议往往要 UTF-16,而编译器文件习惯字节偏移量。转换应只在边界发生,文档化并测试非 ASCII 标识符与制表符。
遇到畸形词法单元时,要消费足够字符保证前进,却不要吞掉无关语法。非法字符可以成为一个 error token;未闭合字符串通常扫描到换行/EOF,同时保留开始引号的范围。报一次主要错误,再让后续阶段看见定义清楚的 error token 或按产品策略停止。
Diagnostic 质量检查
- 指向最小有用范围,再用次要标签(secondary label)标出开始分隔符/相关声明。
- 不要每次从文件开头重算列;行映射(line map)让多错误情况不至于呈平方级。
- 测试应快照精确范围/消息,而不只断言“发生错误”。