Files
TransPyC/lib/vast/__init__.py
T
TermiNexus 109e71a477 refactor: 删除保留性兼容兜底,仅保留新语法主路径
移除 len(list) 直读 __count__、list 方法裸模板回退、旧 C* 文本标记兼容、旧联合兼容;vast 扩展过期直接报错,已重建 _parser.pyd。
2026-10-18 19:22:32 +08:00

205 lines
8.4 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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())