移除 len(list) 直读 __count__、list 方法裸模板回退、旧 C* 文本标记兼容、旧联合兼容;vast 扩展过期直接报错,已重建 _parser.pyd。
205 lines
8.4 KiB
Python
205 lines
8.4 KiB
Python
"""vast - Viper/Python 自研语法解析器(ast 替代品)
|
||
|
||
vast 提供与 CPython 3.12 标准库 ``ast`` 完全一致的 API:
|
||
|
||
- 节点类:``AST`` 及全部 131 个节点类(Name/Call/BinOp/FunctionDef/...)
|
||
- 解析入口:``parse(source, filename, mode, ...)`` 产出 ``Module``
|
||
- 辅助函数:``dump`` / ``walk`` / ``iter_child_nodes`` / ``iter_fields`` /
|
||
``unparse`` / ``copy_location`` / ``fix_missing_locations`` /
|
||
``get_source_segment`` / ``literal_eval``
|
||
- 类型标注占位:``expr`` / ``stmt`` / ``operator`` / ``cmpop`` 等
|
||
|
||
编译器侧替换方式(零修改其余代码)::
|
||
|
||
import vast as ast
|
||
|
||
vast 原生支持 Viper 软关键字 fn/fun/func/function(并入解析器),无需
|
||
ExpandSoftKeywords 预处理。
|
||
"""
|
||
from __future__ import annotations
|
||
|
||
import os as _os
|
||
import re as _re
|
||
import sys as _sys
|
||
|
||
from lib.vast.Nodes import AST
|
||
from lib.vast.Nodes import _NODE_DEFS
|
||
|
||
# 将全部节点类提升到包命名空间
|
||
for _name in _NODE_DEFS:
|
||
globals()[_name] = getattr(_sys.modules['lib.vast.Nodes'], _name)
|
||
|
||
from lib.vast.Parser import Parse # noqa: E402
|
||
from lib.vast.Utils import ( # noqa: E402
|
||
iter_fields, iter_child_nodes, walk, dump, copy_location,
|
||
fix_missing_locations, get_source_segment, literal_eval,
|
||
)
|
||
from lib.vast.Unparser import Unparse as unparse # noqa: E402
|
||
from lib.vast.Tokenizer import SOFT_KEYWORDS # noqa: E402
|
||
|
||
|
||
def _ParsePure(source: str, mode: str) -> AST:
|
||
"""纯 Python vast 解析(vast_c 不可用或回退时使用)。"""
|
||
tree: AST = Parse(source)
|
||
if mode == 'exec':
|
||
return tree
|
||
if mode == 'eval':
|
||
if len(tree.body) != 1 or not isinstance(tree.body[0], _sys.modules['lib.vast.Nodes'].Expr):
|
||
raise SyntaxError('invalid syntax for eval mode')
|
||
expr_node: AST = _sys.modules['lib.vast.Nodes'].Expression(
|
||
body=tree.body[0].value)
|
||
return expr_node
|
||
if mode == 'single':
|
||
interactive: AST = _sys.modules['lib.vast.Nodes'].Interactive(body=tree.body)
|
||
return interactive
|
||
raise ValueError(f"invalid mode {mode!r}")
|
||
|
||
|
||
# C 加速解析器(vast_c 扩展):必需依赖,输出与 vast.parse 结构完全一致。
|
||
# VAST_USE_C=0 显式选择纯 Python;=strict 时 C 路径异常直接抛出(fail-fast)。
|
||
# 扩展不可用或产物早于 vast_c.c 时直接抛错(提示重建),不做静默回退。
|
||
#
|
||
# 运行时 C 路径抛异常时回退纯 Python(响亮回退):向 stderr 打印一行可诊断
|
||
# 信息(文件名 + 异常)。该回退**不会**改变产出——纯 Python 是本包语义基准,
|
||
# 故绝不静默产出与纯 Python 不同的 AST。连续相同的(文件, 异常)只打印一次。
|
||
_VastCBuildHint: str = (
|
||
"[TPC] 请重建 vast C 扩展:按 lib/vast_c/vast_c.c 头部注释的 gcc 命令"
|
||
"生成 lib/vast_c/_parser.pyd 后重试。")
|
||
|
||
_VastCDir: str = _os.path.join(_os.path.dirname(_os.path.abspath(__file__)), '..', 'vast_c')
|
||
_VastCSrc: str = _os.path.join(_VastCDir, 'vast_c.c')
|
||
try:
|
||
_VastCPyd: list[str] = [
|
||
_os.path.join(_VastCDir, _f)
|
||
for _f in _os.listdir(_VastCDir)
|
||
if _f.startswith('_parser') and _f.endswith('.pyd')]
|
||
except OSError:
|
||
_VastCPyd = []
|
||
if _VastCPyd and _os.path.exists(_VastCSrc):
|
||
if max(_os.path.getmtime(_f) for _f in _VastCPyd) < _os.path.getmtime(_VastCSrc):
|
||
raise ImportError(
|
||
"[TPC] vast C 扩展 (_parser.pyd) 早于 vast_c.c,产物已过期。" +
|
||
_VastCBuildHint)
|
||
|
||
try:
|
||
from lib.vast_c import parse as _VAST_C_PARSE
|
||
except Exception as _e:
|
||
raise ImportError(
|
||
f"[TPC] vast C 扩展不可用({type(_e).__name__}: {_e})。" + _VastCBuildHint
|
||
) from _e
|
||
|
||
_CFallbackSeen: set = set()
|
||
|
||
|
||
def _ReportCFallback(filename: str, exc: BaseException) -> None:
|
||
"""C 解析失败回退时的可诊断信息(去重,写 stderr)。"""
|
||
detail: str = f'{type(exc).__name__}: {exc}'
|
||
key = (filename, detail)
|
||
if key in _CFallbackSeen:
|
||
return
|
||
_CFallbackSeen.add(key)
|
||
_sys.stderr.write(
|
||
f'[vast] C 解析器失败,回退纯 Python:{filename}: {detail}\n')
|
||
|
||
|
||
# vast 语法错误统一报文:解析器原生消息末段带 "at line N"(ExprParser/Parser 各站点同款),
|
||
# 取出行号后补齐源码行与列号,用户看到的是单行 [TPC] 报文而不是解析器内部调用链。
|
||
_SYNTAX_LINE_RE: _re.Pattern[str] = _re.compile(r'at line (\d+)')
|
||
# 原生消息里的 token 原文(如 NAME('foo') / OP('*=') / INDENT(' ')),用于定位列号
|
||
_SYNTAX_TOKEN_RE: _re.Pattern[str] = _re.compile(r"\((?:'([^']*)'|\"([^\"]*)\")\)")
|
||
|
||
|
||
def _SourceLine(source: str, lineno: int) -> str:
|
||
"""取 1-based 第 lineno 行源码(行号缺失/越界返回空串)。"""
|
||
if lineno <= 0:
|
||
return ''
|
||
lines: list[str] = source.splitlines()
|
||
if lineno > len(lines):
|
||
return ''
|
||
return lines[lineno - 1]
|
||
|
||
|
||
def _LocateToken(msg: str, line_text: str) -> int:
|
||
"""定位原生消息里 token 原文在该行的 1-based 列;找不到退回首个非空白列。"""
|
||
if not line_text:
|
||
return 0
|
||
pos: int = -1
|
||
matched = _SYNTAX_TOKEN_RE.search(msg)
|
||
if matched is not None:
|
||
tok_text: str = matched.group(1) if matched.group(1) is not None else matched.group(2)
|
||
if tok_text.strip():
|
||
pos = line_text.find(tok_text)
|
||
if pos < 0:
|
||
pos = len(line_text) - len(line_text.lstrip())
|
||
return pos + 1
|
||
|
||
|
||
def _HumanizeSyntaxError(exc: SyntaxError, source: str, filename: str) -> SyntaxError:
|
||
"""把解析器的 SyntaxError 收口成「行:列 + 源码片段 + 原始消息」的单行报文。
|
||
|
||
重新抛出的语法错误带 filename/lineno/offset/text 四要素,故上层即使用
|
||
traceback.format_exc 也只打印该文件对应行与 caret,不再漏出解析器内部调用链。
|
||
"""
|
||
msg: str = str(exc)
|
||
lineno: int = getattr(exc, 'lineno', 0) or 0
|
||
if lineno <= 0:
|
||
found = _SYNTAX_LINE_RE.search(msg)
|
||
if found is not None:
|
||
lineno = int(found.group(1))
|
||
line_text: str = getattr(exc, 'text', '') or _SourceLine(source, lineno)
|
||
col: int = getattr(exc, 'offset', 0) or 0
|
||
if col <= 0:
|
||
col = _LocateToken(msg, line_text)
|
||
new: SyntaxError = SyntaxError(
|
||
f"[TPC] 语法错误({msg}):行 {lineno} 列 {col},源码片段 {line_text.strip()!r}。")
|
||
new.filename = filename
|
||
new.lineno = lineno
|
||
new.offset = col
|
||
new.text = line_text
|
||
return new
|
||
|
||
|
||
# Viper `new` 生命周期软关键字表达式:vast_c(stage 0 子集)不认它,会把 `new` 当普通名
|
||
# 产出 Name 节点。命中时强制走纯 Python 解析(语义基准),避免 C 路径产出错误 AST。
|
||
_NEW_EXPR_RE: _re.Pattern[str] = _re.compile(r'(?<![\w.])new[ \t]+[A-Za-z_]')
|
||
|
||
|
||
def _SourceUsesNewExpr(source: str) -> bool:
|
||
"""源码里是否出现 `new <普通名>` 形态(用于规避 C 解析器对 new 的误解)。"""
|
||
return _NEW_EXPR_RE.search(source) is not None
|
||
|
||
|
||
def parse(source: str, filename: str = '<unknown>', mode: str = 'exec', *,
|
||
type_comments: bool = False, feature_version: tuple[int, int] | int | None = None) -> AST:
|
||
"""解析源码字符串为 AST(与 ast.parse 同签名、同结构)。
|
||
|
||
优先使用 C 加速解析器 vast_c(输出结构完全一致),异常时回退纯 Python vast
|
||
并打印可诊断信息;环境变量 VAST_USE_C:'0' 禁用 C 路径,'strict' 时 C 路径
|
||
异常直接抛出。两条路径的 SyntaxError 都收口成带 文件/行/列/源码片段 的统一
|
||
报文(不再向用户漏出解析器内部 traceback)。
|
||
"""
|
||
if (_VAST_C_PARSE is not None and _os.environ.get('VAST_USE_C', '1') != '0'
|
||
and not _SourceUsesNewExpr(source)):
|
||
try:
|
||
return _VAST_C_PARSE(source, mode)
|
||
except SyntaxError as _exc:
|
||
if _os.environ.get('VAST_USE_C') == 'strict':
|
||
raise _HumanizeSyntaxError(_exc, source, filename) from None
|
||
_ReportCFallback(filename, _exc)
|
||
except Exception as _exc:
|
||
if _os.environ.get('VAST_USE_C') == 'strict':
|
||
raise
|
||
_ReportCFallback(filename, _exc)
|
||
try:
|
||
return _ParsePure(source, mode)
|
||
except SyntaxError as _exc:
|
||
raise _HumanizeSyntaxError(_exc, source, filename) from None
|
||
|
||
|
||
|
||
__all__: list[str] = [
|
||
'AST', 'parse', 'dump', 'walk', 'iter_fields', 'iter_child_nodes',
|
||
'unparse', 'copy_location', 'fix_missing_locations',
|
||
'get_source_segment', 'literal_eval', 'SOFT_KEYWORDS',
|
||
] + list(_NODE_DEFS.keys())
|