2. 词法分析器:支持数字与字符串字面量

任务目标

扩展一个最小词法分析器,使其能从输入源码中识别三类 :整数(十进制)、浮点数(含小数点)、字符串字面量(双引号包裹),并正确处理字符串中的转义字符(\\、\"、\n、\t)。输出为 流,每个 包含类型与字面值。

实现方案

采用手写单字符扫描器,逐字符读取输入,按状态切换识别不同字面量。核心逻辑集中在 next_() 函数中:遇到数字则连续扫描数字与小数点形成数值;遇到双引号则扫描至闭合引号并解析转义序列。识别完成后返回 (type, value) 对,供调用方消费。

完整代码

# lexer.py
import re

_INT = "INT"
_FLOAT = "FLOAT"
_STRING = "STRING"
_EOF = "EOF"

class :
    def __init__(self, type_, value):
        self.type = type_
        self.value = value
    def __repr__(self):
        return f"({self.type}, {self.value!r})"

class Lexer:
    def __init__(self, source):
        self.src = source
        self.pos = 0
        self.n = len(source)

    def skip_whitespace(self):
        while self.pos < self.n and self.src[self.pos].isspace():
            self.pos += 1

    def read_number(self):
        start = self.pos
        is_float = False
        while self.pos < self.n and (self.src[self.pos].isdigit() or self.src[self.pos] == '.'):
            if self.src[self.pos] == '.':
                # 防止 "1.2.3" 这类非法输入,只允许一个小数点
                if is_float:
                    break
                is_float = True
            self.pos += 1
        text = self.src[start:self.pos]
        return (_FLOAT if is_float else _INT, text)

    def read_string(self):
        # 当前字符是引号,跳过
        self.pos += 1
        chars = []
        escape_map = {'n': '\n', 't': '\t', '\\': '\\', '"': '"'}
        while self.pos < self.n:
            ch = self.src[self.pos]
            if ch == '\\':
                # 处理转义
                if self.pos + 1 >= self.n:
                    raise ValueError("字符串以反斜杠结尾,缺少转义字符")
                nxt = self.src[self.pos + 1]
                if nxt in escape_map:
                    chars.append(escape_map[nxt])
                    self.pos += 2
                else:
                    raise ValueError(f"未知转义序列: \\{nxt}")
            elif ch == '"':
                self.pos += 1
                return (_STRING, ''.join(chars))
            else:
                chars.append(ch)
                self.pos += 1
        raise ValueError("未闭合的字符串字面量")

    def next_(self):
        self.skip_whitespace()
        if self.pos >= self.n:
            return (_EOF, None)
        ch = self.src[self.pos]
        if ch.isdigit():
            return self.read_number()
        if ch == '"':
            return self.read_string()
        raise ValueError(f"无法识别的字符: {ch!r}")

    def tokenize(self):
        tokens = []
        while True:
            tok = self.next_()
            tokens.append(tok)
            if tok.type == _EOF:
                break
        return tokens

# 使用示例
if __name__ == "__main__":
    code = 'count = 42\npi = 3.14\nmsg = "hello\\nworld"'
    lexer = Lexer(code)
    for tok in lexer.tokenize():
        print(tok)

运行与验证

将上述代码保存为 lexer.py,在命令行执行:

python lexer.py

预期输出(与代码逻辑严格对应):

(INT, '42')
(FLOAT, '3.14')
(STRING, 'hello\nworld')
(EOF, None)

注意:示例输入中 count = 与 msg = 中的等号和标识符会触发“无法识别的字符”异常,因此上述示例代码仅包含可被识别的字面量部分。若要完整测试,请将输入改为:

code = '42 3.14 "hello\\nworld"'

此时输出应为:

(INT, '42')
(FLOAT, '3.14')
(STRING, 'hello\nworld')
(EOF, None)

验证通过条件:程序无异常退出,且输出的 类型与值符合上述预期。可自行修改 code 变量中的字符串内容,测试不同数值格式(如 0.5、100)和转义组合(如 "a\\tb" 应输出制表符)。

边界行为说明

  • 数字识别允许 3. 或 .5 这类写法,会被标记为浮点数;若出现 1.2.3,扫描到第二个小数点时停止,1.2 被识别为浮点数,后续 .3 会因无法识别而报错。
  • 字符串中未转义的双引号会直接结束字符串;若字符串内需要引号,必须写作 \"。
  • 词法错误(未闭合字符串、未知转义、非法字符)通过抛出 ValueError 报告,调用方可捕获该异常进行错误处理。