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}')

运行与验证

  1. 运行方式:将上述代码保存为lexer_recovery.py,执行python3 lexer_recovery.py。
  2. 预期输出(由代码逻辑直接推出):
    --- 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
    
  3. 通过条件:@和$被报告为错误且被跳过,其后的34和5仍被正确识别为NUMBER,说明恢复机制生效;行号列号与源码中字符实际位置一致。

关键说明

  • 错误恢复策略为单字符跳过:遇到非法字符时消耗该字符并继续,不进入死循环。
  • 行号列号在_advance()中统一维护,保证与错误位置均准确。
  • 若需扩展(如字符串、注释),只需在tokenize()中增加分支,错误处理逻辑无需改动。