4. 词法分析器:错误报告与恢复
任务目标
为词法分析器增加两类能力:错误检测(识别非法字符并报告行号、列号)与错误恢复(跳过非法字符后继续分析后续)。最终通过一组包含合法与非法输入的测试,验证分析器能同时输出正确的流和完整的错误列表。
实现方案
在递归下降的词法扫描循环中,当遇到不属于任何类别的字符时,记录错误位置(行、列),然后跳过该字符并继续扫描。错误信息统一收集到列表中,不中断整体分析流程。核心数据结构为(类型、值、行、列)与LexError(消息、行、列)。
完整代码
from dataclasses import dataclass
from typing import List
@dataclass
class :
type: str
value: str
line: int
col: int
@dataclass
class LexError:
message: str
line: int
col: int
class Lexer:
def __init__(self, source: str):
self.src = source
self.pos = 0
self.line = 1
self.col = 1
self.tokens: List[] = []
self.errors: List[LexError] = []
def _advance(self) -> str:
ch = self.src[self.pos]
self.pos += 1
if ch == '\n':
self.line += 1
self.col = 1
else:
self.col += 1
return ch
def _peek(self, offset: int = 0) -> str:
idx = self.pos + offset
return self.src[idx] if idx < len(self.src) else ''
def _add_(self, type_: str, value: str, line: int, col: int):
self.tokens.append((type_, value, line, col))
def _skip_whitespace(self):
while self.pos < len(self.src) and self._peek() in ' \t\r\n':
self._advance()
def _read_number(self):
line, col = self.line, self.col
start = self.pos
while self.pos < len(self.src) and self._peek().isdigit():
self._advance()
self._add_('NUMBER', self.src[start:self.pos], line, col)
def _read_identifier(self):
line, col = self.line, self.col
start = self.pos
while self.pos < len(self.src) and (self._peek().isalnum() or self._peek() == '_'):
self._advance()
self._add_('IDENT', self.src[start:self.pos], line, col)
def tokenize(self):
while self.pos < len(self.src):
self._skip_whitespace()
if self.pos >= len(self.src):
break
ch = self._peek()
line, col = self.line, self.col
if ch.isdigit():
self._read_number()
elif ch.isalpha() or ch == '_':
self._read_identifier()
elif ch in '+-*/()=':
self._advance()
self._add_(ch, ch, line, col)
else:
# 非法字符:报告并跳过
self._advance()
self.errors.append(LexError(f'非法字符: {ch!r}', line, col))
return self.tokens, self.errors
# 最小验证闭环
if __name__ == '__main__':
source = 'x = 12 + @ 34\nabc $ 5'
lexer = Lexer(source)
tokens, errors = lexer.tokenize()
print('--- Tokens ---')
for t in tokens:
print(f'{t.type:8s} {t.value!r:6s} @ {t.line}:{t.col}')
print('--- Errors ---')
for e in errors:
print(f'{e.message} @ {e.line}:{e.col}')
运行与验证
- 运行方式:将上述代码保存为
lexer_recovery.py,执行python3 lexer_recovery.py。 - 预期输出(由代码逻辑直接推出):
--- Tokens --- IDENT 'x' @ 1:1 = '=' @ 1:3 NUMBER '12' @ 1:5 + '+' @ 1:8 NUMBER '34' @ 1:11 IDENT 'abc' @ 2:1 NUMBER '5' @ 2:7 --- Errors --- 非法字符: '@' @ 1:10 非法字符: '$' @ 2:5 - 通过条件:
@和$被报告为错误且被跳过,其后的34和5仍被正确识别为NUMBER,说明恢复机制生效;行号列号与源码中字符实际位置一致。
关键说明
- 错误恢复策略为单字符跳过:遇到非法字符时消耗该字符并继续,不进入死循环。
- 行号列号在
_advance()中统一维护,保证与错误位置均准确。 - 若需扩展(如字符串、注释),只需在
tokenize()中增加分支,错误处理逻辑无需改动。