"""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'(? bool: """源码里是否出现 `new <普通名>` 形态(用于规避 C 解析器对 new 的误解)。""" return _NEW_EXPR_RE.search(source) is not None def parse(source: str, filename: str = '', 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: raise _HumanizeSyntaxError(_exc, source, filename) from None 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())