TPC 侧取缔对 Python 标准库 ast 模块的依赖,改用 lib/vast 包: - Nodes: 与 CPython 3.12 ast 完全同构的 131 个节点类(含抽象基类继承) - Tokenizer: 词法分析(缩进/字符串/f-string/数字/运算符/软关键字) - ExprParser: 表达式递归下降解析(优先级/lambda/推导式/f-string/切片) - Parser: 语句解析(def/class/if/for/try/with/match/import/注解) - Utils/Unparser: walk/dump/unparse/copy_location/literal_eval 等辅助 - 软关键字 fn/fun/func/function 并入解析器, 废除 ExpandSoftKeywords 预处理 - lib/_bootstrap 将 lib 目录加入 sys.path, import vast as ast 零修改替换 - 52 个文件 import ast 替换为 import vast as ast; 94 文件差分测试 0 失败
491 lines
20 KiB
Python
491 lines
20 KiB
Python
"""vast.tokenizer - Viper/Python 词法分析器
|
||
|
||
将源码字符串切分为 token 流,与 CPython 3.12 ``tokenize`` 语义对齐:
|
||
|
||
- 支持缩进 INDENT/DEDENT、逻辑行 NEWLINE、括号内续行
|
||
- 支持字符串字面量(单引号/双引号/三引号、r/b/u/f 前缀与组合前缀)
|
||
- f-string 作为整体 FSTRING token 输出,内部表达式由 Parser 递归解析
|
||
- 数字字面量(十进制/十六进制/八进制/二进制/浮点/复数 j/下划线分隔)
|
||
- 运算符最长匹配
|
||
|
||
位置信息与 CPython 一致:``col_offset`` 为 UTF-8 字节偏移(非字符偏移),
|
||
行号为 1-based。行列信息通过行起始索引表 + bisect 统一换算,保证跨行
|
||
token(字符串/三引号)的行列准确。
|
||
|
||
软关键字 fn/fun/func/function 不作为独立 token 类型处理(并入解析器),
|
||
tokenizer 产出普通 NAME,由 Parser 在语句首识别为函数定义关键字。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import bisect
|
||
|
||
# =============================================================================
|
||
# Token 类型常量
|
||
# =============================================================================
|
||
ENDMARKER = 'ENDMARKER'
|
||
NAME = 'NAME'
|
||
NUMBER = 'NUMBER'
|
||
STRING = 'STRING'
|
||
FSTRING = 'FSTRING'
|
||
OP = 'OP'
|
||
NEWLINE = 'NEWLINE'
|
||
INDENT = 'INDENT'
|
||
DEDENT = 'DEDENT'
|
||
COMMENT = 'COMMENT'
|
||
NL = 'NL'
|
||
|
||
# 多字符运算符(最长优先,含 Python 3.12 全部运算符)
|
||
_OPERATORS: tuple[str, ...] = (
|
||
'**=', '//=', '<<=', '>>=', '**', '//', '<<', '>>', '<=', '>=', '==', '!=',
|
||
':=', '->', '+=', '-=', '*=', '/=', '%=', '@=', '&=', '|=', '^=', '...',
|
||
'(', ')', '[', ']', '{', '}', ',', ':', '.', ';', '+', '-', '*', '/', '%',
|
||
'@', '&', '|', '^', '~', '<', '>', '=', '!',
|
||
)
|
||
|
||
# 行首空白 + 关键字 + 空白 + 标识符 → 软关键字函数定义(Parser 层处理)
|
||
SOFT_KEYWORDS: frozenset[str] = frozenset({'fn', 'fun', 'func', 'function'})
|
||
|
||
# 字符串前缀字母集合(允许任意顺序/大小写组合,如 fr、Rb、BF)
|
||
_STRING_PREFIX_CHARS: frozenset[str] = frozenset('rRbBuUfF')
|
||
_F_PREFIX = frozenset('fF')
|
||
_R_PREFIX = frozenset('rR')
|
||
_B_PREFIX = frozenset('bB')
|
||
|
||
|
||
class Token:
|
||
"""词法 token。
|
||
|
||
Attributes:
|
||
Type: token 类型(NAME/NUMBER/STRING/FSTRING/OP/NEWLINE/...)
|
||
Value: token 原始文本
|
||
Line: 1-based 行号
|
||
Col: UTF-8 字节偏移(0-based)
|
||
EndLine: 结束行号
|
||
EndCol: 结束列(UTF-8 字节偏移)
|
||
Start: 源码字符偏移(含 UTF-8 字节偏移换算依赖,供 f-string 内部定位)
|
||
End: 源码字符偏移(exclusive)
|
||
"""
|
||
__slots__ = ('Type', 'Value', 'Line', 'Col', 'EndLine', 'EndCol', 'Start', 'End')
|
||
|
||
def __init__(self, tok_type: str, value: str, line: int, col: int,
|
||
end_line: int, end_col: int) -> None:
|
||
self.Type: str = tok_type
|
||
self.Value: str = value
|
||
self.Line: int = line
|
||
self.Col: int = col
|
||
self.EndLine: int = end_line
|
||
self.EndCol: int = end_col
|
||
self.Start: int = -1
|
||
self.End: int = -1
|
||
|
||
def __repr__(self) -> str:
|
||
return (f'Token({self.Type}, {self.Value!r}, '
|
||
f'{self.Line}:{self.Col}-{self.EndLine}:{self.EndCol})')
|
||
|
||
|
||
def _IsIdentStart(ch: str) -> bool:
|
||
"""标识符起始字符:字母或下划线(兼容非 ASCII)。"""
|
||
return ch == '_' or ch.isalpha() or ord(ch) > 127
|
||
|
||
|
||
def _IsIdentPart(ch: str) -> bool:
|
||
"""标识符组成部分:字母/数字/下划线。"""
|
||
return ch == '_' or ch.isalnum() or ord(ch) > 127
|
||
|
||
|
||
class Tokenizer:
|
||
"""Viper/Python 源码 → token 流。
|
||
|
||
Usage::
|
||
|
||
toks: list[Token] = Tokenizer(source).Tokenize()
|
||
"""
|
||
|
||
def __init__(self, source: str) -> None:
|
||
self.Source: str = source
|
||
self.Length: int = len(source)
|
||
self.Pos: int = 0 # 当前字符索引
|
||
self.Tokens: list[Token] = []
|
||
# 括号深度:>0 表示在括号内(逻辑行不结束)
|
||
self.BracketDepth: int = 0
|
||
# 缩进栈:0 为根
|
||
self.Indents: list[int] = [0]
|
||
# 行首标志(决定是否进行缩进分析)
|
||
self.AtLineStart: bool = True
|
||
# 反斜杠续行延续标志:续行后的物理行行首空白不产生 INDENT
|
||
self.ContinuationLine: bool = False
|
||
# 空行/注释行标志:本物理行无实质内容,行尾换行产 NL 而非 NEWLINE
|
||
self.BlankLine: bool = False
|
||
# 注释 token 收集(Parser 忽略,但保留供诊断)
|
||
self.Comments: list[Token] = []
|
||
# 行起始字符索引表(索引 0 对应第 1 行),用于行列换算
|
||
self._LineStarts: list[int] = [0]
|
||
|
||
# ------------------------------------------------------------------
|
||
# 位置换算
|
||
# ------------------------------------------------------------------
|
||
def _PosInfo(self, pos: int) -> tuple[int, int]:
|
||
"""将字符索引换算为 (1-based 行号, UTF-8 字节列偏移)。"""
|
||
idx: int = bisect.bisect_right(self._LineStarts, pos) - 1
|
||
line: int = idx + 1
|
||
col_bytes: int = len(self.Source[self._LineStarts[idx]:pos].encode('utf-8'))
|
||
return line, col_bytes
|
||
|
||
def _Make(self, tok_type: str, start: int, end: int) -> Token:
|
||
"""以 [start, end) 源码区间构造 token。"""
|
||
start_line, start_col = self._PosInfo(start)
|
||
end_line, end_col = self._PosInfo(end)
|
||
tok: Token = Token(tok_type, self.Source[start:end], start_line, start_col,
|
||
end_line, end_col)
|
||
tok.Start = start
|
||
tok.End = end
|
||
return tok
|
||
|
||
def _RegisterNewline(self, pos_after_nl: int) -> None:
|
||
"""登记换行:pos_after_nl 是换行序列之后的字符索引(即新行行首)。"""
|
||
self._LineStarts.append(pos_after_nl)
|
||
|
||
# ------------------------------------------------------------------
|
||
# 主入口
|
||
# ------------------------------------------------------------------
|
||
def Tokenize(self) -> list[Token]:
|
||
"""执行词法分析,返回完整 token 流(以 ENDMARKER 结尾)。"""
|
||
while self.Pos < self.Length:
|
||
ch: str = self.Source[self.Pos]
|
||
if (self.AtLineStart and self.BracketDepth == 0
|
||
and not self.ContinuationLine and ch not in '\r\n'):
|
||
self._HandleIndent()
|
||
if self.Pos >= self.Length:
|
||
break
|
||
continue
|
||
if ch in ' \t\f':
|
||
self.Pos += 1
|
||
continue
|
||
if ch == '\r':
|
||
self.Pos += 1
|
||
if self.Pos < self.Length and self.Source[self.Pos] == '\n':
|
||
self.Pos += 1
|
||
self._HandleNewline(self.Pos)
|
||
continue
|
||
if ch == '\n':
|
||
self.Pos += 1
|
||
self._HandleNewline(self.Pos)
|
||
continue
|
||
if ch == '#':
|
||
self._ScanComment()
|
||
continue
|
||
if ch == '\\':
|
||
self._ScanContinuation()
|
||
continue
|
||
if ch in '([{':
|
||
self.BracketDepth += 1
|
||
self._EmitOp()
|
||
continue
|
||
if ch in ')]}':
|
||
if self.BracketDepth > 0:
|
||
self.BracketDepth -= 1
|
||
self._EmitOp()
|
||
continue
|
||
if ch == "'" or ch == '"' or _IsIdentStart(ch):
|
||
if self._TryScanString():
|
||
continue
|
||
if ch.isdigit():
|
||
self._ScanNumber()
|
||
continue
|
||
if _IsIdentStart(ch):
|
||
self._ScanName()
|
||
continue
|
||
if ch.isdigit() or (ch == '.' and self.Pos + 1 < self.Length
|
||
and self.Source[self.Pos + 1].isdigit()):
|
||
self._ScanNumber()
|
||
continue
|
||
self._EmitOp()
|
||
self._FinishFile()
|
||
return self.Tokens
|
||
|
||
# ------------------------------------------------------------------
|
||
# 换行 / 缩进 / 续行 / 注释
|
||
# ------------------------------------------------------------------
|
||
def _HandleNewline(self, pos_after_nl: int) -> None:
|
||
"""处理物理换行:括号外为 NEWLINE,括号内为 NL。
|
||
|
||
Args:
|
||
pos_after_nl: 换行序列之后的字符索引(新行行首)。
|
||
"""
|
||
# 找换行序列起点(\n 或 \r\n)
|
||
nl_start: int = self.Pos - 1
|
||
if nl_start > 0 and self.Source[nl_start] == '\n' and self.Source[nl_start - 1] == '\r':
|
||
nl_start -= 1
|
||
if self.BracketDepth > 0 or self.BlankLine or (self.AtLineStart and not self.ContinuationLine):
|
||
self.Tokens.append(self._Make(NL, nl_start, self.Pos))
|
||
else:
|
||
self.Tokens.append(self._Make(NEWLINE, nl_start, self.Pos))
|
||
self._RegisterNewline(pos_after_nl)
|
||
self.AtLineStart = True
|
||
self.ContinuationLine = False
|
||
self.BlankLine = False
|
||
|
||
def _HandleIndent(self) -> None:
|
||
"""行首缩进分析:产出 INDENT/DEDENT 或忽略。"""
|
||
start: int = self.Pos
|
||
col: int = 0
|
||
while self.Pos < self.Length:
|
||
ch: str = self.Source[self.Pos]
|
||
if ch == ' ':
|
||
col += 1
|
||
self.Pos += 1
|
||
elif ch == '\t':
|
||
col = (col // 8 + 1) * 8
|
||
self.Pos += 1
|
||
elif ch == '\f':
|
||
self.Pos += 1
|
||
else:
|
||
break
|
||
self.AtLineStart = False
|
||
# 空行/注释行不产生缩进 token,且行尾换行应产 NL(逻辑行不结束)
|
||
if self.Pos >= self.Length or self.Source[self.Pos] in '\r\n#':
|
||
self.BlankLine = True
|
||
return
|
||
cur: int = self.Indents[-1]
|
||
if col > cur:
|
||
self.Indents.append(col)
|
||
self.Tokens.append(self._Make(INDENT, start, self.Pos))
|
||
elif col < cur:
|
||
while self.Indents and self.Indents[-1] > col:
|
||
self.Indents.pop()
|
||
self.Tokens.append(self._Make(DEDENT, self.Pos, self.Pos))
|
||
|
||
def _ScanContinuation(self) -> None:
|
||
"""反斜杠续行:跳过 \\ + 换行(逻辑行内,不产生 NEWLINE)。"""
|
||
if self.Pos + 1 < self.Length and self.Source[self.Pos + 1] in '\r\n':
|
||
self.Pos += 1
|
||
if self.Pos < self.Length and self.Source[self.Pos] == '\r':
|
||
self.Pos += 1
|
||
if self.Pos < self.Length and self.Source[self.Pos] == '\n':
|
||
self.Pos += 1
|
||
self._RegisterNewline(self.Pos)
|
||
self.AtLineStart = True
|
||
self.ContinuationLine = True
|
||
else:
|
||
self.Pos += 1
|
||
|
||
def _ScanComment(self) -> None:
|
||
"""扫描注释到行尾(不产出 COMMENT token,仅保留)。"""
|
||
start: int = self.Pos
|
||
while self.Pos < self.Length and self.Source[self.Pos] not in '\r\n':
|
||
self.Pos += 1
|
||
tok: Token = self._Make(COMMENT, start, self.Pos)
|
||
self.Comments.append(tok)
|
||
|
||
# ------------------------------------------------------------------
|
||
# 基础 token
|
||
# ------------------------------------------------------------------
|
||
def _EmitOp(self) -> None:
|
||
"""运算符最长匹配(含 ... 与增强赋值运算符)。"""
|
||
start: int = self.Pos
|
||
for op in _OPERATORS:
|
||
if self.Source.startswith(op, self.Pos):
|
||
self.Pos += len(op)
|
||
self.Tokens.append(self._Make(OP, start, self.Pos))
|
||
self.AtLineStart = False
|
||
return
|
||
# 未知字符:单字符 OP 兜底
|
||
self.Pos += 1
|
||
self.Tokens.append(self._Make(OP, start, self.Pos))
|
||
self.AtLineStart = False
|
||
|
||
def _ScanName(self) -> None:
|
||
"""扫描标识符。"""
|
||
start: int = self.Pos
|
||
while self.Pos < self.Length and _IsIdentPart(self.Source[self.Pos]):
|
||
self.Pos += 1
|
||
self.Tokens.append(self._Make(NAME, start, self.Pos))
|
||
self.AtLineStart = False
|
||
|
||
def _ScanNumber(self) -> None:
|
||
"""扫描数字字面量:进制前缀/十进制/浮点/指数/复数 j/下划线。"""
|
||
start: int = self.Pos
|
||
# 进制前缀 0x/0o/0b
|
||
if self.Source[self.Pos] == '0' and self.Pos + 1 < self.Length:
|
||
nxt: str = self.Source[self.Pos + 1]
|
||
if nxt in 'xXoObB':
|
||
self.Pos += 2
|
||
while self.Pos < self.Length and (self.Source[self.Pos].isalnum()
|
||
or self.Source[self.Pos] == '_'):
|
||
self.Pos += 1
|
||
self.Tokens.append(self._Make(NUMBER, start, self.Pos))
|
||
self.AtLineStart = False
|
||
return
|
||
# 整数部分
|
||
while self.Pos < self.Length and (self.Source[self.Pos].isdigit()
|
||
or self.Source[self.Pos] == '_'):
|
||
self.Pos += 1
|
||
# 浮点小数点
|
||
if self.Pos < self.Length and self.Source[self.Pos] == '.':
|
||
self.Pos += 1
|
||
while self.Pos < self.Length and (self.Source[self.Pos].isdigit()
|
||
or self.Source[self.Pos] == '_'):
|
||
self.Pos += 1
|
||
# 指数
|
||
if self.Pos < self.Length and self.Source[self.Pos] in 'eE':
|
||
save: int = self.Pos
|
||
self.Pos += 1
|
||
if self.Pos < self.Length and self.Source[self.Pos] in '+-':
|
||
self.Pos += 1
|
||
if self.Pos < self.Length and (self.Source[self.Pos].isdigit()
|
||
or self.Source[self.Pos] == '_'):
|
||
while self.Pos < self.Length and (self.Source[self.Pos].isdigit()
|
||
or self.Source[self.Pos] == '_'):
|
||
self.Pos += 1
|
||
else:
|
||
self.Pos = save # 不是指数,回退
|
||
# 复数 j
|
||
if self.Pos < self.Length and self.Source[self.Pos] in 'jJ':
|
||
self.Pos += 1
|
||
self.Tokens.append(self._Make(NUMBER, start, self.Pos))
|
||
self.AtLineStart = False
|
||
|
||
# ------------------------------------------------------------------
|
||
# 字符串
|
||
# ------------------------------------------------------------------
|
||
def _TryScanString(self) -> bool:
|
||
"""尝试从当前位置扫描字符串字面量(含 r/b/u/f 前缀)。
|
||
|
||
- 前缀字母序列后紧跟引号 → 按字符串扫描,返回 True
|
||
- 否则回退位置,返回 False(由调用方走标识符/数字路径)
|
||
"""
|
||
start: int = self.Pos
|
||
# 收集前缀字母
|
||
while self.Pos < self.Length and self.Source[self.Pos] in _STRING_PREFIX_CHARS:
|
||
self.Pos += 1
|
||
# 前缀后必须是引号,否则回退
|
||
if self.Pos >= self.Length or self.Source[self.Pos] not in '\'"':
|
||
self.Pos = start
|
||
return False
|
||
prefix: str = self.Source[start:self.Pos]
|
||
is_fstring: bool = any(c in _F_PREFIX for c in prefix)
|
||
quote: str = self.Source[self.Pos]
|
||
triple: bool = self.Source.startswith(quote * 3, self.Pos)
|
||
if triple:
|
||
self.Pos += 3
|
||
else:
|
||
self.Pos += 1
|
||
self._ScanStringBody(quote, triple, is_fstring)
|
||
end: int = self.Pos
|
||
tok_type: str = FSTRING if is_fstring else STRING
|
||
self.Tokens.append(self._Make(tok_type, start, end))
|
||
self.AtLineStart = False
|
||
return True
|
||
|
||
def _ScanStringBody(self, quote: str, triple: bool, is_fstring: bool) -> None:
|
||
"""扫描字符串主体直到结束引号(处理转义、f-string 花括号、跨行登记)。"""
|
||
while self.Pos < self.Length:
|
||
ch: str = self.Source[self.Pos]
|
||
if ch == '\\':
|
||
# 反斜杠转义:\{{ 中反斜杠只转义自身,后续 {{ 仍需按花括号转义处理
|
||
if self.Pos + 1 < self.Length and self.Source[self.Pos + 1] in '{}':
|
||
self.Pos += 1
|
||
else:
|
||
self.Pos += 2
|
||
continue
|
||
if ch == '\n':
|
||
# 三引号字符串跨行:登记行起始,保证后续行列换算准确
|
||
self.Pos += 1
|
||
self._RegisterNewline(self.Pos)
|
||
continue
|
||
if is_fstring and ch == '{':
|
||
if self.Source.startswith('{{', self.Pos):
|
||
self.Pos += 2
|
||
continue
|
||
self._ScanFStringExpr()
|
||
continue
|
||
if is_fstring and ch == '}':
|
||
if self.Source.startswith('}}', self.Pos):
|
||
self.Pos += 2
|
||
continue
|
||
self.Pos += 1
|
||
continue
|
||
if ch == quote:
|
||
if triple:
|
||
if self.Source.startswith(quote * 3, self.Pos):
|
||
self.Pos += 3
|
||
return
|
||
self.Pos += 1
|
||
else:
|
||
self.Pos += 1
|
||
return
|
||
self.Pos += 1
|
||
# 未闭合字符串:直接结束(Parser 层报语法错误)
|
||
|
||
def _ScanFStringExpr(self) -> None:
|
||
"""跳过 f-string 的 {expr[!conv][:spec]} 表达式段。
|
||
|
||
表达式内可含字符串字面量(含嵌套 f-string)与括号嵌套,需整体跳过;
|
||
跨行时登记行起始(三引号 f-string)。
|
||
"""
|
||
self.Pos += 1 # 跳过 {
|
||
depth: int = 1
|
||
while self.Pos < self.Length:
|
||
ch: str = self.Source[self.Pos]
|
||
if ch == '\\':
|
||
self.Pos += 2
|
||
continue
|
||
if ch == '\n':
|
||
self.Pos += 1
|
||
self._RegisterNewline(self.Pos)
|
||
continue
|
||
if ch in '\'"':
|
||
self._SkipInnerString()
|
||
continue
|
||
if ch == '(' or ch == '[' or ch == '{':
|
||
depth += 1
|
||
self.Pos += 1
|
||
continue
|
||
if ch == ')' or ch == ']':
|
||
depth -= 1
|
||
self.Pos += 1
|
||
continue
|
||
if ch == '}':
|
||
if depth == 1:
|
||
self.Pos += 1
|
||
return
|
||
depth -= 1
|
||
self.Pos += 1
|
||
continue
|
||
self.Pos += 1
|
||
|
||
def _SkipInnerString(self) -> None:
|
||
"""跳过 f-string 表达式内部的字符串字面量(含前缀与三引号)。"""
|
||
save: int = self.Pos
|
||
while self.Pos < self.Length and self.Source[self.Pos] in _STRING_PREFIX_CHARS:
|
||
self.Pos += 1
|
||
if self.Pos >= self.Length or self.Source[self.Pos] not in '\'"':
|
||
self.Pos = save
|
||
self.Pos += 1
|
||
return
|
||
inner_quote: str = self.Source[self.Pos]
|
||
triple: bool = self.Source.startswith(inner_quote * 3, self.Pos)
|
||
if triple:
|
||
self.Pos += 3
|
||
self._ScanStringBody(inner_quote, True, False)
|
||
else:
|
||
self.Pos += 1
|
||
self._ScanStringBody(inner_quote, False, False)
|
||
|
||
# ------------------------------------------------------------------
|
||
# 文件尾
|
||
# ------------------------------------------------------------------
|
||
def _FinishFile(self) -> None:
|
||
"""文件末尾:补 NEWLINE、DEDENT、ENDMARKER。"""
|
||
if self.Tokens and self.Tokens[-1].Type not in (NEWLINE, DEDENT, INDENT):
|
||
self.Tokens.append(self._Make(NEWLINE, self.Pos, self.Pos))
|
||
while len(self.Indents) > 1:
|
||
self.Indents.pop()
|
||
self.Tokens.append(self._Make(DEDENT, self.Pos, self.Pos))
|
||
self.Tokens.append(self._Make(ENDMARKER, self.Pos, self.Pos))
|
||
|
||
|
||
def TokenizeSource(source: str) -> list[Token]:
|
||
"""便捷入口:源码 → token 流。"""
|
||
return Tokenizer(source).Tokenize()
|