sig_files 可能不含 include 的 .vpi;改为直接扫描 temp_dir/*.vpi 匹配 'Module: traceback',确保 main 模块拿到 Install 符号。
5186 lines
284 KiB
Python
5186 lines
284 KiB
Python
from __future__ import annotations
|
||
|
||
import sys
|
||
import os
|
||
import re
|
||
import shutil
|
||
import subprocess
|
||
import vast as ast
|
||
import traceback
|
||
import json
|
||
import pickle
|
||
import time
|
||
import cProfile
|
||
from collections import OrderedDict
|
||
from concurrent.futures import ProcessPoolExecutor, ThreadPoolExecutor, as_completed, wait, FIRST_COMPLETED
|
||
from lib.core.VLogger import get_logger as _vlog
|
||
from lib.constants.config import mode as _config_mode
|
||
from lib.core.Handles.HandlesBase import CTypeInfo
|
||
from lib.core.SymbolData import ModuleSymbols
|
||
from lib.core import TypeSystem
|
||
from lib.core.SymbolUtils import ExpandSoftKeywords, DefineDeclIntTypeFromAnnotation
|
||
from lib.core.SymbolUtils import ClassHasFuncLevelCVTable, IsVtableSlotMethod
|
||
from lib.Projectrans.Utils import compute_sha1, find_reachable_files_from_entries, topological_sort_files, parse_python_file
|
||
from lib.Projectrans.Phase1Generator import _TryGlobalCache, _SaveToGlobalCache, _GLOBAL_CACHE_DIR
|
||
import TransPyC
|
||
from StubGen import PythonToStubConverter
|
||
from lib.core.LLVMCG.IRMetadataFix import SanitizeIRMetadata
|
||
|
||
|
||
class ReportedCompileError(RuntimeError):
|
||
"""主进程已输出完整编译错误(含源码定位)的终止信号。
|
||
|
||
该异常只用于"错误已经打印过"的路径:main()/run() 抛出后,
|
||
顶层入口(Projectrans.py)不得再次打印错误内容,直接以失败退出码结束,
|
||
否则同一错误会在 worker/主进程/顶层各出现一遍。
|
||
"""
|
||
|
||
|
||
# PROF 热点:nt._path_isfile 2.6 万次 16.4s(Windows 磁盘 stat ~0.6ms/次)。
|
||
# 编译进程内文件状态稳定(obj/ll 一旦生成不删除、不被移动),用模块级缓存
|
||
# 避免重复 stat。仅用于"存在性检查"类调用(不用于内容变化的判断)。
|
||
_FILE_STAT_CACHE: dict[str, bool] = {}
|
||
|
||
|
||
# --traceback(Ctrl+C 调用栈)强制纳入编译+链接的 include 模块源文件名。
|
||
# 该模块对外只有 Install() 入口,且无任何用户代码 import 它 —— 若不强制纳入,
|
||
# 产物里既没有 Install 符号、也没有调用它的启动期构造器,功能静默失效。
|
||
# .vp 优先,.py 仅为向后兼容的回退名。
|
||
_TRACEBACK_MODULE_FILES: tuple[str, ...] = ('traceback.vp', 'traceback.py')
|
||
|
||
|
||
def _CachedIsFile(path: str) -> bool:
|
||
r: bool | None = _FILE_STAT_CACHE.get(path)
|
||
if r is None:
|
||
r = os.path.isfile(path)
|
||
_FILE_STAT_CACHE[path] = r
|
||
return r
|
||
|
||
|
||
# ---------------------------------------------------------------
|
||
# 文本读缓存(性能):同一个文件在一次构建里被**多个阶段反复读**
|
||
# (prof 基线 PROF/20260930_224857:_io.open 12,740 次 / self 28.8s、
|
||
# read 11,902 次 / self 30.4s、TextIOWrapper.__exit__ 16.6s —— 128 个模块
|
||
# 却产生上万次读,主要是同一份 .stub.ll / .ll / deps.json 被
|
||
# _compile_one / _recombine_ll / 依赖解析等重复打开)。
|
||
# 键 = **路径**(不做 stat / 时间戳校验)。性能前提(用户口径):TPC 单次运行内
|
||
# 非本进程写出的文件内容不变 —— 因此不需要"每次读都 stat 一次"来判新旧;
|
||
# 若再 stat,等于把省下的 1.2 万次 open 换成 1.2 万次 stat(仍是系统调用热点)。
|
||
# 正确性前提:**本进程写出/改写/删除过的文件,一律在该动作后调用
|
||
# _InvalidateTextCache**(写点见文件内全部 open(..., 'w') 与 os.remove 处)。
|
||
# 只缓存 newline=None 的默认换行翻译读,与直接 open(...).read() 语义逐字相同。
|
||
# 内存:按总字节预算 + LRU 淘汰;单文件超过 _TEXT_CACHE_FILE_MAX 不入缓存
|
||
# (合并后的 .ll 可达数十 MB,避免把内存吃光)。
|
||
# ---------------------------------------------------------------
|
||
_TEXT_CACHE_MAX_BYTES: int = 64 * 1024 * 1024
|
||
_TEXT_CACHE_FILE_MAX: int = 32 * 1024 * 1024
|
||
_TextReadCache: OrderedDict[str, str] = OrderedDict()
|
||
_TextReadCacheBytes: int = 0
|
||
|
||
|
||
def _ReadTextCached(path: str, newline: str | None = None) -> str:
|
||
"""读文本文件(按路径缓存,零 stat);newline 非 None 时退化为直接读(不进缓存)。"""
|
||
global _TextReadCacheBytes
|
||
if newline is not None:
|
||
# 逐字节保真的读(newline='' 等)语义特殊:不进缓存,避免与默认换行翻译混用
|
||
with open(path, 'r', encoding='utf-8', newline=newline) as _f:
|
||
return _f.read()
|
||
_hit: str | None = _TextReadCache.get(path)
|
||
if _hit is not None:
|
||
_TextReadCache.move_to_end(path)
|
||
return _hit
|
||
with open(path, 'r', encoding='utf-8') as _f:
|
||
_text: str = _f.read()
|
||
if len(_text) <= _TEXT_CACHE_FILE_MAX:
|
||
_TextReadCache[path] = _text
|
||
_TextReadCacheBytes += len(_text)
|
||
while _TextReadCacheBytes > _TEXT_CACHE_MAX_BYTES and len(_TextReadCache) > 1:
|
||
_, _old = _TextReadCache.popitem(last=False)
|
||
_TextReadCacheBytes -= len(_old)
|
||
return _text
|
||
|
||
|
||
def _InvalidateTextCache(path: str) -> None:
|
||
"""丢弃 path 的文本读缓存条目:本进程**写出/改写/删除**该文件后必须调用。
|
||
|
||
这是本缓存唯一的正确性保证(读侧不再 stat 判新旧):漏掉任一写点,后续读会
|
||
拿到该路径的旧文本。典型必失效场景:先读 → 同一 tick 内原地改写 → 再读,
|
||
且改写前后字节数相同(实例:_compile_one 前的 type 定义重排,行集合不变仅次序变;
|
||
_recombine_ll 的旧 sha1 前缀替换,16 位 hex 等长替换)。
|
||
"""
|
||
global _TextReadCacheBytes
|
||
_old = _TextReadCache.pop(path, None)
|
||
if _old is not None:
|
||
_TextReadCacheBytes -= len(_old)
|
||
|
||
|
||
_ASAN_DLL_A = 'libclang_rt.asan_dynamic-x86_64.dll.a'
|
||
|
||
|
||
def _FindAsanRuntime() -> tuple[str, str] | None:
|
||
"""通过 PATH 定位 clang64 的 ASan 运行库(按 LLVM 大版本取最新的那个)
|
||
|
||
mingw64 的 clang 只为 x86_64-w64-windows-gnu 目标提供 builtins / fuzzer /
|
||
profile / orc 运行库,**没有 ASan runtime**(<resource-dir>/lib/
|
||
x86_64-w64-windows-gnu/ 是空目录),所以不能靠 `-fsanitize=address` 让驱动
|
||
自动去找 —— 那只会得到
|
||
"could not open .../libclang_rt.asan_dynamic.dll.a: No such file or directory"。
|
||
clang64(MSVC ABI) 那边才有 libclang_rt.asan_dynamic-x86_64.dll.a 与
|
||
libclang_rt.asan_dynamic_runtime_thunk-x86_64.a,只能借用这两份。
|
||
|
||
路径**不再写死**:遍历 PATH 的每个目录 E,按 `E` 与 `dirname(E)` 两种可能取 base,
|
||
拼出 `<base>/lib/clang/<大版本>/lib/windows`,命中即为链接库目录;对应的运行期
|
||
DLL 目录取 `<base>/bin`。
|
||
|
||
Returns:
|
||
(链接库目录, 运行期 DLL 目录);都找不到返回 None
|
||
"""
|
||
hits: list[tuple[str, str]] = []
|
||
seen: set[str] = set()
|
||
for entry in os.environ.get('PATH', '').split(os.pathsep):
|
||
if not entry:
|
||
continue
|
||
for base in (entry, os.path.dirname(entry)):
|
||
if not base:
|
||
continue
|
||
root = os.path.join(base, 'lib', 'clang')
|
||
if root in seen or not os.path.isdir(root):
|
||
continue
|
||
seen.add(root)
|
||
for name in os.listdir(root):
|
||
cand = os.path.join(root, name, 'lib', 'windows')
|
||
if os.path.isfile(os.path.join(cand, _ASAN_DLL_A)):
|
||
hits.append((cand, os.path.join(base, 'bin')))
|
||
if not hits:
|
||
return None
|
||
|
||
def _Ver(item: tuple[str, str]) -> int:
|
||
# 目录名是 LLVM 大版本号(如 "22")——按数值比,否则 "9" 会排在 "22" 之后
|
||
v = item[0].split(os.sep)[-3]
|
||
return int(v) if v.isdigit() else 0
|
||
|
||
hits.sort(key=_Ver)
|
||
return hits[-1]
|
||
|
||
|
||
# ---------- --traceback:地址→源行 旁文件(<exe>.stackmap)----------
|
||
#
|
||
# 开启 --traceback 时,链接成功后用 llvm-dwarfdump 解析最终 PE 的
|
||
# .debug_line/.debug_info,把已有的"函数级/语句级 DWARF 行标记"导出成
|
||
# <exe>.stackmap,供 exe 内运行时处理器在 Ctrl+C/Ctrl+Break 时把 CRT 到当前
|
||
# 的返回地址逐帧翻译回 ".vp:行/函数"。以下函数只在开关开启时被调用;未开启时
|
||
# 不引入任何新依赖、任何新行为。
|
||
|
||
_PE_MAGIC: bytes = b'PE\0\0'
|
||
|
||
|
||
def _ReadPEImageLayout(exe_path: str) -> tuple[int, int, dict[str, tuple[int, int]]] | None:
|
||
"""读取 PE 的 (ImageBase, SizeOfImage, 节表) ;非 PE / 解析失败返回 None。
|
||
|
||
节表为 {节名: (节 RVA, 节 VirtualSize)}。写端需要这三样才能把 DWARF 地址严格
|
||
归一到"运行期 RVA"(运行期 traceback.vp 用 `帧地址 − GetModuleHandleA(NULL)`):
|
||
* 合法行 ⇔ 地址落在映像 VA 区间 [ImageBase, ImageBase + SizeOfImage);
|
||
* "运行期可能出现的 RVA 区间"的粗判据 = [.text 节 RVA, SizeOfImage)。
|
||
(原 _ReadPEImageBase 只返回 ImageBase,不足以做上述判定,故扩展为本函数。)
|
||
"""
|
||
import struct
|
||
try:
|
||
with open(exe_path, 'rb') as f:
|
||
head: bytes = f.read(0x1000)
|
||
except Exception:
|
||
return None
|
||
if len(head) < 0x40 or head[:2] != b'MZ':
|
||
return None
|
||
e_lfanew: int = struct.unpack_from('<I', head, 0x3C)[0]
|
||
if e_lfanew + 24 > len(head) or head[e_lfanew:e_lfanew + 4] != _PE_MAGIC:
|
||
return None
|
||
coff_off: int = e_lfanew + 4
|
||
n_sections: int = struct.unpack_from('<H', head, coff_off + 2)[0]
|
||
opt_size: int = struct.unpack_from('<H', head, coff_off + 16)[0]
|
||
opt_off: int = coff_off + 20 # COFF 文件头固定 20 字节
|
||
if opt_off + 32 > len(head):
|
||
return None
|
||
magic: int = struct.unpack_from('<H', head, opt_off)[0]
|
||
if magic == 0x20B: # PE32+
|
||
image_base: int = struct.unpack_from('<Q', head, opt_off + 24)[0]
|
||
elif magic == 0x10B: # PE32
|
||
image_base = struct.unpack_from('<I', head, opt_off + 28)[0]
|
||
else:
|
||
return None
|
||
# SizeOfImage 在两种可选头里都在 +56(PE32 多出的 BaseOfData 在 ImageBase 之前)
|
||
size_of_image: int = struct.unpack_from('<I', head, opt_off + 56)[0]
|
||
sections: dict[str, tuple[int, int]] = {}
|
||
sec_off: int = opt_off + opt_size
|
||
for i in range(n_sections):
|
||
base: int = sec_off + i * 40
|
||
if base + 40 > len(head):
|
||
break
|
||
name: str = head[base:base + 8].split(b'\x00', 1)[0].decode('ascii', 'replace')
|
||
sections[name] = (struct.unpack_from('<I', head, base + 12)[0], # VirtualAddress = 节 RVA
|
||
struct.unpack_from('<I', head, base + 8)[0]) # VirtualSize
|
||
return image_base, size_of_image, sections
|
||
|
||
|
||
# llvm-dwarfdump --debug-line 行表;行记录列形如
|
||
# Address Line Column File ISA Discriminator Flags
|
||
# 0x0000000140001000 12 0 1 0 0 is_stmt
|
||
_DWARF_LINE_HEADER_RE = re.compile(r'^Address\s+Line\s+Column\s+File\b')
|
||
_DWARF_LINE_ROW_RE = re.compile(r'^(0x[0-9A-Fa-f]+)\s+(\d+)\s+(\d+)\s+(\d+)\b')
|
||
_DWARF_FILENAMES_RE = re.compile(r'^\s*file_names\[\s*(\d+)\]\s*:')
|
||
_DWARF_DIRNAMES_RE = re.compile(r'^\s*(?:directory_names|include_directories)\[\s*(\d+)\]\s*[:=]\s*(.*)$')
|
||
_DWARF_NAME_RE = re.compile(r'^\s*name:\s*(.*)$')
|
||
_DWARF_DIRINDEX_RE = re.compile(r'^\s*dir_index:\s*(\d+)\s*$')
|
||
# llvm-dwarfdump --debug-info 里的 DISubprogram(DW_AT_low_pc/high_pc/name)
|
||
_DWARF_TAG_ANY_RE = re.compile(r'DW_TAG_\w+')
|
||
_DWARF_TAG_SUBPROGRAM_RE = re.compile(r'DW_TAG_subprogram\b')
|
||
_DWARF_LOWPC_RE = re.compile(r'DW_AT_low_pc\s*\(\s*(0x[0-9A-Fa-f]+)\s*\)')
|
||
_DWARF_HIGHPC_RE = re.compile(r'DW_AT_high_pc\s*\(\s*(0x[0-9A-Fa-f]+)\s*\)')
|
||
_DWARF_ATNAME_RE = re.compile(r'DW_AT_name\s*\("((?:[^"\\]|\\.)*)"\)')
|
||
|
||
# ---------------------------------------------------------------
|
||
# `_split_ll` 的逐行正则**预编译**(性能:该函数对合并后的 .ll 逐行跑两遍,
|
||
# prof 实测 43.9万行级输入下 re 模块级函数的缓存查找(re._compile)达 1970 万次
|
||
# ≈ 7s,另有 3100 万次 re.Pattern.match;每次 `re.match(字面量, ...)` 都要做
|
||
# "模式串哈希 → 缓存查找"。预编译成模块级常量后省掉这层查找,且匹配路径更短。
|
||
# 语义与逐字写法完全一致(模式串一字未改)。
|
||
# ---------------------------------------------------------------
|
||
_RE_DEFINE_SHA1: re.Pattern[str] = re.compile(r'define\s+[^@]*@"?([a-f0-9]+)\.')
|
||
_RE_DEFINE_NAME: re.Pattern[str] = re.compile(r'define\s+[^@]*@"?([^"\s(]+)"?')
|
||
_RE_DECLARE_NAME: re.Pattern[str] = re.compile(r'declare\s+[^@]*@"?([^"\s(]+)"?')
|
||
_RE_LLVM_INTRINSIC: re.Pattern[str] = re.compile(r'@("?)llvm\.')
|
||
_RE_AT_NAME: re.Pattern[str] = re.compile(r'(@\S+)')
|
||
_RE_SHA1_16_DOT: re.Pattern[str] = re.compile(r'^[a-f0-9]{16}\.')
|
||
_RE_DEFINE_PREFIX: re.Pattern[str] = re.compile(r'^define\s+')
|
||
_RE_DEFINE_LINKAGE: re.Pattern[str] = re.compile(
|
||
r'\b(linkonce_odr|weak_odr|linkonce|weak|common|appending|internal|private)\s+')
|
||
_RE_ALWAYSINLINE: re.Pattern[str] = re.compile(r'\balwaysinline\s+')
|
||
# `_compile_one` 的逐行符号扫描/全局去重里用到的两条(同一预编译理由)
|
||
_RE_AT_EXTERNAL: re.Pattern[str] = re.compile(r'@\S+\s*=\s*external\s+')
|
||
_RE_DECLARE_FULL: re.Pattern[str] = re.compile(
|
||
r'^declare\s+(?:dso_local\s+)?[^@]*@\"?([^"\s\(]+)\"?\s*\(')
|
||
|
||
# 其余字面量正则的模块级预编译(同一性能理由:省掉每次调用的"模式串哈希 →
|
||
# re 缓存查找";模式串一字未改,flags 原样保留)。凡模式串不同或 flags 不同
|
||
# (有/无 re.MULTILINE)之处都各自独立成常量,保证匹配语义逐字不变。
|
||
# `_AddWeakComdat`:定位函数头第一个元数据(`!...`)附着点 / 全局定义尾部属性插入点
|
||
_RE_METADATA_ATTACH: re.Pattern[str] = re.compile(r'\s![\w.]')
|
||
_RE_COMDAT_TAIL_ATTRS: re.Pattern[str] = re.compile(r',\s*(?:align|section|partition|code_model)\b')
|
||
# LLVM 22+ external global 带初值 → linkonce(带行首锚 + MULTILINE)
|
||
_RE_EXTERNAL_GLOBAL_VALUE: re.Pattern[str] = re.compile(
|
||
r'^(@[\w.]+)\s*=\s*external\s+global\s+(\w[\w*]*)\s+(-?\d+|null|zeroinitializer|true|false)$',
|
||
re.MULTILINE)
|
||
# 同上但**不带行首锚**(opt 内联后整段内容替换处)
|
||
_RE_EXTERNAL_GLOBAL_VALUE_PLAIN: re.Pattern[str] = re.compile(
|
||
r'(@[\w.]+)\s*=\s*external\s+global\s+(\w[\w*]*)\s+(-?\d+|null|zeroinitializer|true|false)$',
|
||
re.MULTILINE)
|
||
# define/declare 后跟显式链接性关键字 → 其后补 dso_local
|
||
_RE_DEFINE_DECLARE_LINKAGE: re.Pattern[str] = re.compile(
|
||
r'^(define|declare)\s+(private|internal|available_externally|linkonce|weak|common|appending|extern_weak|linkonce_odr|weak_odr|external)\s+',
|
||
re.MULTILINE)
|
||
# define/declare 后无链接性/dso_local(含 `@` 分支)→ 直接补 dso_local
|
||
_RE_DEFINE_DECLARE_NO_LINKAGE_AT: re.Pattern[str] = re.compile(
|
||
r'^(define|declare)\s+(?!private|internal|available_externally|linkonce|weak|common|appending|extern_weak|linkonce_odr|weak_odr|external|dso_local|@)',
|
||
re.MULTILINE)
|
||
# 同上但**无 `@` 分支**(traceback include 模块专用)
|
||
_RE_DEFINE_DECLARE_NO_LINKAGE: re.Pattern[str] = re.compile(
|
||
r'^(define|declare)\s+(?!private|internal|available_externally|linkonce|weak|common|appending|extern_weak|linkonce_odr|weak_odr|external|dso_local)',
|
||
re.MULTILINE)
|
||
# 全局定义(非 llvm.*)带链接性关键字 → 补 dso_local(无 MULTILINE)
|
||
_RE_GLOBAL_LINKAGE: re.Pattern[str] = re.compile(
|
||
r'(@(?!llvm\.)[\w.]+)\s*=\s*(private|internal|available_externally|linkonce|weak|common|appending|extern_weak|linkonce_odr|weak_odr)\s+(global|constant)')
|
||
# 同上但带 MULTILINE
|
||
_RE_GLOBAL_LINKAGE_MULTILINE: re.Pattern[str] = re.compile(
|
||
r'(@(?!llvm\.)[\w.]+)\s*=\s*(private|internal|available_externally|linkonce|weak|common|appending|extern_weak|linkonce_odr|weak_odr)\s+(global|constant)',
|
||
re.MULTILINE)
|
||
# 全局定义无链接性 → 补 dso_local(无 MULTILINE)
|
||
_RE_GLOBAL_DSOLOCAL: re.Pattern[str] = re.compile(
|
||
r'(@[\w.]+)\s*=\s+(?!private|internal|available_externally|linkonce|weak|common|appending|extern_weak|linkonce_odr|weak_odr|dso_local)(global|constant)')
|
||
# 同上但带 MULTILINE(原文此处带行首锚 `^`:避免匹配到字符串常量内容里的 global/constant)
|
||
_RE_GLOBAL_DSOLOCAL_MULTILINE: re.Pattern[str] = re.compile(
|
||
r'^(@[\w.]+)\s*=\s+(?!private|internal|available_externally|linkonce|weak|common|appending|extern_weak|linkonce_odr|weak_odr|dso_local)(global|constant)',
|
||
re.MULTILINE)
|
||
# 收集已定义函数名(define 头)
|
||
_RE_DEFINE_HEAD: re.Pattern[str] = re.compile(
|
||
r'^define\s+(?:dso_local\s+)?[^@]*@\"?([^"\s\(]+)\"?\s*\(',
|
||
re.MULTILINE)
|
||
# 判定 IR 是否含 define 行(traceback 警告)
|
||
_RE_DEFINE_LINE: re.Pattern[str] = re.compile(r'^define ', re.MULTILINE)
|
||
|
||
|
||
def _SourceHasRealDef(src_path: str) -> bool:
|
||
"""模块是否含有真正的 `def`(而不是只有 `extern def`)。
|
||
|
||
extern-only 模块(如 includes/win32/win32console.vp)里的 define 是编译器合成的转发体,
|
||
没有源级函数体,本来就不会有 DWARF 行表;对它们发 "--traceback 未携带 DWARF" 属于误报。
|
||
读不到源码时返回 True,保持旧行为(宁可多提示,不漏提示)。
|
||
"""
|
||
try:
|
||
content, _ = parse_python_file(src_path)
|
||
except Exception:
|
||
return True
|
||
if not content:
|
||
return True
|
||
for raw in content.splitlines():
|
||
s: str = raw.lstrip()
|
||
if s.startswith('def ') or s.startswith('async def '):
|
||
return True
|
||
return False
|
||
|
||
|
||
def _ParseDwarfLineRows(dump_text: str) -> list[tuple[int, str, int]]:
|
||
"""从 llvm-dwarfdump 输出解析出 [(地址, 文件绝对路径, 行号)]。
|
||
|
||
逐个 CU(debug_line[0x..])重建目录/文件表;行记录过滤掉 line==0 的
|
||
end_sequence 行。文件路径优先用文件表里的绝对 name;name 为相对时按 dir_index
|
||
指向的目录拼接(DWARF v5 取 directory_names,v4 取 include_directories)。
|
||
|
||
⚠ 地址口径:**原样返回 dwarfdump 打印的地址**(本例为含 ImageBase 的 VA
|
||
0x14000xxxx,但被链接器按 COMDAT 去重丢弃的泛型副本其地址恰为 0)。
|
||
本函数**不**做"整表 VA/RVA"的判定——那正是旧实现失真的根因(全局最小值一旦为
|
||
0,整表都不减基址)。归一化(逐条 `地址 − ImageBase`,并丢弃不能归一的条目)
|
||
由调用方 `_WriteStackMap` 依 PE.ImageBase/SizeOfImage 完成。
|
||
"""
|
||
rows: list[tuple[int, str, int]] = []
|
||
dirs: dict[int, str] = {}
|
||
files: dict[int, tuple[str, int]] = {}
|
||
pending_kind: str | None = None
|
||
pending_idx: int = -1
|
||
pending_name: str | None = None
|
||
pending_dir_index: int = 0
|
||
in_line_section: bool = False
|
||
|
||
def _Unquote(text: str) -> str:
|
||
text = text.strip()
|
||
if len(text) >= 2 and text[0] == '"' and text[-1] == '"':
|
||
text = text[1:-1]
|
||
return text
|
||
|
||
def _Commit() -> None:
|
||
nonlocal pending_kind, pending_name, pending_dir_index
|
||
if pending_kind == 'file' and pending_name is not None:
|
||
files[pending_idx] = (pending_name, pending_dir_index)
|
||
pending_kind = None
|
||
pending_name = None
|
||
pending_dir_index = 0
|
||
|
||
def _Resolve(idx: int) -> str:
|
||
entry = files.get(idx)
|
||
if entry is None:
|
||
return '??'
|
||
name, dir_index = entry
|
||
if os.path.isabs(name):
|
||
return name
|
||
base: str = dirs.get(dir_index, '')
|
||
if base and not os.path.isabs(base):
|
||
root: str = dirs.get(0, '')
|
||
if root and os.path.isabs(root):
|
||
base = os.path.join(root, base)
|
||
return os.path.normpath(os.path.join(base, name)) if base else name
|
||
|
||
for line in dump_text.splitlines():
|
||
stripped: str = line.strip()
|
||
if not stripped:
|
||
continue
|
||
if 'debug_line contents' in stripped:
|
||
in_line_section = True
|
||
continue
|
||
if 'contents:' in stripped and 'debug_' in stripped:
|
||
in_line_section = False
|
||
continue
|
||
if not in_line_section:
|
||
continue
|
||
if stripped.startswith('debug_line['):
|
||
_Commit()
|
||
dirs = {}
|
||
files = {}
|
||
continue
|
||
m = _DWARF_FILENAMES_RE.match(line)
|
||
if m:
|
||
_Commit()
|
||
pending_kind = 'file'
|
||
pending_idx = int(m.group(1))
|
||
continue
|
||
m = _DWARF_DIRNAMES_RE.match(line)
|
||
if m:
|
||
_Commit()
|
||
value: str = _Unquote(m.group(2))
|
||
if value:
|
||
dirs[int(m.group(1))] = value
|
||
else:
|
||
pending_kind = 'dir'
|
||
pending_idx = int(m.group(1))
|
||
continue
|
||
m = _DWARF_NAME_RE.match(line)
|
||
if m and pending_kind is not None:
|
||
name: str = _Unquote(m.group(1))
|
||
if pending_kind == 'file':
|
||
pending_name = name
|
||
else:
|
||
dirs[pending_idx] = name
|
||
pending_kind = None
|
||
continue
|
||
m = _DWARF_DIRINDEX_RE.match(line)
|
||
if m and pending_kind == 'file':
|
||
pending_dir_index = int(m.group(1))
|
||
continue
|
||
if _DWARF_LINE_HEADER_RE.match(line):
|
||
_Commit()
|
||
continue
|
||
m = _DWARF_LINE_ROW_RE.match(line)
|
||
if m:
|
||
_Commit()
|
||
row_line: int = int(m.group(2))
|
||
if row_line > 0:
|
||
rows.append((int(m.group(1), 16), _Resolve(int(m.group(4))), row_line))
|
||
return rows
|
||
|
||
|
||
def _ParseDwarfSubprograms(dump_text: str) -> list[tuple[int, int, str]]:
|
||
"""从 llvm-dwarfdump --debug-info 输出解析 [(low_pc, high_pc, 函数名)]。
|
||
|
||
DW_AT_high_pc 有两种形态且 dwarfdump 都打成 `(0x…)`:`DW_FORM_addr`(绝对
|
||
结束地址,值 > low_pc)与常量形式(相对 low_pc 的**长度**,值通常 < low_pc)。
|
||
本函数以 `high_pc <= low_pc` 判定后者并按 `low_pc + 长度` 还原;缺 high_pc 时
|
||
退化为 [low_pc, low_pc + 1)。
|
||
|
||
⚠ 地址口径同 `_ParseDwarfLineRows`:**原样返回**,不做整表 VA/RVA 判定;被
|
||
链接器 COMDAT 去重丢弃的泛型副本其 low_pc 恰为 0(实测 viperc.exe 有 756 个),
|
||
由调用方 `_WriteStackMap` 逐条归一化并丢弃。
|
||
"""
|
||
result: list[tuple[int, int, str]] = []
|
||
cur_low: int | None = None
|
||
cur_high: int | None = None
|
||
cur_name: str | None = None
|
||
in_prog: bool = False
|
||
|
||
def _Flush() -> None:
|
||
nonlocal cur_low, cur_high, cur_name, in_prog
|
||
if in_prog and cur_low is not None and cur_name:
|
||
hi: int = cur_high if cur_high is not None else cur_low + 1
|
||
if hi <= cur_low:
|
||
hi = cur_low + cur_high # high_pc 为 offset 形式
|
||
if hi > cur_low:
|
||
result.append((cur_low, hi, cur_name))
|
||
cur_low = None
|
||
cur_high = None
|
||
cur_name = None
|
||
in_prog = False
|
||
|
||
for line in dump_text.splitlines():
|
||
if _DWARF_TAG_ANY_RE.search(line):
|
||
if _DWARF_TAG_SUBPROGRAM_RE.search(line):
|
||
_Flush()
|
||
in_prog = True
|
||
else:
|
||
_Flush()
|
||
continue
|
||
if not in_prog:
|
||
continue
|
||
m = _DWARF_LOWPC_RE.search(line)
|
||
if m:
|
||
cur_low = int(m.group(1), 16)
|
||
continue
|
||
m = _DWARF_HIGHPC_RE.search(line)
|
||
if m:
|
||
cur_high = int(m.group(1), 16)
|
||
continue
|
||
m = _DWARF_ATNAME_RE.search(line)
|
||
if m and cur_name is None:
|
||
cur_name = m.group(1)
|
||
_Flush()
|
||
return result
|
||
|
||
|
||
# 显式并行度覆盖:TPC_JOBS=N(N>=1)——使用者明确指定 worker 数并**自行承担**
|
||
# 内存风险(含 N=1 即显式接受串行)。设了它就不再走内存校验/预分配。
|
||
_JOBS_ENV: str = 'TPC_JOBS'
|
||
# 已通过"预分配校验"的预算(MB):同一预算不重复校验(翻译/编译两阶段各查一次)
|
||
_MemProbeDone: set[int] = set()
|
||
# 预分配校验的分块大小(MB):分块提交并**全部持有**到校验结束(峰值 = 预算),
|
||
# 结束后立即整体释放 —— 紧接其后就创建 worker 进程,OS 直接复用这批刚释放的页。
|
||
_MEM_PROBE_CHUNK_MB: int = 256
|
||
# 自动并行度上限(用户 2026-10-01 定:16 worker 太多)。只约束**自动**取值,
|
||
# 显式 TPC_JOBS=N 仍按硬件/文件数上限生效。
|
||
_MAX_WORKERS: int = 8
|
||
|
||
|
||
def _MemStatus() -> tuple[int, int]:
|
||
"""(可用物理内存 MB, 物理内存总量 MB);读取失败返回 (-1, -1),调用方按"未知"处理"""
|
||
try:
|
||
import ctypes
|
||
|
||
class _MemStat(ctypes.Structure):
|
||
_fields_ = [
|
||
("dwLength", ctypes.c_ulong),
|
||
("dwMemoryLoad", ctypes.c_ulong),
|
||
("ullTotalPhys", ctypes.c_ulonglong),
|
||
("ullAvailPhys", ctypes.c_ulonglong),
|
||
("ullTotalPageFile", ctypes.c_ulonglong),
|
||
("ullAvailPageFile", ctypes.c_ulonglong),
|
||
("ullTotalVirtual", ctypes.c_ulonglong),
|
||
("ullAvailVirtual", ctypes.c_ulonglong),
|
||
("ullAvailExtendedVirtual", ctypes.c_ulonglong),
|
||
]
|
||
|
||
stat = _MemStat()
|
||
stat.dwLength = ctypes.sizeof(_MemStat)
|
||
if ctypes.windll.kernel32.GlobalMemoryStatusEx(ctypes.byref(stat)):
|
||
return (stat.ullAvailPhys // (1024 * 1024),
|
||
stat.ullTotalPhys // (1024 * 1024))
|
||
except Exception:
|
||
pass
|
||
return (-1, -1)
|
||
|
||
|
||
def _ProbeReserveMb(mb: int) -> bool:
|
||
"""预分配校验:一次性提交 mb MB(分块持有后整体释放),成功返回 True。
|
||
|
||
为什么要"预分配"而不是只读 ullAvailPhys:可用物理内存含大量**可回收**的
|
||
缓存页/其他进程随时会吃掉的余量,只按它决策会出现"这一秒够、真跑起来不够"
|
||
的波动(编译时间在 400s / 1180s 两档之间随机跳的直接来源)。先真正把预算
|
||
拿到手(页级提交)再放掉,等于向 OS 确认过这笔预算确实可分配。
|
||
"""
|
||
if mb <= 0:
|
||
return True
|
||
Bufs = []
|
||
Left = mb
|
||
try:
|
||
while Left > 0:
|
||
Cur = min(_MEM_PROBE_CHUNK_MB, Left)
|
||
Bufs.append(bytearray(Cur * 1024 * 1024))
|
||
Left -= Cur
|
||
return True
|
||
except MemoryError:
|
||
return False
|
||
finally:
|
||
del Bufs
|
||
|
||
|
||
def _PickWorkerCount(n_files: int, per_worker_mb: int = 350) -> int:
|
||
"""并行 worker 数的**唯一**决策入口(CPU 上限 + 内存预分配双约束)。
|
||
|
||
口径(用户 2026-10-01 定,取代旧"内存紧就降级串行"):
|
||
· **自动上限 `_MAX_WORKERS`=8**:16 worker 收益不足、内存压力翻倍(用户 2026-10-01 定);
|
||
显式 TPC_JOBS 不受此限(只受硬件/文件数约束)。
|
||
1. **不降级**:内存不足**绝不**静默退回单 worker 串行 —— 旧实现 `by_mem =
|
||
max(1, avail_mb // per_worker_mb)` 在可用内存稍紧时就整体降为串行,
|
||
同一条命令 76 文件逐个翻译 1180s、并行 ~400s,时间随机跳 2-3 倍
|
||
(profile 实证:224857 那次主进程 0 次 _translate_file=并行,438s;
|
||
231916/235354 两次主进程 75 次 _translate_file=串行,1048s/1168s)。
|
||
2. **预分配 + 强制预留**:先按 base×per_worker_mb 做一次性提交校验
|
||
(_ProbeReserveMb),拿到就并行、拿不到就 fail-fast 报出"可用/需要"
|
||
数字,由使用者释放内存或**显式** TPC_JOBS=N 接受其它并行度。
|
||
3. 显式 TPC_JOBS=N 优先且跳过校验(使用者自行承担内存风险,N=1 即显式串行)。
|
||
|
||
单 worker 预估 per_worker_mb(Python 解释器 + llvmlite module + 共享符号表
|
||
快照);实测翻译阶段 worker 稳态 ~110MB、峰值 ~300MB,故取 350(编译阶段
|
||
调用方传 400)。Windows 用 GlobalMemoryStatusEx 读物理内存。
|
||
"""
|
||
hw_max = min(os.cpu_count() or 4, n_files)
|
||
base = min(hw_max, _MAX_WORKERS)
|
||
if base <= 1:
|
||
return base
|
||
env_jobs: str | None = os.environ.get(_JOBS_ENV)
|
||
if env_jobs:
|
||
try:
|
||
want = int(env_jobs)
|
||
except ValueError:
|
||
want = base
|
||
# 显式指定只受硬件/文件数约束(不受 _MAX_WORKERS 限制),并跳过内存校验
|
||
want = max(1, min(want, hw_max))
|
||
_vlog().info(f" [并行] {_JOBS_ENV}={env_jobs} → workers={want}(显式指定,跳过校验)")
|
||
return want
|
||
avail_mb, total_mb = _MemStatus()
|
||
need_mb = base * per_worker_mb
|
||
if avail_mb < 0:
|
||
# 读不到内存信息:不阻塞编译(按上限并行),但必须显式告知
|
||
_vlog().warning(f" [并行] 读不到物理内存,取 workers={base}(预算 {need_mb}MB)")
|
||
return base
|
||
if avail_mb < need_mb:
|
||
raise RuntimeError(
|
||
f"[并行翻译] 内存不足:需 {need_mb}MB({base}×{per_worker_mb}MB),"
|
||
f"可用 {avail_mb}MB/{total_mb}MB;释放内存或设 {_JOBS_ENV}=N 重跑")
|
||
if need_mb not in _MemProbeDone:
|
||
_t0: float = _time.time()
|
||
if not _ProbeReserveMb(need_mb):
|
||
raise RuntimeError(
|
||
f"[并行翻译] 预分配 {need_mb}MB 失败({base}×{per_worker_mb}MB,"
|
||
f"可用 {avail_mb}MB/{total_mb}MB);释放内存或设 {_JOBS_ENV}=N 重跑")
|
||
_MemProbeDone.add(need_mb)
|
||
_vlog().info(f" [并行] 预分配 {need_mb}MB 通过({_time.time() - _t0:.2f}s)")
|
||
_vlog().info(f" [并行] workers={base}(预算 {need_mb}MB / 可用 {avail_mb}MB)")
|
||
return base
|
||
|
||
|
||
def _ExtractCDefineConstantsFromPyi(pyi_path: str, all_dc: dict[str, object],
|
||
symtab: object | None = None,
|
||
all_dt: dict[str, tuple[int, bool]] | None = None) -> None:
|
||
"""从 .pyi 文件中提取 define 常量到 all_dc。文件不存在时静默跳过。
|
||
|
||
治本修复:原实现只支持 ast.Constant 和 float() 调用,
|
||
无法处理 CUnsignedLong(-11)(ast.Call)和
|
||
FOREGROUND_GREEN | FOREGROUND_INTENSITY(ast.BinOp)等复杂表达式。
|
||
现使用 ConstEvaluator.eval_full 统一求值,支持:
|
||
- ast.Constant: 简单常量 (0x0002)
|
||
- ast.Call: 类型构造 (CUnsignedLong(-11))
|
||
- ast.BinOp: 位运算 (FOREGROUND_RED | FOREGROUND_GREEN)
|
||
- ast.UnaryOp: 一元运算 (-11)
|
||
- ast.Name: 引用其他 define 常量
|
||
"""
|
||
if not os.path.exists(pyi_path):
|
||
return
|
||
try:
|
||
# 复用 parse_python_file 的 AST 缓存(.pyi 文件被多处解析)
|
||
_, tree = parse_python_file(pyi_path)
|
||
if tree is None:
|
||
return
|
||
# 治本修复:使用 ConstEvaluator 处理复杂表达式
|
||
# 创建 mock Gen 对象,其 _define_constants 指向 all_dc,
|
||
# 使 ConstEvaluator 能查找同文件中已提取的常量(如 FOREGROUND_RED 引用 FOREGROUND_BLUE)
|
||
from lib.core.ConstEvaluator import ConstEvaluator, EvalContext
|
||
|
||
class _MockGen:
|
||
"""轻量 mock,仅提供 _define_constants 供 ConstEvaluator 查找"""
|
||
def __init__(self) -> None:
|
||
self._define_constants: dict[str, object] = all_dc
|
||
|
||
mock_gen: _MockGen = _MockGen()
|
||
ctx: EvalContext = EvalContext(Gen=mock_gen)
|
||
|
||
for node in ast.iter_child_nodes(tree):
|
||
if isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name):
|
||
Markers: tuple[str, ...] = (tuple(getattr(node, 'Markers', None) or ())
|
||
+ tuple(getattr(node.annotation, 'Markers', None) or ()))
|
||
# 新语法 `define NAME = v` 由解析器写入 Markers。
|
||
if 'define' in Markers and node.value:
|
||
val: object = ConstEvaluator.eval_full(node.value, ctx)
|
||
if val is None and isinstance(node.value, ast.Call) and len(node.value.args) == 1:
|
||
# 类型构造的**裸别名**形态(如 `DWORD(-11)`,非 `CUnsignedLong(-11)`):
|
||
# eval_full 只认 `X(...)` / `from t import X`(见 ConstEvaluator
|
||
# _eval_compile_time_call),裸 typedef 别名既不在 _t_module 也不在
|
||
# mock ctx 的 _builtin_type_symbols 里 → 返回 None → 该常量被整条丢弃。
|
||
# 现场:win32file.vp 的 STD_INPUT/OUTPUT/ERROR_HANDLE 是全仓**唯一**用
|
||
# 别名构造写的 define → 唯独这三个进不了 _all_define_constants,导致
|
||
# 模块属性访问 `wf.STD_ERROR_HANDLE` 解析落空、退化成未定义的全局符号
|
||
# (链接失败)。define 关心的是常量值,故回退求值其唯一实参(位型一致)。
|
||
val = ConstEvaluator.eval_full(node.value.args[0], ctx)
|
||
if val is not None and isinstance(val, (int, float, str)):
|
||
all_dc[node.target.id] = val
|
||
# 带类型注册:记录该 define 的**声明整数类型**(如 `DWORD` → (32, False)),
|
||
# 供使用点按声明类型物化(u32 用 i32 + 位型,而不是按值大小猜 i32/i64)。
|
||
if all_dt is not None and isinstance(val, int) and not isinstance(val, bool):
|
||
decl_type: tuple[int, bool] | None = DefineDeclIntTypeFromAnnotation(symtab, node.annotation)
|
||
if decl_type is not None:
|
||
all_dt[node.target.id] = decl_type
|
||
except Exception as e:
|
||
_vlog().warning(f"Phase2Translator: 忽略异常 {e}", exc_info=e)
|
||
|
||
|
||
def _IsExportExternFuncNode(node: ast.FunctionDef) -> bool:
|
||
"""函数是否导出/外部可见:读 `export def` / `extern def` 的 AST 标记与 Markers。"""
|
||
if bool(getattr(node, 'Export', False)) or bool(getattr(node, 'Extern', False)):
|
||
return True
|
||
Markers: tuple[str, ...] = getattr(node, 'Markers', None) or ()
|
||
return 'export' in Markers or 'extern' in Markers or 'state' in Markers
|
||
|
||
|
||
def _CountUnresolvedTypedefs(module_symbols: object) -> int:
|
||
"""该模块"有值注解却解析不出类型"的 typedef 个数(供声明阶段判断是否要下一轮)。
|
||
|
||
typedef 右侧可以引用**其它模块**的 typedef:例 win32base 的
|
||
`typedef HANDLE = VOIDPTR`(VOIDPTR 在 stdint)——本模块先 stage 时右侧
|
||
解析为空(original_type_kind/original_class 均为 None),要等依赖模块入表后
|
||
重 stage 才能补全。返回 0 表示本模块声明已完整(不再需要重跑)。
|
||
"""
|
||
miss: int = 0
|
||
for td in getattr(module_symbols, 'typedefs', []):
|
||
if getattr(td, 'value', None) is None and getattr(td, 'annotation', None) is None:
|
||
continue
|
||
if getattr(td, 'original_type_kind', None) is None and getattr(td, 'original_class', None) is None:
|
||
miss += 1
|
||
return miss
|
||
|
||
|
||
def _parallel_translate_worker(src_path: str, out_path: str, src_root: str, temp_dir: str, output_dir: str,
|
||
include_dirs: list[str], triple: str | None, datalayout: str | None,
|
||
sha1_map: dict[str, str], sig_files: dict[str, str],
|
||
stub_files: dict[str, str], include_py_map: dict[str, str], slice_level: int,
|
||
shared_sym_pickle_path: str, struct_sha1_map: dict[str, str],
|
||
prebuilt_ModuleSha1Map: dict[str, str] | None = None,
|
||
hotspot_sites_file: str | None = None,
|
||
hotspot_sites_all: bool = False,
|
||
hotspot_callees: list[str] | None = None,
|
||
traceback_enabled: bool = False,
|
||
dwarf_enabled: bool = False) -> tuple[str, str, str]:
|
||
# 注意:不可跨任务复用 Phase2Translator/共享符号表!_translate_file 编译过程中
|
||
# 会向共享 SymbolTable 写入符号(import insert/类注册等),复用导致前文件的
|
||
# 符号污染后文件 → 跨模块 struct 类型前缀错位(如 7cadfe07e983fd90.CompilerFactory
|
||
# 混入 _list 前缀,Type of #1 arg mismatch)。每任务必须从 pickle 反序列化出
|
||
# 独立副本(原实现;worker 复用省掉的 pickle.load 时间仅 ~0.5s/次,远小于
|
||
# worker 内翻译计算,不值得以正确性为代价)。
|
||
try:
|
||
trans = Phase2Translator(src_root, temp_dir, output_dir,
|
||
compile_cmd='llc', include_dirs=include_dirs,
|
||
target_triple=triple, target_datalayout=datalayout,
|
||
build_pyi_cache=False)
|
||
trans.sha1_map = sha1_map
|
||
trans.sig_files = sig_files
|
||
trans.stub_files = stub_files
|
||
trans.include_py_map = include_py_map
|
||
trans.slice_level = slice_level
|
||
trans.struct_sha1_map = struct_sha1_map
|
||
# 调用点热点统计与每函数计时(--hotspot-sites):worker 子进程无父进程属性,
|
||
# 须显式透传(None=未启用,零影响);生成 IR 时各模块处理器统一追加写最终 <path>
|
||
trans.hotspot_sites_file = hotspot_sites_file
|
||
trans.hotspot_sites_all = hotspot_sites_all
|
||
trans.hotspot_callees = hotspot_callees
|
||
# Ctrl+C 调用栈(--traceback):worker 子进程无父进程属性,须显式透传
|
||
# (False=未启用,零影响);traceback 模块据此注入 Install() 启动期构造器
|
||
trans.traceback_enabled = traceback_enabled
|
||
# 函数级 DWARF(--traceback 时 include 模块也必须带行表,否则栈里落到
|
||
# includes/* 的帧取不到源行):worker 子进程无父进程属性,须显式透传
|
||
# (False=未启用,零影响),仅用于 _translate_file 的 dwarf_enabled 形参
|
||
trans.dwarf_enabled = dwarf_enabled
|
||
|
||
if shared_sym_pickle_path and os.path.exists(shared_sym_pickle_path):
|
||
# 使用 pickle.load(f) 流式加载,避免 pickle.loads(f.read()) 一次性
|
||
# 将整个 pickle 文件读入内存(磁盘副本 + 反序列化对象同时占用内存)导致 MemoryError。
|
||
# 同时提高递归限制,防止深层嵌套对象反序列化失败。
|
||
sys.setrecursionlimit(max(sys.getrecursionlimit(), 100000))
|
||
with open(shared_sym_pickle_path, 'rb') as f:
|
||
shared_data = pickle.load(f)
|
||
_sym_bytes: bytes | None = shared_data.get('symbol_table_bytes')
|
||
if _sym_bytes is not None:
|
||
trans._shared_symbol_table = pickle.loads(_sym_bytes)
|
||
trans._shared_source_module_sig_files = shared_data['source_module_sig_files']
|
||
trans._shared_all_dc = shared_data['all_dc']
|
||
trans._shared_all_decl_types = shared_data.get('all_decl_types', {})
|
||
trans._shared_export_extern_funcs = shared_data['export_extern_funcs']
|
||
trans.inline_func_symbols = shared_data['inline_func_symbols']
|
||
trans.function_default_args = shared_data['function_default_args']
|
||
trans.function_param_names = shared_data.get('function_param_names', {})
|
||
trans._shared_generic_class_templates = shared_data.get('generic_class_templates', {})
|
||
trans._shared_doc_meta = shared_data.get('shared_doc_meta', {})
|
||
trans._shared_class_vtable = shared_data.get('class_vtable', set())
|
||
trans._shared_cross_module_vtable = shared_data.get('cross_module_vtable', set())
|
||
trans._shared_class_methods = shared_data.get('class_methods', {})
|
||
trans._shared_cross_module_novtable = shared_data.get('cross_module_novtable', set())
|
||
else:
|
||
trans._precollect_inline_symbols()
|
||
trans._build_shared_symbol_data()
|
||
|
||
trans._translate_file(src_path, out_path, prebuilt_ModuleSha1Map=prebuilt_ModuleSha1Map, dwarf_enabled=dwarf_enabled)
|
||
return ('ok', src_path, '')
|
||
except Exception as e:
|
||
# fail-fast:worker 不写 stderr,只把错误(含源码定位)返回主进程;
|
||
# 由主进程统一输出一次,避免同一错误打印两遍。
|
||
return ('error', src_path, f'{type(e).__name__}: {e}')
|
||
|
||
|
||
def _parallel_translate_batch(batch: list, src_root: str, temp_dir: str, output_dir: str,
|
||
include_dirs: list[str], triple: str | None, datalayout: str | None,
|
||
sha1_map: dict[str, str], sig_files: dict[str, str],
|
||
stub_files: dict[str, str], include_py_map: dict[str, str], slice_level: int,
|
||
shared_sym_pickle_path: str, struct_sha1_map: dict[str, str],
|
||
hotspot_sites_file: str | None = None,
|
||
hotspot_sites_all: bool = False,
|
||
hotspot_callees: list[str] | None = None,
|
||
dwarf_enabled: bool = False) -> tuple[list, list]:
|
||
"""批次子任务:worker 内创建一个 Phase2Translator + pickle.load 一次,顺序翻译批内全部文件。
|
||
|
||
batch 每项: (i, src_path, out_path, sha1, prebuilt_ModuleSha1Map)
|
||
|
||
防污染:SymbolTable 是编译期唯一被写的共享对象(import insert/类注册),
|
||
批内每个文件翻译前从 symbol_table_bytes 反序列化出干净副本,前文件的
|
||
符号写入不会残留到后文件(否则跨模块 struct 类型前缀错位)。
|
||
返回 (错误列表[(src_path, err)], batch, per_file_times[(src_path, elapsed, rel)])。
|
||
错误只回传、不写 stderr:完整错误由主进程统一输出一次(含源码定位)。
|
||
"""
|
||
_current_src: str = batch[0][1] if batch else ''
|
||
per_file_times: list[tuple[str, float, str]] = []
|
||
try:
|
||
trans = Phase2Translator(src_root, temp_dir, output_dir,
|
||
compile_cmd='llc', include_dirs=include_dirs,
|
||
target_triple=triple, target_datalayout=datalayout,
|
||
build_pyi_cache=False)
|
||
trans.sha1_map = sha1_map
|
||
trans.sig_files = sig_files
|
||
trans.stub_files = stub_files
|
||
trans.include_py_map = include_py_map
|
||
trans.slice_level = slice_level
|
||
trans.struct_sha1_map = struct_sha1_map
|
||
# 调用点热点统计与每函数计时(--hotspot-sites):批内每文件同样透传
|
||
trans.hotspot_sites_file = hotspot_sites_file
|
||
trans.hotspot_sites_all = hotspot_sites_all
|
||
trans.hotspot_callees = hotspot_callees
|
||
|
||
symbol_table_bytes: bytes | None = None
|
||
if shared_sym_pickle_path and os.path.exists(shared_sym_pickle_path):
|
||
sys.setrecursionlimit(max(sys.getrecursionlimit(), 100000))
|
||
with open(shared_sym_pickle_path, 'rb') as f:
|
||
shared_data = pickle.load(f)
|
||
symbol_table_bytes = shared_data.get('symbol_table_bytes')
|
||
if symbol_table_bytes is not None:
|
||
trans._shared_symbol_table = pickle.loads(symbol_table_bytes)
|
||
trans._shared_source_module_sig_files = shared_data['source_module_sig_files']
|
||
trans._shared_all_dc = shared_data['all_dc']
|
||
trans._shared_all_decl_types = shared_data.get('all_decl_types', {})
|
||
trans._shared_export_extern_funcs = shared_data['export_extern_funcs']
|
||
trans.inline_func_symbols = shared_data['inline_func_symbols']
|
||
trans.function_default_args = shared_data['function_default_args']
|
||
trans.function_param_names = shared_data.get('function_param_names', {})
|
||
trans._shared_generic_class_templates = shared_data.get('generic_class_templates', {})
|
||
trans._shared_doc_meta = shared_data.get('shared_doc_meta', {})
|
||
trans._shared_class_vtable = shared_data.get('class_vtable', set())
|
||
trans._shared_cross_module_vtable = shared_data.get('cross_module_vtable', set())
|
||
trans._shared_class_methods = shared_data.get('class_methods', {})
|
||
trans._shared_cross_module_novtable = shared_data.get('cross_module_novtable', set())
|
||
else:
|
||
trans._precollect_inline_symbols()
|
||
trans._build_shared_symbol_data()
|
||
|
||
batch_errors: list = []
|
||
for _i, src_path, out_path, _sha1, msm in batch:
|
||
_f0: float = time.time()
|
||
_current_src = src_path
|
||
# 每文件前重置 SymbolTable 为干净副本(防跨文件污染)
|
||
if symbol_table_bytes is not None:
|
||
trans._shared_symbol_table = pickle.loads(symbol_table_bytes)
|
||
trans._shared_symbol_table.translator = None
|
||
trans._translate_file(src_path, out_path, prebuilt_ModuleSha1Map=msm, dwarf_enabled=dwarf_enabled)
|
||
per_file_times.append((src_path, time.time() - _f0, os.path.relpath(src_path, src_root)))
|
||
return (batch_errors, batch, per_file_times)
|
||
except Exception as e:
|
||
# fail-fast:任意文件编译出错立即终止本批(不再翻译批内剩余文件)。
|
||
# worker 不写 stderr —— 错误(含源码定位)随返回值交给主进程统一输出一次。
|
||
_src: str = _current_src or (batch[0][1] if batch else '')
|
||
return ([(_src, f'{type(e).__name__}: {e}')], batch, per_file_times)
|
||
|
||
|
||
# ==================================================================
|
||
# 子进程性能剖析:主进程 cProfile 只能看到进程池等待(WaitForMultipleObjects/
|
||
# as_completed),看不到 worker 进程内部 CPU 分布(vast parse 等热点)。
|
||
# 固定启用(不设环境变量开关),worker 每个任务导出 cProfile dump
|
||
# (worker_prof_{pid}_{seq}.prof),主进程导出 main_prof_{pid}.prof + main_time.txt。
|
||
# 输出落在 PROF_DIR/<启动时间>/ 下的**本次编译专属目录**(2026-09-29 起)。
|
||
# ==================================================================
|
||
import functools as _functools
|
||
import time as _time
|
||
|
||
# 硬编码 PROF 根目录(2026-08-18 起不再用 TPC_WORKER_PROF 环境变量控制)
|
||
PROF_DIR: str = r'D:\Users\TermiNexus\Desktop\TransPyC\PROF'
|
||
|
||
# 本次编译的 run 目录:按启动时间在 PROF_DIR 下新建子目录,**不清理旧目录**
|
||
# (2026-09-29 用户要求保留历次 profile 以便跨次对比,旧行为是每次编译删掉
|
||
# PROF 根目录下全部 *.prof ⇒ 只能看最近一次)。
|
||
# 子目录名经环境变量传给 worker:ProcessPoolExecutor 用 spawn 起新解释器,
|
||
# 进程内模块态不共享;环境变量随子进程继承 ⇒ 同一次编译的所有 worker 落在
|
||
# 同一个 run 目录,不会各建各的目录。
|
||
_PROF_RUN_ENV: str = 'TPC_PROF_RUN_DIR'
|
||
_prof_run_dir: str | None = None
|
||
|
||
|
||
def StartProfRun() -> str:
|
||
"""每次编译开始时调用一次:创建本次编译专属的 PROF 子目录,返回其路径。
|
||
|
||
目录名 = PROF 根目录下 `YYYYmmdd_HHMMSS`(编译启动时刻);同名(同一秒内
|
||
二次启动)补 `_N` 序号。创建失败时回落到 PROF 根目录,保证不因 profiling
|
||
影响编译本身。成功后把目录写进环境变量,供 worker 子进程复用。
|
||
"""
|
||
global _prof_run_dir
|
||
if _prof_run_dir:
|
||
return _prof_run_dir
|
||
_name: str = _time.strftime('%Y%m%d_%H%M%S')
|
||
_dir: str = os.path.join(PROF_DIR, _name)
|
||
_seq: int = 1
|
||
while os.path.isdir(_dir):
|
||
_seq += 1
|
||
_dir = os.path.join(PROF_DIR, f'{_name}_{_seq}')
|
||
try:
|
||
os.makedirs(_dir, exist_ok=True)
|
||
except OSError:
|
||
return PROF_DIR
|
||
_prof_run_dir = _dir
|
||
os.environ[_PROF_RUN_ENV] = _dir
|
||
return _dir
|
||
|
||
|
||
def GetProfRunDir() -> str:
|
||
"""当前进程写 profile 应落的目录。
|
||
|
||
主进程 = StartProfRun 选定的 run 目录;worker = 继承来的环境变量指向的目录。
|
||
两者都取不到(未先调 StartProfRun,如 worker 被单独拉起)时回落到 PROF 根目录,
|
||
宁可文件落在根目录也不丢 dump。
|
||
"""
|
||
global _prof_run_dir
|
||
if _prof_run_dir:
|
||
return _prof_run_dir
|
||
_env_dir: str = os.environ.get(_PROF_RUN_ENV, '')
|
||
if _env_dir:
|
||
_prof_run_dir = _env_dir
|
||
return _env_dir
|
||
return PROF_DIR
|
||
|
||
|
||
def _ProfileWorker(_fn):
|
||
@_functools.wraps(_fn)
|
||
def _wrapper(*args, **kwargs):
|
||
_prof = None
|
||
try:
|
||
# cProfile 默认不启用(环境变量 TPC_WORKER_PROF=1 开启):
|
||
# Python 只允许一个 profiling tool 同时活跃,TPC 的 cProfile 与
|
||
# viztracer --log_multiprocess 冲突(cProfile.enable() 检测到已激活
|
||
# profiler 抛 "Another profiling tool is already active" → worker 崩溃
|
||
# → batch 异常 → 编译终止)。外部 profiler 活跃(sys.getprofile()
|
||
# 非 None)时即使开启也跳过让位;enable() 异常兜底吞掉保编译。
|
||
if os.environ.get('TPC_WORKER_PROF', '0') == '1' and sys.getprofile() is None:
|
||
_prof = cProfile.Profile()
|
||
try:
|
||
_prof.enable()
|
||
except RuntimeError:
|
||
_prof = None
|
||
return _fn(*args, **kwargs)
|
||
finally:
|
||
if _prof is not None:
|
||
_prof.disable()
|
||
_seq: int = getattr(_wrapper, '_seq', 0) + 1
|
||
_wrapper._seq = _seq
|
||
try:
|
||
_out_dir: str = GetProfRunDir()
|
||
if not os.path.isdir(_out_dir):
|
||
os.makedirs(_out_dir, exist_ok=True)
|
||
_out: str = os.path.join(_out_dir, f'worker_prof_{os.getpid()}_{_seq}.prof')
|
||
_prof.dump_stats(_out)
|
||
except Exception:
|
||
pass
|
||
|
||
return _wrapper
|
||
|
||
|
||
_parallel_translate_worker = _ProfileWorker(_parallel_translate_worker)
|
||
_parallel_translate_batch = _ProfileWorker(_parallel_translate_batch)
|
||
|
||
|
||
# ==================================================================
|
||
# worker pyi 缓存预热:ProcessPoolExecutor(spawn) 下每个 worker 重新
|
||
# import 模块后,HandlesImports.ImportHandle 的类级 _pyi_cache 全空,
|
||
# 每进程首次任务重新 vast.parse 全部依赖 .vpi/.pyi
|
||
# (cProfile: 4479 次 parse / 245s)。
|
||
# 方案:主进程把 temp 下全部 .vpi 的 vast AST 树 pickle 落盘
|
||
# (temp/_pyi_cache_v1.pkl,任务 B),worker initializer 启动时
|
||
# pickle.load 一次填充 _pyi_cache(任务 A);load 失败回退逐文件
|
||
# vast.parse。文件系统共享即为"共用缓存",不做跨进程共享内存(任务 C)。
|
||
# 环境变量 TPC_WORKER_WARMUP=0 可关闭(回归对比基线)。
|
||
# ==================================================================
|
||
_PYI_CACHE_PKL_NAME: str = '_pyi_cache_v1.pkl'
|
||
_PYI_CACHE_SIG_NAME: str = '_pyi_cache_v1.sig.json'
|
||
_PYI_CACHE_VERSION: int = 1
|
||
|
||
|
||
def _ScanPyiFiles(temp_dir: str) -> list[str]:
|
||
"""扫描 temp 目录下全部 .vpi/.pyi 文件,返回绝对路径列表(与
|
||
_TryLoadClassMembersFromPyi 的 _pyi_cache 键格式一致)。"""
|
||
try:
|
||
names: list[str] = os.listdir(temp_dir)
|
||
except OSError:
|
||
return []
|
||
paths: list[str] = []
|
||
for fname in sorted(names):
|
||
if fname.startswith('_'):
|
||
continue
|
||
if fname.endswith('.vpi') or fname.endswith('.pyi'):
|
||
paths.append(os.path.join(temp_dir, fname))
|
||
return paths
|
||
|
||
|
||
def _PyiCacheSignature(paths: list[str]) -> list[tuple[str, int, float]]:
|
||
"""vpi 清单签名:(basename, size, mtime) 元组列表,用于缓存失效判断。"""
|
||
sig: list[tuple[str, int, float]] = []
|
||
for p in paths:
|
||
try:
|
||
st = os.stat(p)
|
||
sig.append((os.path.basename(p), st.st_size, st.st_mtime))
|
||
except OSError:
|
||
sig.append((os.path.basename(p), -1, -1.0))
|
||
return sig
|
||
|
||
|
||
def _LoadPyiCachePkl(temp_dir: str) -> dict | None:
|
||
"""pickle.load temp/_pyi_cache_v1.pkl,返回 {abs_path: vast树|None}。
|
||
文件不存在/损坏/版本不符时返回 None(调用方回退逐文件解析)。"""
|
||
pkl_path: str = os.path.join(temp_dir, _PYI_CACHE_PKL_NAME)
|
||
try:
|
||
with open(pkl_path, 'rb') as f:
|
||
data: dict = pickle.load(f)
|
||
if not isinstance(data, dict) or data.get('version') != _PYI_CACHE_VERSION:
|
||
return None
|
||
cache = data.get('cache')
|
||
if not isinstance(cache, dict):
|
||
return None
|
||
return cache
|
||
except Exception:
|
||
return None
|
||
|
||
|
||
def _BuildPyiCacheFile(temp_dir: str) -> str | None:
|
||
"""构建/刷新 temp/_pyi_cache_v1.pkl:遍历全部 .vpi 做 vast.parse 后
|
||
pickle.dump(含版本号与签名)。签名(basename,size,mtime)未变时跳过。
|
||
返回 pkl 路径;失败(无 vpi / pickle 不兼容等)返回 None。
|
||
注意:vast AST 可 pickle 性由 Nodes.py 侧保证;若实际 pickle.dump
|
||
失败,此处静默回退,worker 的 initializer 将改为逐文件 vast.parse。"""
|
||
paths: list[str] = _ScanPyiFiles(temp_dir)
|
||
if not paths:
|
||
return None
|
||
sig: list[tuple[str, int, float]] = _PyiCacheSignature(paths)
|
||
pkl_path: str = os.path.join(temp_dir, _PYI_CACHE_PKL_NAME)
|
||
sig_path: str = os.path.join(temp_dir, _PYI_CACHE_SIG_NAME)
|
||
try:
|
||
if _CachedIsFile(pkl_path) and _CachedIsFile(sig_path):
|
||
stored = json.loads(_ReadTextCached(sig_path))
|
||
# json 往返后 tuple→list,需规范化再比较
|
||
if stored == [[n, s, m] for n, s, m in sig]:
|
||
return pkl_path
|
||
except Exception:
|
||
pass
|
||
cache: dict[str, object] = {}
|
||
# 增量复用:Phase2 过程会新增 include .vpi(签名每次变 → 全量重建 12.2s)。
|
||
# 加载旧 pkl,按 (basename, size, mtime) 匹配未变化的文件复用 vast 树,
|
||
# 只对新增/变化的文件重新 parse。
|
||
old_by_name: dict[str, tuple[int, float, object | None]] = {}
|
||
try:
|
||
old = _LoadPyiCachePkl(temp_dir)
|
||
if isinstance(old, dict):
|
||
old_sig = old.get('signature') or []
|
||
old_trees = list((old.get('cache') or {}).values())
|
||
for _i, (_name, _size, _mtime) in enumerate(old_sig):
|
||
_tree = old_trees[_i] if _i < len(old_trees) else None
|
||
old_by_name[_name] = (_size, _mtime, _tree)
|
||
except Exception:
|
||
pass
|
||
for p, (name, size, mtime) in zip(paths, sig):
|
||
old_entry = old_by_name.get(name)
|
||
if (old_entry is not None and old_entry[0] == size
|
||
and old_entry[1] == mtime and old_entry[2] is not None):
|
||
cache[p] = old_entry[2]
|
||
continue
|
||
try:
|
||
# 走 LRU 文本缓存:同一 .vpi 在增量复用/多 worker 预热路径会被反复读
|
||
cache[p] = ast.parse(_ReadTextCached(p), p)
|
||
except Exception:
|
||
cache[p] = None
|
||
try:
|
||
with open(pkl_path, 'wb') as f:
|
||
pickle.dump({'version': _PYI_CACHE_VERSION, 'signature': sig, 'cache': cache}, f)
|
||
with open(sig_path, 'w', encoding='utf-8') as f:
|
||
json.dump(sig, f)
|
||
return pkl_path
|
||
except Exception as e:
|
||
_vlog().warning(f"Phase2Translator: pyi 缓存落盘失败({e}),worker 将回退逐文件解析")
|
||
return None
|
||
|
||
|
||
def _PyiCacheWorkerInitializer(temp_dir: str) -> None:
|
||
"""worker 进程启动时预热 ImportHandle._pyi_cache(类级缓存)。
|
||
|
||
spawn 下每 worker 重新 import 模块后 _pyi_cache 为空,首次任务会
|
||
重新 vast.parse 全部依赖 .vpi(4479 次 parse / 245s)。此处启动时
|
||
一次性填充:优先 pickle.load 主进程落盘的 _pyi_cache_v1.pkl;
|
||
失败(文件不存在/损坏/版本不符/pickle 不兼容)回退逐文件 vast.parse。
|
||
预热只解析 .vpi(pyi 树小);stub 索引保留 _build_stub_index 懒加载。
|
||
TPC_WORKER_WARMUP=0 时关闭(便于回归对比)。
|
||
"""
|
||
if os.environ.get('TPC_WORKER_WARMUP', '1') != '1':
|
||
return
|
||
if not os.path.isdir(temp_dir):
|
||
return
|
||
from lib.core.Handles.HandlesImports import ImportHandle
|
||
cache: dict | None = _LoadPyiCachePkl(temp_dir)
|
||
if cache is not None:
|
||
ImportHandle._pyi_cache = cache
|
||
ImportHandle._pyi_cache_dir = os.path.abspath(temp_dir)
|
||
return
|
||
# 回退:逐文件 vast.parse 预热(worker 单线程,无竞争)
|
||
try:
|
||
new_cache: dict[str, object] = {}
|
||
for p in _ScanPyiFiles(temp_dir):
|
||
try:
|
||
new_cache[p] = ast.parse(_ReadTextCached(p), p)
|
||
except Exception:
|
||
new_cache[p] = None
|
||
if new_cache:
|
||
ImportHandle._pyi_cache = new_cache
|
||
ImportHandle._pyi_cache_dir = os.path.abspath(temp_dir)
|
||
except Exception:
|
||
pass
|
||
|
||
|
||
def _count_braces_outside_strings(text: str) -> int:
|
||
"""统计花括号差值,跳过 c"..." 字符串字面量内部的括号
|
||
|
||
快路径(prof 基线:本函数 102,397 次调用 / self 2.0s,占 _split_ll cumtime
|
||
3.35s 的 83%,是全流程 str.strip 之后第二大的"纯 Python 逐字符"热点):
|
||
行内不含 `c"` 时,"跳过字符串内部括号"这一语义退化为"整行括号计数",
|
||
直接用 C 级 str.count 一次算完。含 `c"` 的行(主要见于 c"..." 全局串常量)
|
||
仍走下面的逐字符扫描,语义逐字不变。
|
||
"""
|
||
if 'c"' not in text:
|
||
return text.count('{') - text.count('}')
|
||
count = 0
|
||
i = 0
|
||
while i < len(text):
|
||
if text[i:i+2] == 'c"' and (i == 0 or text[i-1] != '\\'):
|
||
# 跳过 c"..." 字面量
|
||
i += 2
|
||
while i < len(text):
|
||
if text[i] == '"' and (i == 0 or text[i-1] != '\\'):
|
||
i += 1
|
||
break
|
||
i += 1
|
||
continue
|
||
if text[i] == '{':
|
||
count += 1
|
||
elif text[i] == '}':
|
||
count -= 1
|
||
i += 1
|
||
return count
|
||
|
||
|
||
# LLVM IR "无引号标识符"的精确规则(LangRef "Identifiers";实现即
|
||
# LLLexer::ReadVarName):首字符 ∈ [-a-zA-Z$._],后续字符 ∈ [-a-zA-Z$._0-9]。
|
||
# 首字符**不允许是数字**:`$884f3d94ea4d0123.md5_F` 会让 LLLexer::LexDollar
|
||
# 走到 ReadVarName() 返回 false ⇒ 产出 error token ⇒ 顶层解析报
|
||
# "expected top-level entity"。故 comdat 名必须以同一规则判定是否加引号
|
||
# (与 LLVM 自身 AsmWriter 的输出形态一致:`$"884f3d94ea4d0123.md5_F"`)。
|
||
# 另:引号名 @"..." 不支持转义,名字含 `"`/`\` 时两种形态都非法 ⇒ 见
|
||
# _AddWeakComdat 内的 fail-fast(不静默替换、不跳过)。
|
||
_LLVM_UNQUOTED_NAME_RE: re.Pattern[str] = re.compile(
|
||
r'[-a-zA-Z$._][-a-zA-Z$._0-9]*\Z')
|
||
|
||
# comdat 声明行:`$name = comdat any` / `$"name" = comdat any`(允许行首缩进)
|
||
_RE_COMDAT_DECL: re.Pattern[str] = re.compile(r'^\s*\$("?)([^"=\s]+)\1\s*=\s*comdat\b')
|
||
|
||
|
||
def _DedupComdatDecls(ll_content: str) -> str:
|
||
"""删除重复的 `$name = comdat any` 声明(保留首次出现)。
|
||
|
||
落盘的 `_dso.ll` 是「本模块 + 各依赖 .stub.ll/.text.ll 片段」拼接而成,
|
||
每个片段各自带一份同名 comdat 声明(片段生成时也会各自补 comdat)。同名
|
||
声明出现两次即 llc 报 `redefinition of comdat '$…'`(现场:泛型特化
|
||
`list[<sha1>.AST*].__before_init__` 在同一文件里出现 4 次)。comdat 声明
|
||
在顶层可位于任意位置,保留首次出现即可,语义不变。
|
||
"""
|
||
if '= comdat' not in ll_content:
|
||
return ll_content
|
||
seen: set[str] = set()
|
||
kept: list[str] = []
|
||
for line in ll_content.splitlines(True):
|
||
m = _RE_COMDAT_DECL.match(line)
|
||
if m is not None:
|
||
name: str = m.group(2)
|
||
if name in seen:
|
||
continue
|
||
seen.add(name)
|
||
kept.append(line)
|
||
return ''.join(kept)
|
||
|
||
|
||
def _AddWeakComdat(ll_content: str) -> str:
|
||
"""为 weak/linkonce 系列 函数/全局 定义补 COMDAT(COFF/PE 必需)。
|
||
|
||
COFF 下无 COMDAT 的 weak/linkonce 定义会被 llc 展开成强符号
|
||
`.weak.<name>.default`(weak external 的实现副本)。同一弱定义出现在
|
||
多个模块时(泛型特化方法如 `e94ef9e139461915.list[rawptr[AST]].
|
||
__before_init__`、`linkonce`/`weak` 标注的跨模块共享常量),副本同名 →
|
||
ld.lld 报 duplicate symbol。补 COMDAT(any) 后 llc 输出单一 COMDAT
|
||
成员,链接器自动去重(llc/ld.lld 端到端已实验验证)。
|
||
"""
|
||
if (' linkonce ' not in ll_content and ' linkonce_odr ' not in ll_content
|
||
and ' weak_odr ' not in ll_content and ' weak ' not in ll_content):
|
||
return _DedupComdatDecls(ll_content)
|
||
# 拼接而来的片段可能各自已带同名 comdat 声明 ⇒ 先去重,再做本次补全
|
||
ll_content = _DedupComdatDecls(ll_content)
|
||
weak_links = 'linkonce|linkonce_odr|weak|weak_odr|extern_weak'
|
||
comdat_names: list[str] = []
|
||
|
||
# 1) 函数定义:define <linkage> [dso_local] ... @name(...) { → @name(...) comdat {
|
||
def _fn_repl(m: re.Match) -> str:
|
||
comdat_names.append(m.group(3))
|
||
head: str = m.group(1)
|
||
# comdat 是函数头关键字,必须写在 !dbg 等元数据附着**之前**:LLVM 解析器
|
||
# 读完 `!...` 附着后就只接受 `{`,附着之后写 comdat 会报
|
||
# "expected '{' in function body"(`[^{]*` 会跨行吞掉换行,--dwarf 给
|
||
# 每个 define 行尾加了 !dbg 后插入点就落到了附着之后)。故插入点前移到
|
||
# 第一个元数据附着之前。
|
||
attach = _RE_METADATA_ATTACH.search(head)
|
||
if attach is None:
|
||
return head + ' comdat ' + m.group(4)
|
||
return head[:attach.start()] + ' comdat' + head[attach.start():] + m.group(4)
|
||
ll_content = re.sub(
|
||
r'^(define\s+(?:dso_local\s+)?(?:%s)\s+[^@]*@("?)([^"\s\(]+)\2[^{]*)(\{)' % weak_links,
|
||
_fn_repl, ll_content, flags=re.MULTILINE)
|
||
|
||
# 2) 全局定义:@name = <linkage> [dso_local] (global|constant) <init> → 值后补 ', comdat'
|
||
# (comdat 需位于 align/section 等尾部属性之前,符合 LLVM IR 文法)
|
||
def _g_repl(m: re.Match) -> str:
|
||
line = m.group(0)
|
||
if ' comdat' in line:
|
||
return line
|
||
comdat_names.append(m.group(3))
|
||
ins = _RE_COMDAT_TAIL_ATTRS.search(line)
|
||
if ins:
|
||
return line[:ins.start()] + ', comdat' + line[ins.start():]
|
||
return line + ', comdat'
|
||
ll_content = re.sub(
|
||
r'^(@("?)([^"=\s]+)\2\s*=\s*(?:%s)\s+(?:dso_local\s+)?(?:global|constant)[^\n]*)' % weak_links,
|
||
_g_repl, ll_content, flags=re.MULTILINE)
|
||
|
||
if not comdat_names:
|
||
return ll_content
|
||
decls: list[str] = []
|
||
for name in dict.fromkeys(comdat_names):
|
||
# fail-fast:LLVM 的引号标识符 @"..." **不支持转义**(LLLexer 读到 `"` 即认为名字
|
||
# 结束),故名字里含 `"` / `\` / 换行时,加不加引号都产不出合法 IR。此处不静默
|
||
# 跳过、也不做字符替换(那会把符号名改得与源码/其它模块不一致,链接期更难查),
|
||
# 直接指名报错。名字来自弱符号命名 `<sha1>.<标识符>`,出现这些字符说明上游符号名
|
||
# 构造有误,应在上游修正。
|
||
if '"' in name or '\\' in name or '\n' in name or '\r' in name:
|
||
raise RuntimeError(
|
||
f'comdat 名无法表达为 LLVM 标识符(含双引号/反斜杠/换行): {name!r};'
|
||
'LLVM 引号名 @"..." 不支持转义,产出即非法 IR(opt 解析报 '
|
||
'"expected top-level entity")。请在上游符号名构造处修正。')
|
||
if _LLVM_UNQUOTED_NAME_RE.match(name):
|
||
decls.append(f'${name} = comdat any')
|
||
else:
|
||
decls.append(f'$"{name}" = comdat any')
|
||
comdat_block = '\n'.join(decls) + '\n'
|
||
lines = ll_content.splitlines(True)
|
||
header_end = 0
|
||
for i, line in enumerate(lines):
|
||
stripped = line.strip()
|
||
if stripped.startswith(';') or stripped.startswith('target ') or stripped.startswith('source_filename') or not stripped:
|
||
header_end = i + 1
|
||
continue
|
||
break
|
||
return ''.join(lines[:header_end]) + comdat_block + ''.join(lines[header_end:])
|
||
|
||
|
||
class Phase2Translator:
|
||
"""阶段二:使用声明接口翻译源文件"""
|
||
|
||
INCLUDE_LIB_EXTENSIONS: tuple[str, ...] = ('.dll', '.ll', '.so', '.o', '.a', '.lib')
|
||
INCLUDE_SRC_EXTENSIONS: tuple[str, ...] = ('.c', '.cpp', '.cc', '.cxx', '.m', '.mm')
|
||
# includes 只认项目配置(project.vpj includes),禁止仓库硬编码回退。
|
||
# INCLUDE_DIRS 保留为空以兼容旧引用;实际目录由 __init__ 写入 PROJECT_INCLUDE_DIRS。
|
||
INCLUDE_DIRS: list[str] = []
|
||
PROJECT_INCLUDE_DIRS: list[str] = []
|
||
# include 源文件 sha1/存在性缓存(_build_current_ModuleSha1Map):
|
||
# 编译期间 include .vp/.py 源文件不变,避免每文件重复 isfile + open +
|
||
# compute_sha1(PROF 显示 9306 isfile + 4653 open + TextIOWrapper.read
|
||
# 35.9s 的大头来源,99 次调用 × 全量 include 重算)。
|
||
_include_sha1_cache: dict[str, str] = {}
|
||
_include_exist_cache: dict[str, bool] = {}
|
||
|
||
def __init__(self, src_root: str, temp_dir: str, output_dir: str, compile_cmd: str = 'llc', compile_flags: list[str] | None = None, linker_cmd: str | None = None, linker_flags: list[str] | None = None, linker_output: str | None = None, static_libs: list[str] | None = None, dynamic_libs: list[str] | None = None, include_dirs: list[str] | None = None, entry_files: list[str] | None = None, target_triple: str | None = None, target_datalayout: str | None = None, force_recompile_includes: bool = False, build_pyi_cache: bool = True, asan_mode: bool = False, dwarf_enabled: bool = False, traceback_enabled: bool = False) -> None:
|
||
self.src_root = os.path.abspath(src_root)
|
||
self.temp_dir = os.path.abspath(temp_dir)
|
||
self.output_dir = os.path.abspath(output_dir)
|
||
self.compile_cmd = compile_cmd
|
||
self.compile_flags = compile_flags or ['-filetype=obj']
|
||
self.triple = target_triple
|
||
self.datalayout = target_datalayout
|
||
if not self.triple:
|
||
for i, flag in enumerate(self.compile_flags):
|
||
if flag.startswith('-mtriple='):
|
||
self.triple = flag[len('-mtriple='):]
|
||
elif flag == '-mtriple' and i + 1 < len(self.compile_flags):
|
||
self.triple = self.compile_flags[i + 1]
|
||
self.linker_cmd = linker_cmd
|
||
self.linker_flags = linker_flags or []
|
||
self.linker_output = linker_output
|
||
# ASan 模式:由 Config 的 --asan 参数直传(不再用 TPC_ASAN 环境变量,
|
||
# 环境变量在 worker 子进程/外部调用链下不可靠)。
|
||
# 检测 stdlib 堆/栈/全局越界与垃圾指针(MemBuddy pool 内部由 canary 覆盖)。
|
||
# 注意:mingw64 的 clang 不提供 x86_64-w64-windows-gnu 目标的 ASan runtime,
|
||
# 只能借用 clang64(MSVC ABI) 的那两份:PATH 末尾追加 clang64/bin(运行期 DLL),
|
||
# 链接时显式 -l: 那两份 .a/.dll.a(见 _FindAsanRuntime 的说明)。
|
||
self.asan_mode = asan_mode
|
||
self.asan_rt_dir: str | None = None
|
||
if self.asan_mode:
|
||
_rt = _FindAsanRuntime()
|
||
if _rt is not None:
|
||
self.asan_rt_dir, _asan_bin = _rt
|
||
# clang64/bin 必须**追加**到 PATH 末尾:既要让运行期能找到
|
||
# libclang_rt.asan_dynamic-x86_64.dll,又不能前置 —— 前置会让裸名
|
||
# clang++.exe / llc / opt 解析到 clang64 的 libc++ 版本,与 includes/
|
||
# 里 mingw64(libstdc++) 编的静态库链接不上。
|
||
if _asan_bin and os.path.isdir(_asan_bin):
|
||
os.environ['PATH'] = os.environ.get('PATH', '') + os.pathsep + _asan_bin
|
||
self.slice_level = 3
|
||
# 函数级 DWARF 调试元数据(--dwarf;--asan 时自动开启):False=未启用
|
||
# (零影响,不构造任何 DI 元数据);True=App 源模块的 .ll 携带
|
||
# !llvm.dbg.cu + 每函数 DISubprogram + 入口 DILocation,供 ASan /
|
||
# llvm-symbolizer 反查 ".vp:函数起始行"。
|
||
self.dwarf_enabled: bool = dwarf_enabled
|
||
# Ctrl+C 调用栈(--traceback):False=未启用(零影响,不调用任何外部
|
||
# 工具、不写任何旁文件);True=链接成功后额外用 llvm-dwarfdump 把最终
|
||
# exe 的 DWARF 行表导出为 "<exe>.stackmap"(地址→源行旁文件,供 exe 内
|
||
# 运行时在 Ctrl+C/Ctrl+Break 时逐帧翻译返回地址)。要求 --dwarf 已开启
|
||
# (Config 侧联动,否则行表为空),且目标为 Windows PE。
|
||
self.traceback_enabled: bool = traceback_enabled
|
||
# 调用点热点统计(--hotspot-sites):None=未启用(零影响);str=调用点计数
|
||
# 落盘文件路径(每函数计时落同目录 hotspot_time.txt)。各模块运行时自带
|
||
# Ctrl+C 处理器、统一以追加方式写该路径;含 main 的模块在 main 入口清空一次
|
||
# → 每次运行一份结果。
|
||
self.hotspot_sites_file: str | None = None
|
||
# 是否对全部直接 call 点插桩(忽略白名单);白名单被调函数基名列表(None=默认)
|
||
self.hotspot_sites_all: bool = False
|
||
self.hotspot_callees: list[str] | None = None
|
||
self.sha1_map: dict[str, str] = {}
|
||
self.sig_files: dict[str, str] = {}
|
||
self.stub_files: dict[str, str] = {}
|
||
self.include_py_map: dict[str, str] = {}
|
||
self.used_includes: set[str] = set()
|
||
self._last_ErrorStack: list | None = None
|
||
self.function_default_args: dict[str, list] = {}
|
||
self.function_param_names: dict[str, list] = {}
|
||
self.inline_func_symbols: dict[str, CTypeInfo] = {}
|
||
self.extra_link_files: list[str] = []
|
||
self.extra_compile_files: list[str] = []
|
||
self.extra_py_files: list[str] = []
|
||
# project.vpj linker.static_libs:显式静态链接文件(.a/.obj),
|
||
# 加入 extra_link_files 参与最终链接(位于 -o 之后、-l 库标志之前)。
|
||
if static_libs:
|
||
for lib in static_libs:
|
||
if lib not in self.extra_link_files:
|
||
self.extra_link_files.append(lib)
|
||
# project.vpj linker.dynamic_libs:显式动态链接文件(.dll/.so),
|
||
# 同样作为链接输入(GNU ld 从 DLL/SO 提取导入符号);运行时需
|
||
# DLL/SO 位于 exe 同目录 / PATH / LD_LIBRARY_PATH。
|
||
if dynamic_libs:
|
||
for lib in dynamic_libs:
|
||
if lib not in self.extra_link_files:
|
||
self.extra_link_files.append(lib)
|
||
self._include_sha1s: set[str] = set()
|
||
# 持有 ProcessPoolExecutor 引用,避免局部变量 GC 时 finalizer 阻塞 ~99s
|
||
# (join_executor_internals 在 GC 时同步等待 worker 退出队列)
|
||
self._executor_pool: ProcessPoolExecutor | None = None
|
||
self.entry_files: list[str] | None = entry_files
|
||
self.force_recompile_includes: bool = force_recompile_includes
|
||
# pyi cache 构建开关:仅主进程编译启动时构建一次(_LoadSha1Map 内),
|
||
# worker 子进程只读主进程落盘的 temp/_pyi_cache_v1.pkl(_PyiCacheWorkerInitializer),
|
||
# 不重复构建——worker 每任务重建 pkl 的 pickle.load/dump(43 次 × ~0.5s ≈ 23s
|
||
# + pickle.loads 10.4s)纯属浪费;worker 缺 includes vpi 时运行期按需 parse 回退。
|
||
self._build_pyi_cache: bool = build_pyi_cache
|
||
self._shared_symbol_table: object | None = None
|
||
self._shared_source_module_sig_files: dict[str, str] = {}
|
||
self._shared_all_dc: dict[str, dict] = {}
|
||
self._shared_all_decl_types: dict[str, tuple[int, bool]] = {}
|
||
self._shared_export_extern_funcs: dict[str, str] = {}
|
||
self.include_dirs: list[str] = []
|
||
self.struct_sha1_map: dict[str, str] = {}
|
||
self._shared_generic_class_templates: dict[str, dict] = {}
|
||
self._shared_doc_meta: dict[str, dict[str, str]] = {}
|
||
# 共享 vtable 状态跨模块(修复 pool.alloc(48) 虚表分派失败导致段错误)
|
||
self._shared_class_vtable: set[str] = set()
|
||
self._shared_cross_module_vtable: set[str] = set()
|
||
self._shared_class_methods: dict[str, list[str]] = {}
|
||
self._shared_cross_module_novtable: set[str] = set()
|
||
default_dirs: list[str] = []
|
||
if include_dirs:
|
||
seen = set()
|
||
self.include_dirs = []
|
||
for d in list(include_dirs) + default_dirs:
|
||
d = os.path.abspath(d)
|
||
if d not in seen and os.path.isdir(d):
|
||
self.include_dirs.append(d)
|
||
seen.add(d)
|
||
else:
|
||
self.include_dirs = default_dirs
|
||
# 严格:只认项目 includes;供 builtins.vp 扫描等静态路径使用
|
||
Phase2Translator.PROJECT_INCLUDE_DIRS = list(self.include_dirs)
|
||
self._LoadSha1Map()
|
||
|
||
def _LoadSha1Map(self) -> None:
|
||
"""从 temp_dir 加载 SHA1 映射、签名文件和 stub 声明文件列表。
|
||
temp_dir 中缺失的文件会从全局缓存补充。
|
||
"""
|
||
if not os.path.exists(self.temp_dir):
|
||
_vlog().error(f"声明目录不存在: {self.temp_dir}")
|
||
return
|
||
|
||
for fname in os.listdir(self.temp_dir):
|
||
fpath = os.path.join(self.temp_dir, fname)
|
||
if fname.startswith('_'):
|
||
continue
|
||
if fname.endswith('.vpi') or fname.endswith('.pyi'):
|
||
sha1 = fname[:-4]
|
||
self.sig_files[sha1] = fpath
|
||
elif fname.endswith('.stub.ll'):
|
||
sha1 = fname[:-8]
|
||
self.stub_files[sha1] = fpath
|
||
|
||
# 从内存存储器读取 SHA1 映射(_sha1_map.txt 仅人类可读,不再作为机器数据源)
|
||
# 延迟导入避免循环依赖(Config 导入 Phase2Translator)
|
||
from lib.Projectrans.Config import get_sha1_map_store
|
||
mem_store: dict[str, str] = get_sha1_map_store()
|
||
if mem_store:
|
||
for sha1, rel in mem_store.items():
|
||
self.sha1_map[sha1] = rel
|
||
if rel.startswith('includes/'):
|
||
module_name = os.path.splitext(os.path.basename(rel))[0]
|
||
self.include_py_map[module_name] = sha1
|
||
else:
|
||
# 内存存储器为空(独立运行 Phase2 的回退):从 stub 文件名推断
|
||
for sha1, stub_path in self.stub_files.items():
|
||
self.sha1_map[sha1] = sha1
|
||
|
||
# 从全局缓存补充 temp_dir 中缺失的 .vpi / .stub.ll。
|
||
# 只补充"当前编译有效"的 sha1(在 sha1_map 中):.transpyc_cache 会残留
|
||
# 旧版源码的缓存(如旧 __builder.vp 的 9026a429f5e2d0e0.pyi/.stub.ll 与新
|
||
# d9b3d06934cd1898 共存),不加过滤会把旧版 .vpi/.stub.ll 也加载进
|
||
# sig_files/stub_files → 同名类(IRBuilder)按旧 sha1 前缀注册 →
|
||
# ld.lld undefined symbol: 9026a429f5e2d0e0.new_builder。
|
||
if os.path.isdir(_GLOBAL_CACHE_DIR):
|
||
for fname in os.listdir(_GLOBAL_CACHE_DIR):
|
||
if fname.startswith('_'):
|
||
continue
|
||
if fname.endswith('.vpi') or fname.endswith('.pyi'):
|
||
sha1 = fname[:-4]
|
||
if sha1 in self.sha1_map and sha1 not in self.sig_files:
|
||
self.sig_files[sha1] = os.path.join(_GLOBAL_CACHE_DIR, fname)
|
||
elif fname.endswith('.stub.ll'):
|
||
sha1 = fname[:-8]
|
||
if sha1 in self.sha1_map and sha1 not in self.stub_files:
|
||
self.stub_files[sha1] = os.path.join(_GLOBAL_CACHE_DIR, fname)
|
||
|
||
# 构建 pyi AST pickle 缓存(temp/_pyi_cache_v1.pkl)供 worker 预热:
|
||
# Phase1 已生成全部 .vpi,此处把 vast 树落盘,避免每个 worker 首次任务
|
||
# 重新 vast.parse(4479 次 parse / 245s)。签名未变时零开销跳过。
|
||
# 仅主进程构建(build_pyi_cache=True);worker 只读 pkl,不重复构建。
|
||
if self._build_pyi_cache and os.environ.get('TPC_WORKER_WARMUP', '1') == '1':
|
||
_BuildPyiCacheFile(self.temp_dir)
|
||
|
||
def _build_struct_sha1_map(self) -> dict[str, str]:
|
||
struct_sha1_map = {}
|
||
valid_sha1_keys = set(self.sha1_map.keys())
|
||
# 收集 temp_dir 和全局缓存中的 .vpi/.pyi 文件
|
||
pyi_dirs: list[str] = [self.temp_dir]
|
||
if os.path.isdir(_GLOBAL_CACHE_DIR):
|
||
pyi_dirs.append(_GLOBAL_CACHE_DIR)
|
||
_scanned_sha1: set[str] = set()
|
||
for scan_dir in pyi_dirs:
|
||
if not os.path.isdir(scan_dir):
|
||
continue
|
||
for fname in os.listdir(scan_dir):
|
||
if not (fname.endswith('.vpi') or fname.endswith('.pyi')):
|
||
continue
|
||
sha1_key = fname.replace('.vpi', '').replace('.pyi', '')
|
||
if sha1_key not in valid_sha1_keys:
|
||
continue
|
||
# 同一模块在两处目录都有文件时只解析一次(与下面的类名归属判据无关)
|
||
if sha1_key in _scanned_sha1:
|
||
continue
|
||
_scanned_sha1.add(sha1_key)
|
||
pyi_path = os.path.join(scan_dir, fname)
|
||
# 复用 parse_python_file 的 AST 缓存(.pyi 文件可能被多处解析)
|
||
_, tree = parse_python_file(pyi_path)
|
||
if tree is None:
|
||
continue
|
||
for node in ast.iter_child_nodes(tree):
|
||
# 每类只登记一次,**先扫到的为准**:判据必须是**类名**。
|
||
# 原实现写成 `if sha1_key in struct_sha1_map` —— 用模块 SHA1 去查一张
|
||
# 按**类名**建的表,判据恒假 ⇒ 实际是"后扫到的覆盖先扫到的",
|
||
# 同名类的归属随 os.listdir 顺序漂移(现场:includes/ast/astaux.vp 与
|
||
# includes/ast/tokens.vp 各有一个 `class Keyword`,定义端拿到的前缀
|
||
# 可能是**另一个模块**的 sha1 ⇒ 同一类出现两个 struct 实例、
|
||
# `__before_init__` 声明与调用点分叉)。
|
||
if isinstance(node, ast.ClassDef):
|
||
# 嵌套类按扁平名 `{外类}_{内类}` 登记(与定义端
|
||
# HandlesClassDef._EmitNestedClassDefs、Phase1Generator.
|
||
# _RegisterNestedStructNames 同源)。此前用 ast.walk 会把嵌套类按
|
||
# **短名** `Inner` 登记 —— 既占住平坦命名空间,又与定义端的
|
||
# `外类_内类` 不同源(跨模块前缀分裂)。
|
||
self._RegisterStructSha1Rec(node, node.name, sha1_key, struct_sha1_map)
|
||
return struct_sha1_map
|
||
|
||
def _RegisterStructSha1Rec(self, Node, FlatName: str, Sha1Key: str,
|
||
StructSha1Map: dict) -> None:
|
||
"""递归登记"类名 → 定义模块 SHA1"(嵌套类用扁平名 `{外类}_{内类}`)"""
|
||
if not FlatName:
|
||
return
|
||
if FlatName not in StructSha1Map:
|
||
StructSha1Map[FlatName] = Sha1Key
|
||
Body = getattr(Node, 'body', None)
|
||
if not Body:
|
||
return
|
||
for Item in Body:
|
||
if not isinstance(Item, ast.ClassDef):
|
||
continue
|
||
InnerName: str | None = getattr(Item, 'name', None)
|
||
if not InnerName:
|
||
continue
|
||
self._RegisterStructSha1Rec(Item, f'{FlatName}_{InnerName}', Sha1Key, StructSha1Map)
|
||
|
||
def run(self) -> None:
|
||
"""扫描源目录,翻译并生成 .ll 含代码文件(导入遍历)"""
|
||
import sys as _sys
|
||
if self.entry_files:
|
||
py_files = list(self.entry_files)
|
||
else:
|
||
main_py = os.path.join(self.src_root, 'main.vp')
|
||
if not os.path.exists(main_py):
|
||
# 回退到 main.py(向后兼容)
|
||
main_py = os.path.join(self.src_root, 'main.py')
|
||
if os.path.exists(main_py):
|
||
py_files = [main_py]
|
||
else:
|
||
py_files = []
|
||
for root, dirs, files in os.walk(self.src_root):
|
||
dirs[:] = [d for d in dirs if d not in ('__pycache__',)]
|
||
for file in files:
|
||
if file.endswith('.vp') or file.endswith('.py'):
|
||
py_files.append(os.path.join(root, file))
|
||
|
||
if not py_files:
|
||
_vlog().info("未找到入口文件或源文件")
|
||
return
|
||
|
||
reachable = find_reachable_files_from_entries(self.src_root, py_files)
|
||
py_files = list(reachable)
|
||
|
||
py_files = topological_sort_files(py_files, self.src_root)
|
||
|
||
_vlog().info(f"找到 {len(py_files)} 个可达源文件(已按依赖排序)")
|
||
_vlog().info("处理顺序:")
|
||
for i, f in enumerate(py_files[:10], 1):
|
||
rel = os.path.relpath(f, self.src_root)
|
||
_vlog().info(f" {i}. {rel}")
|
||
os.makedirs(self.output_dir, exist_ok=True)
|
||
|
||
valid_sha1s = set(self.sha1_map.keys())
|
||
old_stub_count = len(self.stub_files)
|
||
old_sig_count = len(self.sig_files)
|
||
self.stub_files = {k: v for k, v in self.stub_files.items() if k in valid_sha1s}
|
||
# sig_files 不能按 sha1_map(仅 App 源文件)过滤:include 模块的 .vpi
|
||
# 也必须保留,否则跨模块 extern(ReadProcessMemory / StackWalk64 等)
|
||
# 无法识别为裸名,链接时报 undefined symbol。
|
||
self.sig_files = {k: v for k, v in self.sig_files.items() if os.path.isfile(v)}
|
||
if old_stub_count != len(self.stub_files) or old_sig_count != len(self.sig_files):
|
||
_vlog().info(f"过滤旧文件: stub {old_stub_count}->{len(self.stub_files)}, sig {old_sig_count}->{len(self.sig_files)}")
|
||
|
||
active_sha1s = set()
|
||
self._precollect_inline_symbols()
|
||
self._build_shared_symbol_data()
|
||
self._precollect_vtable_state()
|
||
self.struct_sha1_map = self._build_struct_sha1_map()
|
||
|
||
need_translate = []
|
||
# --hotspot-sites:缓存 .ll / .stub.ll+.text.ll 可能不含热点计数/计时 IR,
|
||
# 启用时一律重译,保证每个模块都嵌入 internal 计数器、计时器与
|
||
# @llvm.global_ctors 自注册。未启用时 hotspot_on=False,缓存与重组分支
|
||
# 行为与改动前完全一致(零影响)。
|
||
# --dwarf(函数级 DWARF)同理:缓存 .ll/.obj 不含 !llvm.dbg.cu,必须一并
|
||
# 强制重译,否则开启开关后仍复用旧产物、看不到任何 .vp 行号。
|
||
# --traceback 依赖同一份 .debug_line 行表(Config 已联动开启 --dwarf),
|
||
# 这里显式纳入,避免将来联动改动后静默复用无 DI 的旧产物。
|
||
hotspot_on: bool = bool(self.hotspot_sites_file) or bool(self.dwarf_enabled) or bool(self.traceback_enabled)
|
||
for i, src_path in enumerate(py_files, 1):
|
||
rel = os.path.relpath(src_path, self.src_root)
|
||
# 复用 parse_python_file 的 AST 缓存(get_file_dependencies 已 parse 过)
|
||
content, tree = parse_python_file(src_path)
|
||
sha1 = compute_sha1(content)
|
||
active_sha1s.add(sha1)
|
||
out_path = os.path.join(self.output_dir, f"{sha1}.ll")
|
||
|
||
current_ModuleSha1Map = self._build_current_ModuleSha1Map(sha1)
|
||
|
||
if tree is not None:
|
||
try:
|
||
for node in ast.walk(tree):
|
||
if isinstance(node, ast.Import):
|
||
for alias in node.names:
|
||
self.used_includes.add(alias.name.split('.')[0])
|
||
elif isinstance(node, ast.ImportFrom):
|
||
if node.module:
|
||
self.used_includes.add(node.module.split('.')[0])
|
||
# 检测 assert 使用,自动添加 testcheck 到 used_includes:
|
||
# assert 译为 testcheck.check(...),该库须被发现、编译并链接。
|
||
if isinstance(node, ast.Assert):
|
||
self.used_includes.add('testcheck')
|
||
# 检测 list/dict 容器使用,自动添加 _list/_dict/json 到 used_includes
|
||
# 以便 _scan_include_libraries 能发现并编译这些库文件
|
||
if isinstance(node, ast.Subscript) and isinstance(node.value, ast.Name):
|
||
if node.value.id == 'dict':
|
||
self.used_includes.add('_dict')
|
||
self.used_includes.add('json')
|
||
elif node.value.id == 'list':
|
||
self.used_includes.add('_list')
|
||
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
|
||
if node.func.id == 'dict':
|
||
self.used_includes.add('_dict')
|
||
self.used_includes.add('json')
|
||
elif node.func.id == 'list':
|
||
self.used_includes.add('_list')
|
||
# 检测类型注解 d: dict / l: list (非泛型形式)
|
||
if isinstance(node, ast.AnnAssign) and isinstance(node.annotation, ast.Name):
|
||
if node.annotation.id == 'dict':
|
||
self.used_includes.add('_dict')
|
||
self.used_includes.add('json')
|
||
elif node.annotation.id == 'list':
|
||
self.used_includes.add('_list')
|
||
except Exception as _e:
|
||
if _config_mode == "strict":
|
||
raise
|
||
_vlog().warning(f"收集源文件导入的 include 模块失败: {_e}", "Exception")
|
||
|
||
# --hotspot-sites:上面两个缓存/重组分支在 hotspot_on 时被跳过,
|
||
# 本轮每个模块都会强制重译 .ll;此时必须让旧的 {sha1}.obj 一并失效,
|
||
# 否则 _compile_ll_files 会因 .obj 已存在而跳过重编,复用上一次未插桩
|
||
# 的目标文件,导致部分模块热点计时/计数静默缺失、耗时被计入调用者 self。
|
||
# 这里用 os.path.isfile 而非 _CachedIsFile:避免把"已存在"写进存在性
|
||
# 缓存后又被本函数删除,使后续 _compile_ll_files 读到过期缓存而漏编。
|
||
if hotspot_on:
|
||
obj_path = os.path.join(self.output_dir, f"{sha1}.obj")
|
||
if os.path.isfile(obj_path):
|
||
try:
|
||
os.remove(obj_path)
|
||
except OSError as e:
|
||
# 不中断构建,但真实错误(如权限不足)必须打印告警,不静默吞掉
|
||
_vlog().warning(
|
||
f"Phase2Translator: 删除热点插桩模块的旧目标文件失败: {obj_path}: {e}")
|
||
|
||
if not hotspot_on and _CachedIsFile(out_path):
|
||
deps_changed = self._check_deps_changed(sha1, current_ModuleSha1Map)
|
||
if not deps_changed:
|
||
_vlog().info(f"[{i}/{len(py_files)}] 跳过(缓存): {rel} -> {sha1}.ll")
|
||
continue
|
||
else:
|
||
if self._recombine_ll(sha1, current_ModuleSha1Map):
|
||
_vlog().info(f"[{i}/{len(py_files)}] 重组(依赖变化): {rel} -> {sha1}.ll")
|
||
continue
|
||
_vlog().info(f"[{i}/{len(py_files)}] 重译(依赖变化): {rel} -> {sha1}.ll")
|
||
|
||
elif not hotspot_on:
|
||
stub_cache = os.path.join(self.output_dir, f"{sha1}.stub.ll")
|
||
text_cache = os.path.join(self.output_dir, f"{sha1}.text.ll")
|
||
if _CachedIsFile(stub_cache) and _CachedIsFile(text_cache):
|
||
if self._recombine_ll(sha1, current_ModuleSha1Map):
|
||
_vlog().info(f"[{i}/{len(py_files)}] 重组(缓存恢复): {rel} -> {sha1}.ll")
|
||
continue
|
||
|
||
need_translate.append((i, src_path, out_path, sha1, current_ModuleSha1Map))
|
||
|
||
if need_translate:
|
||
# 提前从 include 文件源码中提取函数参数名,供主项目编译时 keyword 参数匹配使用
|
||
self._extract_include_param_names_from_dirs()
|
||
# 提前从 App 源文件中提取函数默认参数,供跨模块调用参数检查使用
|
||
# (避免并行编译时,被调用函数的文件尚未编译完,导致默认参数信息缺失)
|
||
self._extract_app_default_args(need_translate)
|
||
|
||
_vlog().info(f"\n需要翻译 {len(need_translate)} 个文件")
|
||
t_start = time.time()
|
||
|
||
n_workers = _PickWorkerCount(len(need_translate))
|
||
|
||
if n_workers > 1 and len(need_translate) > 1:
|
||
_vlog().info(f" 并行翻译 (workers={n_workers})")
|
||
|
||
shared_pickle_path = os.path.join(self.temp_dir, '_shared_sym.json')
|
||
try:
|
||
# symbol_table_bytes:SymbolTable 独立 pickle 字节,供批次 worker
|
||
# 每文件前反序列化出干净副本(SymbolTable 是编译期唯一被写的共享
|
||
# 对象,批次内复用 trans 时必须在每个文件前重置,防止前文件 import
|
||
# 写入的符号污染后文件 → 跨模块 struct 类型前缀错位)。
|
||
symbol_table_bytes: bytes = pickle.dumps(self._shared_symbol_table)
|
||
shared_data = {
|
||
'symbol_table_bytes': symbol_table_bytes,
|
||
'source_module_sig_files': self._shared_source_module_sig_files,
|
||
'all_dc': self._shared_all_dc,
|
||
'all_decl_types': self._shared_all_decl_types,
|
||
'export_extern_funcs': self._shared_export_extern_funcs,
|
||
'inline_func_symbols': self.inline_func_symbols,
|
||
'function_default_args': self.function_default_args,
|
||
'function_param_names': self.function_param_names,
|
||
'generic_class_templates': self._shared_generic_class_templates,
|
||
'shared_doc_meta': self._shared_doc_meta,
|
||
'class_vtable': self._shared_class_vtable,
|
||
'cross_module_vtable': self._shared_cross_module_vtable,
|
||
'class_methods': self._shared_class_methods,
|
||
'cross_module_novtable': self._shared_cross_module_novtable,
|
||
}
|
||
with open(shared_pickle_path, 'wb') as f:
|
||
f.write(pickle.dumps(shared_data))
|
||
_vlog().info(f" 共享符号表已序列化 ({os.path.getsize(shared_pickle_path)//1024}KB)")
|
||
except Exception as e:
|
||
_vlog().warning(f"符号表序列化失败({e}),worker将自行构建")
|
||
shared_pickle_path = ''
|
||
|
||
# 复用 self._executor_pool:避免局部变量 GC 时 finalizer 阻塞 ~99s
|
||
# (join_executor_internals 在 GC 时同步等待 worker 退出队列)
|
||
if self._executor_pool is None:
|
||
# initializer 预热 ImportHandle._pyi_cache(pickle.load 落盘缓存),
|
||
# 避免每个 worker 首次任务重新 vast.parse 全部依赖 .vpi
|
||
self._executor_pool = ProcessPoolExecutor(max_workers=n_workers,
|
||
initializer=_PyiCacheWorkerInitializer,
|
||
initargs=(self.temp_dir,))
|
||
executor = self._executor_pool
|
||
try:
|
||
# 批次子任务模型:多个文件包装为一个子任务(batch),
|
||
# 每 worker 处理一批——Phase2Translator 创建 + pickle.load 只做
|
||
# 一次,批内文件顺序翻译(每文件前重置 SymbolTable 防污染)。
|
||
# 相比"一文件一任务":减少进程池调度/通信/构造开销
|
||
# (200+ 文件 → n_workers 个批次任务)。
|
||
batches: list[list] = []
|
||
# 按源文件大小加权分桶(LPT 贪心:最大文件优先放入当前总成本最小的桶),
|
||
# 使各批次总成本均匀。原按文件数均分(batch_size=ceil(N/n))导致大文件
|
||
# 集中的批次拖长尾,其他 worker 提前完成空等(任务颗粒度不均匀)。
|
||
if len(need_translate) <= n_workers:
|
||
batches = [[item] for item in need_translate]
|
||
else:
|
||
sized_items: list = []
|
||
for item in need_translate:
|
||
try:
|
||
_size: int = os.path.getsize(item[1])
|
||
except OSError:
|
||
_size = 0
|
||
sized_items.append((item, _size))
|
||
buckets: list[list] = [[] for _ in range(n_workers)]
|
||
bucket_cost: list[int] = [0] * n_workers
|
||
for item, _size in sorted(sized_items, key=lambda t: t[1], reverse=True):
|
||
target: int = min(range(n_workers), key=lambda w: bucket_cost[w])
|
||
buckets[target].append(item)
|
||
bucket_cost[target] += _size
|
||
batches = [b for b in buckets if b]
|
||
futures = {}
|
||
for bidx, batch in enumerate(batches):
|
||
rel_first = os.path.relpath(batch[0][1], self.src_root)
|
||
future = executor.submit(
|
||
_parallel_translate_batch,
|
||
batch,
|
||
self.src_root, self.temp_dir, self.output_dir,
|
||
self.include_dirs, self.triple, self.datalayout,
|
||
self.sha1_map, self.sig_files, self.stub_files,
|
||
self.include_py_map, self.slice_level,
|
||
shared_pickle_path, self.struct_sha1_map,
|
||
hotspot_sites_file=self.hotspot_sites_file,
|
||
hotspot_sites_all=self.hotspot_sites_all,
|
||
hotspot_callees=self.hotspot_callees,
|
||
dwarf_enabled=self.dwarf_enabled,
|
||
)
|
||
futures[future] = (bidx, rel_first, len(batch))
|
||
|
||
done_files = 0
|
||
total_files = len(need_translate)
|
||
for future in as_completed(futures):
|
||
bidx, rel_first, bcount = futures[future]
|
||
try:
|
||
batch_errors, batch, per_file_times = future.result()
|
||
except ReportedCompileError:
|
||
raise
|
||
except Exception as e:
|
||
# fail-fast:进程池已 broken(worker 异常退出)→ 取消
|
||
# 全部剩余批次并终止编译,不空等。
|
||
_vlog().error(f" 批次 {bidx} 异常(worker 已终止): {e}")
|
||
for fut in futures:
|
||
if not fut.done():
|
||
fut.cancel()
|
||
if self._executor_pool is not None:
|
||
self._executor_pool.shutdown(wait=False, cancel_futures=True)
|
||
self._executor_pool = None
|
||
raise ReportedCompileError(
|
||
f"批次 {bidx} worker 异常终止,编译立即停止") from None
|
||
if batch_errors:
|
||
# worker 只回传错误、不写 stderr:完整错误(含源码定位)
|
||
# 只在这里输出一次,随后立即终止(fail-fast)。
|
||
src, err = batch_errors[0]
|
||
rel = os.path.relpath(src, self.src_root) if src else rel_first
|
||
_vlog().error(f"\n文件翻译失败: {rel}\n{err}")
|
||
for fut in futures:
|
||
if not fut.done():
|
||
fut.cancel()
|
||
if self._executor_pool is not None:
|
||
self._executor_pool.shutdown(wait=False, cancel_futures=True)
|
||
self._executor_pool = None
|
||
raise ReportedCompileError(
|
||
f"{rel} 翻译失败,编译立即停止") from None
|
||
for src, elapsed, rel in per_file_times:
|
||
done_files += 1
|
||
_vlog().info(f" [{done_files}/{total_files}] {rel} ({elapsed:.1f}s)")
|
||
_vlog().info(f" [批次完成] {bcount} 文件, 首个 {rel_first}")
|
||
except Exception:
|
||
# 异常时立即清理,正常流程不 shutdown(留给 includes 阶段复用)
|
||
if self._executor_pool is not None:
|
||
self._executor_pool.shutdown(wait=False, cancel_futures=True)
|
||
self._executor_pool = None
|
||
raise
|
||
|
||
else:
|
||
for idx, (i, src_path, out_path, sha1, msm) in enumerate(need_translate):
|
||
rel = os.path.relpath(src_path, self.src_root)
|
||
t0 = time.time()
|
||
try:
|
||
self._translate_file(src_path, out_path, prebuilt_ModuleSha1Map=msm, dwarf_enabled=self.dwarf_enabled)
|
||
t1 = time.time()
|
||
_vlog().info(f" [{idx+1}/{len(need_translate)}] {rel} ({t1-t0:.1f}s)")
|
||
except Exception as e:
|
||
# 单点输出:完整错误(含源码定位)只打印这一次
|
||
_vlog().error(f"\n文件翻译失败: {rel}\n{type(e).__name__}: {e}")
|
||
raise ReportedCompileError(
|
||
f"翻译 {rel} 失败,已终止编译") from None
|
||
t_end = time.time()
|
||
_vlog().info(f" 翻译总耗时: {t_end-t_start:.1f}s")
|
||
|
||
_vlog().info(f"\n[阶段二完成] .ll 文件生成到: {self.output_dir}")
|
||
self._scan_include_libraries()
|
||
self._compile_include_py_files(active_sha1s)
|
||
self._compile_ll_files(active_sha1s)
|
||
self._compile_include_sources()
|
||
if self.linker_cmd:
|
||
self._link_obj_files(active_sha1s)
|
||
# 清理 ProcessPoolExecutor:所有翻译已完成,shutdown(wait=False) 让 worker
|
||
# 后台退出。避免 GC finalizer 在程序末尾阻塞 ~99s(join_executor_internals)
|
||
if self._executor_pool is not None:
|
||
self._executor_pool.shutdown(wait=False, cancel_futures=True)
|
||
self._executor_pool = None
|
||
|
||
def _build_current_ModuleSha1Map(self, self_sha1: str | None) -> dict[str, str]:
|
||
"""构建当前模块的依赖 SHA1 映射(与 _translate_file 中逻辑一致)"""
|
||
ModuleSha1Map = {}
|
||
for sha1_key, sig_path in self.sig_files.items():
|
||
if sha1_key == self_sha1:
|
||
continue
|
||
rel_path = self.sha1_map.get(sha1_key, sha1_key)
|
||
if rel_path.startswith('includes/'):
|
||
inc_mod_name = os.path.splitext(rel_path)[0].replace(os.sep, '.').replace('/', '.')
|
||
inc_mod_name = inc_mod_name[len('includes/'):]
|
||
inc_short_name = inc_mod_name.split('.')[-1] if '.' in inc_mod_name else inc_mod_name
|
||
actual_sha1 = sha1_key
|
||
for inc_dir in self.include_dirs:
|
||
if os.path.isdir(inc_dir):
|
||
py_path = os.path.join(inc_dir, rel_path[len('includes/'):].replace(os.sep, '/'))
|
||
py_path = os.path.splitext(py_path)[0] + '.vp'
|
||
# 存在性/内容缓存:编译期 include 源文件不变,命中跳过磁盘 I/O
|
||
exists = Phase2Translator._include_exist_cache.get(py_path)
|
||
if exists is None:
|
||
exists = _CachedIsFile(py_path)
|
||
Phase2Translator._include_exist_cache[py_path] = exists
|
||
if not exists:
|
||
# 回退到 .py(向后兼容)
|
||
py_path = os.path.splitext(py_path)[0] + '.py'
|
||
exists = Phase2Translator._include_exist_cache.get(py_path)
|
||
if exists is None:
|
||
exists = _CachedIsFile(py_path)
|
||
Phase2Translator._include_exist_cache[py_path] = exists
|
||
if exists:
|
||
py_sha1 = Phase2Translator._include_sha1_cache.get(py_path)
|
||
if py_sha1 is None:
|
||
py_sha1 = compute_sha1(_ReadTextCached(py_path))
|
||
Phase2Translator._include_sha1_cache[py_path] = py_sha1
|
||
actual_sha1 = py_sha1
|
||
break
|
||
ModuleSha1Map[inc_mod_name] = actual_sha1
|
||
ModuleSha1Map[inc_short_name] = actual_sha1
|
||
# Also add parent package name (e.g., 'hashlib' for 'hashlib.__init__')
|
||
if inc_short_name == '__init__' and '.' in inc_mod_name:
|
||
parent_pkg = inc_mod_name.rsplit('.', 1)[0]
|
||
ModuleSha1Map[parent_pkg] = actual_sha1
|
||
continue
|
||
module_name = os.path.splitext(rel_path)[0].replace(os.sep, '.').replace('/', '.')
|
||
ModuleSha1Map[module_name] = sha1_key
|
||
short_name = module_name.split('.')[-1] if '.' in module_name else module_name
|
||
ModuleSha1Map[short_name] = sha1_key
|
||
# 包入口文件:注册父包名(如 'os.__init__' -> 'os'),使 import os 能解析到包级符号
|
||
if short_name == '__init__' and '.' in module_name:
|
||
parent_pkg = module_name.rsplit('.', 1)[0]
|
||
ModuleSha1Map[parent_pkg] = sha1_key
|
||
return ModuleSha1Map
|
||
|
||
@staticmethod
|
||
def _extract_llvm_type_name(line: str) -> str | None:
|
||
"""从 LLVM IR 类型定义行提取类型名(字符串操作,不使用正则)
|
||
|
||
支持 list[str]、dict[str, int] 等含特殊字符的类型名。
|
||
例如:
|
||
%"18e6990aa81cc5b6.list[str]" = type { ... } → "18e6990aa81cc5b6.list[str]"
|
||
%struct.foo = type { ... } → "struct.foo"
|
||
"""
|
||
stripped = line.strip()
|
||
if not stripped.startswith('%'):
|
||
return None
|
||
# 处理带引号的情况: %"name" = type ...
|
||
if stripped.startswith('%"'):
|
||
end_quote = stripped.find('"', 2)
|
||
if end_quote > 0:
|
||
return stripped[2:end_quote]
|
||
return None
|
||
# 处理不带引号的情况: %name = type ...
|
||
eq_pos = stripped.find('=')
|
||
if eq_pos > 1:
|
||
return stripped[1:eq_pos].strip()
|
||
return None
|
||
|
||
@staticmethod
|
||
def _find_all_type_refs(text: str) -> set[str]:
|
||
"""查找文本中所有 %"..."/%name 类型引用(字符串操作,不使用正则)
|
||
|
||
支持 list[str]、dict[str, int] 等含特殊字符的类型名。
|
||
"""
|
||
refs: set[str] = set()
|
||
i: int = 0
|
||
n: int = len(text)
|
||
while i < n:
|
||
if text[i] == '%':
|
||
if i + 1 < n and text[i + 1] == '"':
|
||
# 带引号: %"name"
|
||
end: int = text.find('"', i + 2)
|
||
if end > 0:
|
||
name: str = text[i + 2:end]
|
||
if name:
|
||
refs.add(name)
|
||
i = end + 1
|
||
continue
|
||
else:
|
||
# 不带引号: %name (仅匹配字母、数字、下划线、点)
|
||
j: int = i + 1
|
||
while j < n and (text[j].isalnum() or text[j] in '._'):
|
||
j += 1
|
||
if j > i + 1:
|
||
refs.add(text[i + 1:j])
|
||
i = j
|
||
continue
|
||
i += 1
|
||
return refs
|
||
|
||
@staticmethod
|
||
def _split_ll(ll_content: str) -> tuple[str, str]:
|
||
"""将 .ll 内容拆分为 stub(声明)和 text(代码)两部分
|
||
|
||
stub: target, 注释, %type = type, declare, @xxx = external global
|
||
text: define ... { ... }, @xxx = global/constant (带初始化器)
|
||
"""
|
||
stub_lines = []
|
||
text_lines = []
|
||
in_define = False
|
||
brace_depth = 0
|
||
module_sha1 = None
|
||
defined_names = set()
|
||
|
||
for line in ll_content.splitlines(True):
|
||
# 第一遍只关心 `define ...` 行,而合并后的 .ll 里这类行占比极低。
|
||
# `'define' not in line` 是 C 级子串扫描 ⇒ 绝大多数行连 strip 都不做
|
||
# (prof 实测 str.strip 1.79 亿次 / str.startswith 2.37 亿次是全流程
|
||
# 最大的 Python 侧热点,本函数两遍逐行扫描是其主要来源)。
|
||
if 'define' not in line:
|
||
continue
|
||
stripped = line.strip()
|
||
|
||
if stripped.startswith('define '):
|
||
dm = _RE_DEFINE_SHA1.match(stripped)
|
||
if dm and module_sha1 is None:
|
||
module_sha1 = dm.group(1)
|
||
dm2 = _RE_DEFINE_NAME.match(stripped)
|
||
if dm2:
|
||
defined_names.add(dm2.group(1))
|
||
|
||
in_global_def = False
|
||
global_var_name = None
|
||
global_type_parts = []
|
||
global_brace_depth = 0
|
||
|
||
for line in ll_content.splitlines(True):
|
||
# 续行(函数体指令 / 全局初始化器)在合并 .ll 里占绝大多数,而这两支
|
||
# 只需要"行内容 + 花括号计数" ⇒ 不进 strip(前导/尾随空白不含花括号,
|
||
# _count_braces_outside_strings 的结果逐字不变)。prof 实测 str.strip
|
||
# 1.79 亿次为最大 Python 侧热点,逐行 strip 是其主要来源。
|
||
if in_global_def:
|
||
text_lines.append(line)
|
||
global_type_parts.append(line.strip())
|
||
# 预筛:无花括号的行必然贡献 0(与计数器返回值逐字等价,见其快路径)
|
||
if '{' in line or '}' in line:
|
||
global_brace_depth += _count_braces_outside_strings(line)
|
||
if global_brace_depth <= 0:
|
||
full_type = ' '.join(global_type_parts).strip()
|
||
depth = 0
|
||
type_end = len(full_type)
|
||
for ci, cc in enumerate(full_type):
|
||
if cc in ('{', '['):
|
||
depth += 1
|
||
elif cc in ('}', ']'):
|
||
depth -= 1
|
||
elif depth == 0 and cc == ' ':
|
||
type_end = ci
|
||
break
|
||
var_type = full_type[:type_end].strip()
|
||
if var_type:
|
||
stub_lines.append(f'{global_var_name} = external global {var_type}\n')
|
||
in_global_def = False
|
||
global_var_name = None
|
||
global_type_parts = []
|
||
global_brace_depth = 0
|
||
continue
|
||
|
||
if in_define:
|
||
text_lines.append(line)
|
||
# 预筛:函数体指令行绝大多数不含花括号,跳过即省掉一次函数调用
|
||
# (prof:本函数 102k 次调用;有花括号的行仍按原语义计数)
|
||
if '{' in line or '}' in line:
|
||
brace_depth += _count_braces_outside_strings(line)
|
||
if brace_depth <= 0:
|
||
in_define = False
|
||
continue
|
||
|
||
stripped = line.strip()
|
||
if stripped.startswith('define '):
|
||
text_lines.append(line)
|
||
in_define = True
|
||
brace_depth = _count_braces_outside_strings(stripped)
|
||
decl_line = _RE_DEFINE_PREFIX.sub('declare ', stripped)
|
||
decl_line = _RE_DEFINE_LINKAGE.sub('', decl_line)
|
||
decl_line = _RE_ALWAYSINLINE.sub('', decl_line)
|
||
paren_pos = decl_line.rfind(')')
|
||
if paren_pos >= 0:
|
||
decl_line = decl_line[:paren_pos + 1]
|
||
stub_lines.append(decl_line + '\n')
|
||
continue
|
||
|
||
if stripped == '{' and text_lines and not in_define:
|
||
prev_stripped = text_lines[-1].strip() if text_lines else ''
|
||
if prev_stripped.startswith('define ') or prev_stripped.endswith('noredzone') or prev_stripped.endswith(')'):
|
||
text_lines.append(line)
|
||
brace_depth = 1
|
||
in_define = True
|
||
if prev_stripped.startswith('define '):
|
||
# 与上面 define 分支同一组预编译正则(模式串一字未改),
|
||
# 避免 re 模块级的"模式串哈希→缓存查找"逐行开销
|
||
decl_line = _RE_DEFINE_PREFIX.sub('declare ', prev_stripped)
|
||
decl_line = _RE_DEFINE_LINKAGE.sub('', decl_line)
|
||
decl_line = _RE_ALWAYSINLINE.sub('', decl_line)
|
||
paren_pos = decl_line.rfind(')')
|
||
if paren_pos >= 0:
|
||
decl_line = decl_line[:paren_pos + 1]
|
||
stub_lines.append(decl_line + '\n')
|
||
continue
|
||
|
||
if stripped.startswith('@') and ' global ' in stripped and 'external global' not in stripped:
|
||
text_lines.append(line)
|
||
# LLVM 保留全局(如 --hotspot-sites 的 @llvm.global_ctors;名字含点故
|
||
# 最终 IR 保留引号)是 appending 定义,禁止改写为 "external global"
|
||
# 存根(否则 .ll 重组后 llc 报错)
|
||
if _RE_LLVM_INTRINSIC.match(stripped):
|
||
continue
|
||
if ' internal ' not in stripped and ' private ' not in stripped:
|
||
name_match = _RE_AT_NAME.match(stripped)
|
||
if name_match:
|
||
var_name = name_match.group(1)
|
||
global_pos = stripped.find(' global ')
|
||
after_global = stripped[global_pos + 8:]
|
||
depth = 0
|
||
type_end = len(after_global)
|
||
for ci, cc in enumerate(after_global):
|
||
if cc in ('{', '['):
|
||
depth += 1
|
||
elif cc in ('}', ']'):
|
||
depth -= 1
|
||
elif depth == 0 and cc == ' ':
|
||
type_end = ci
|
||
break
|
||
var_type = after_global[:type_end].strip()
|
||
brace_depth_gv = 0
|
||
for cc in var_type:
|
||
if cc == '{':
|
||
brace_depth_gv += 1
|
||
elif cc == '}':
|
||
brace_depth_gv -= 1
|
||
if brace_depth_gv > 0:
|
||
in_global_def = True
|
||
global_var_name = var_name
|
||
global_type_parts = [var_type]
|
||
global_brace_depth = brace_depth_gv
|
||
elif var_type:
|
||
stub_lines.append(f'{var_name} = external global {var_type}\n')
|
||
continue
|
||
if stripped.startswith('@') and ' constant ' in stripped:
|
||
text_lines.append(line)
|
||
continue
|
||
|
||
if stripped.startswith('declare'):
|
||
dm = _RE_DECLARE_NAME.match(stripped)
|
||
if dm:
|
||
fname = dm.group(1)
|
||
if fname in defined_names:
|
||
stub_lines.append(line)
|
||
continue
|
||
dot_pos = fname.find('.')
|
||
if dot_pos > 0:
|
||
fname_sha1 = fname[:dot_pos]
|
||
if module_sha1 and fname_sha1 != module_sha1 and len(fname_sha1) >= 16:
|
||
continue
|
||
if _RE_SHA1_16_DOT.match(fname):
|
||
continue
|
||
stub_lines.append(line)
|
||
continue
|
||
|
||
stub_lines.append(line)
|
||
|
||
return ''.join(stub_lines), ''.join(text_lines)
|
||
|
||
def _save_deps(self, sha1: str, ModuleSha1Map: dict[str, str]) -> None:
|
||
"""保存依赖指纹到 .deps.json"""
|
||
deps_path = os.path.join(self.output_dir, f"{sha1}.deps.json")
|
||
deps = {}
|
||
for mod_name, dep_sha1 in ModuleSha1Map.items():
|
||
deps[mod_name] = dep_sha1
|
||
with open(deps_path, 'w', encoding='utf-8') as f:
|
||
json.dump(deps, f)
|
||
_InvalidateTextCache(deps_path)
|
||
|
||
def _LoadDeps(self, sha1: str) -> dict[str, str] | None:
|
||
"""加载依赖指纹"""
|
||
deps_path = os.path.join(self.output_dir, f"{sha1}.deps.json")
|
||
if _CachedIsFile(deps_path):
|
||
try:
|
||
return json.loads(_ReadTextCached(deps_path))
|
||
except Exception as _e:
|
||
if _config_mode == "strict":
|
||
raise
|
||
_vlog().warning(f"加载依赖指纹 JSON 失败: {_e}", "Exception")
|
||
return None
|
||
|
||
def _check_deps_changed(self, sha1: str, current_ModuleSha1Map: dict[str, str]) -> bool:
|
||
"""检查依赖指纹是否变化"""
|
||
saved_deps = self._LoadDeps(sha1)
|
||
if saved_deps is None:
|
||
return True
|
||
for mod_name, dep_sha1 in current_ModuleSha1Map.items():
|
||
if saved_deps.get(mod_name) != dep_sha1:
|
||
return True
|
||
for mod_name, dep_sha1 in saved_deps.items():
|
||
if current_ModuleSha1Map.get(mod_name) != dep_sha1:
|
||
return True
|
||
return False
|
||
|
||
def _recombine_ll(self, sha1: str, current_ModuleSha1Map: dict[str, str]) -> bool:
|
||
"""依赖变化时重新组合 .ll:更新 .stub.ll 中的 SHA1 前缀,拼接 .stub.ll + .text.ll"""
|
||
saved_deps = self._LoadDeps(sha1)
|
||
if saved_deps is None:
|
||
return False
|
||
|
||
stub_path = os.path.join(self.output_dir, f"{sha1}.stub.ll")
|
||
text_path = os.path.join(self.output_dir, f"{sha1}.text.ll")
|
||
ll_path = os.path.join(self.output_dir, f"{sha1}.ll")
|
||
|
||
if not _CachedIsFile(stub_path) or not _CachedIsFile(text_path):
|
||
return False
|
||
|
||
stub_content = _ReadTextCached(stub_path)
|
||
text_content = _ReadTextCached(text_path)
|
||
|
||
sha1_replacements = {}
|
||
for mod_name, old_sha1 in saved_deps.items():
|
||
new_sha1 = current_ModuleSha1Map.get(mod_name)
|
||
if new_sha1 and old_sha1 != new_sha1:
|
||
sha1_replacements[old_sha1] = new_sha1
|
||
|
||
if sha1_replacements:
|
||
for old_sha1, new_sha1 in sha1_replacements.items():
|
||
stub_content = stub_content.replace(old_sha1, new_sha1)
|
||
text_content = text_content.replace(old_sha1, new_sha1)
|
||
|
||
combined = stub_content
|
||
if not combined.endswith('\n'):
|
||
combined += '\n'
|
||
combined += text_content
|
||
|
||
with open(ll_path, 'w', encoding='utf-8', newline='\n') as f:
|
||
f.write(combined)
|
||
with open(stub_path, 'w', encoding='utf-8', newline='\n') as f:
|
||
f.write(stub_content)
|
||
with open(text_path, 'w', encoding='utf-8', newline='\n') as f:
|
||
f.write(text_content)
|
||
_InvalidateTextCache(ll_path)
|
||
_InvalidateTextCache(stub_path)
|
||
_InvalidateTextCache(text_path)
|
||
|
||
self._save_deps(sha1, current_ModuleSha1Map)
|
||
|
||
obj_path = os.path.join(self.output_dir, f"{sha1}.obj")
|
||
if _CachedIsFile(obj_path):
|
||
os.remove(obj_path)
|
||
|
||
return True
|
||
|
||
def _inject_auto_imports(self, code: str, src_path: str) -> str:
|
||
"""检测源码中对 list/dict 容器的使用,自动注入 import _list / import _dict。
|
||
当使用 dict 时,同时注入 import json (用于 dict 的 JSON 处理功能)。
|
||
检测到 assert 语句时自动注入 import testcheck(assert 译为
|
||
testcheck.check(...),需该模块的跨模块符号声明)。
|
||
|
||
检测模式:
|
||
- list[T] / dict[V] 作为类型注解 (ast.Subscript, value=Name(id='list'/'dict'))
|
||
- list[T](...) / dict[V](...) 作为构造函数调用 (ast.Call, func=Subscript)
|
||
- list(...) / dict(...) 作为构造函数调用 (ast.Call, func=Name)
|
||
- assert 语句 (ast.Assert)
|
||
"""
|
||
try:
|
||
# `.vp` 源码须先过 Viper 软关键字层(`enum X:` / `fn` 等)再解析:
|
||
# 否则此处 SyntaxError 会静默 return 原 code(自动注入 import _list/_dict
|
||
# 失效),与主翻译路径(Shell/TransPyC 已展开)口径不一致。
|
||
tree = ast.parse(ExpandSoftKeywords(code), src_path)
|
||
except SyntaxError:
|
||
return code
|
||
|
||
# 检查是否已经导入了 _list / _dict / json / testcheck
|
||
has_list_import: bool = False
|
||
has_dict_import: bool = False
|
||
has_json_import: bool = False
|
||
has_testcheck_import: bool = False
|
||
for node in ast.iter_child_nodes(tree):
|
||
if isinstance(node, ast.Import):
|
||
for alias in node.names:
|
||
if alias.name == '_list':
|
||
has_list_import = True
|
||
elif alias.name == '_dict':
|
||
has_dict_import = True
|
||
elif alias.name == 'json':
|
||
has_json_import = True
|
||
elif alias.name == 'testcheck':
|
||
has_testcheck_import = True
|
||
elif isinstance(node, ast.ImportFrom):
|
||
if node.module == '_list':
|
||
has_list_import = True
|
||
elif node.module == '_dict':
|
||
has_dict_import = True
|
||
elif node.module == 'json':
|
||
has_json_import = True
|
||
elif node.module == 'testcheck':
|
||
has_testcheck_import = True
|
||
if has_list_import and has_dict_import and has_json_import and has_testcheck_import:
|
||
break
|
||
|
||
# 检查是否使用了 list / dict 容器,以及是否含 assert 语句
|
||
uses_list: bool = False
|
||
uses_dict: bool = False
|
||
uses_assert: bool = False
|
||
for node in ast.walk(tree):
|
||
if isinstance(node, ast.Assert):
|
||
uses_assert = True
|
||
if isinstance(node, ast.Subscript):
|
||
if isinstance(node.value, ast.Name):
|
||
if node.value.id == 'list':
|
||
uses_list = True
|
||
elif node.value.id == 'dict':
|
||
uses_dict = True
|
||
if isinstance(node, ast.Call) and isinstance(node.func, ast.Name):
|
||
if node.func.id == 'list':
|
||
uses_list = True
|
||
elif node.func.id == 'dict':
|
||
uses_dict = True
|
||
# 检测类型注解 d: dict / l: list (非泛型形式)
|
||
if isinstance(node, ast.AnnAssign) and isinstance(node.annotation, ast.Name):
|
||
if node.annotation.id == 'dict':
|
||
uses_dict = True
|
||
elif node.annotation.id == 'list':
|
||
uses_list = True
|
||
|
||
# 注入 import 到源码开头
|
||
prefix: str = ''
|
||
if uses_list and not has_list_import:
|
||
prefix += 'import _list\n'
|
||
if uses_dict and not has_dict_import:
|
||
prefix += 'import _dict\n'
|
||
if uses_dict and not has_json_import:
|
||
prefix += 'import json\n'
|
||
# 含 assert 的模块等价于在文件顶部写下 import testcheck(已导入则不重复)
|
||
if uses_assert and not has_testcheck_import:
|
||
prefix += 'import testcheck\n'
|
||
if prefix:
|
||
return prefix + code
|
||
return code
|
||
|
||
def _translate_file(self, src_path: str, out_path: str, prebuilt_ModuleSha1Map: dict[str, str] | None = None, dwarf_enabled: bool = False) -> None:
|
||
"""翻译单个源文件,嵌入相关 .stub.ll 声明
|
||
|
||
dwarf_enabled: 函数级 DWARF 调试元数据(--dwarf),默认 False(零影响)。
|
||
App 源模块在 --dwarf/--asan/--traceback 时传 True。include 文件默认
|
||
传 False(其 .obj 会落进跨项目共享的 includes.binary 预编译缓存,注入 DI
|
||
会污染该缓存);仅 --traceback 时传 True——此时 include 的 .obj 改落
|
||
隔离目录(includes.binary_traceback,见 _get_precompiled_obj_path),
|
||
不会污染共享缓存。
|
||
"""
|
||
_t0: float = time.time()
|
||
# 每个文件翻译入口置脏:本文件/其它文件在此之间新写的 *.stub.ll 全部可见
|
||
# (ImportHandle._build_stub_index 的零 syscall 快路径据此判断是否重扫目录;
|
||
# 顺序模式与 worker 模式的每文件粒度皆覆盖,见 MarkStubSetDirty)。
|
||
from lib.core.Handles.HandlesImports import ImportHandle as _IH
|
||
_IH.MarkStubSetDirty()
|
||
code = _ReadTextCached(src_path)
|
||
|
||
# SHA1 以主流程 out_path 文件名为准(= 主流程 compute_sha1(parse_python_file 内容)),
|
||
# 必须在自动导入注入之前确定。不可在此重读文件重算:主流程收集(need_translate)
|
||
# 与本次实际翻译之间若文件内容变化(如编辑器保存),重算会得到不同 sha1,
|
||
# 导致函数定义前缀 ≠ 文件名/调用方引用前缀 → 链接 undefined symbol
|
||
# (实例:696d41ca86e0a29a.BuildTopoSortedIndices 定义被生成成 5536… 前缀)。
|
||
sha1 = os.path.splitext(os.path.basename(out_path))[0]
|
||
cur_sha1 = compute_sha1(code)
|
||
if cur_sha1 != sha1:
|
||
_vlog().warning(
|
||
f"模块内容在收集后已变化(文件名sha1={sha1}, 当前sha1={cur_sha1}): {src_path},"
|
||
f"仍按主流程 sha1 生成前缀以保证链接一致;下次构建将采用新 sha1", "SHA1")
|
||
|
||
# 自动导入: 检测 list 容器使用,自动注入 import _list
|
||
# 注入行会让 AST 节点 lineno 整体下移 N(磁盘真实行号 = AST 行号 - N);
|
||
# 记下 N 供报错渲染还原磁盘行号(如 _RaiseUnresolvedExpr)。
|
||
_raw_newlines: int = code.count('\n')
|
||
code = self._inject_auto_imports(code, src_path)
|
||
_line_offset: int = code.count('\n') - _raw_newlines
|
||
|
||
if prebuilt_ModuleSha1Map is not None:
|
||
ModuleSha1Map = prebuilt_ModuleSha1Map
|
||
else:
|
||
ModuleSha1Map = self._build_current_ModuleSha1Map(sha1)
|
||
|
||
trans = TransPyC.TransPyC(code=code, triple=self.triple, datalayout=self.datalayout)
|
||
trans.translator.CurrentFile = src_path
|
||
# 函数级 DWARF(--dwarf):透传给 Translator,由其传给每个新 LlvmCodeGenerator;
|
||
# 未启用时不设置 → 与改动前逐字节一致(零影响)
|
||
if dwarf_enabled:
|
||
trans.translator.DwarfEnabled = True
|
||
# Ctrl+C 调用栈(--traceback):仅当本文件就是 traceback 模块时置位——
|
||
# 只有该模块需要注入 @llvm.global_ctors 构造器调用自己的 Install()
|
||
# (见 BaseGen._RegisterTracebackInstallViaCtor)。其它模块不设置该属性
|
||
# → Gen.traceback_ctor 保持 False,IR 与改动前逐字节一致(零影响)。
|
||
_traceback_ctor: bool = bool(
|
||
self.traceback_enabled
|
||
and os.path.basename(src_path) in _TRACEBACK_MODULE_FILES)
|
||
if _traceback_ctor:
|
||
trans.translator.TracebackCtor = True
|
||
# --traceback:记录 traceback.Install 的跨模块符号名,含 main 的模块在入口
|
||
# 直接调用它(不依赖 .ctors/.init_array,避免 MinGW 下 .ctors.65534 不执行)。
|
||
if self.traceback_enabled:
|
||
_tb_sha1: str | None = None
|
||
_temp_vpi: str = getattr(self, 'temp_dir', '') or ''
|
||
if _temp_vpi and os.path.isdir(_temp_vpi):
|
||
for _fn in os.listdir(_temp_vpi):
|
||
if not _fn.endswith('.vpi'):
|
||
continue
|
||
try:
|
||
if 'Module: traceback' in _ReadTextCached(os.path.join(_temp_vpi, _fn))[:512]:
|
||
_tb_sha1 = _fn[:-4]
|
||
break
|
||
except Exception:
|
||
continue
|
||
if _tb_sha1 is None:
|
||
for _k, _p in self.sig_files.items():
|
||
try:
|
||
if 'traceback' in os.path.basename(_p):
|
||
_tb_sha1 = _k
|
||
break
|
||
if 'Module: traceback' in _ReadTextCached(_p)[:512]:
|
||
_tb_sha1 = _k
|
||
break
|
||
except Exception:
|
||
continue
|
||
trans.translator.TracebackEnabled = self.traceback_enabled
|
||
if _tb_sha1 is not None:
|
||
trans.translator.TracebackInstallSymbol = f'{_tb_sha1}.Install'
|
||
sys.stderr.write(
|
||
f"[traceback] file={os.path.basename(src_path)} "
|
||
f"enabled={self.traceback_enabled} "
|
||
f"InstallSymbol={getattr(trans.translator, 'TracebackInstallSymbol', None)}\n")
|
||
sys.stderr.flush()
|
||
trans.SliceLevel = self.slice_level
|
||
trans.translator.SliceLevel = self.slice_level
|
||
trans.translator.SliceCount = 0
|
||
trans.translator.SliceInfos = []
|
||
trans.translator.LlvmGen = None
|
||
trans.translator._module_sha1 = sha1
|
||
trans.translator._line_offset = _line_offset
|
||
trans.translator._ModuleSha1Map = ModuleSha1Map
|
||
trans.translator._struct_sha1_map = self.struct_sha1_map
|
||
trans.translator._global_function_default_args = self.function_default_args
|
||
trans.translator._global_function_param_names = self.function_param_names
|
||
trans.translator._temp_dir = self.temp_dir
|
||
# includes 只认 project.vpj 配置的 include 目录(不再回退仓库 TransPyCRoot/includes):
|
||
# 目录重组/缺失时避免声明解析从仓库或 sig 回退路径产生错误时序(memhub 未注册→i32)
|
||
_proj_inc = [os.path.abspath(d) for d in self.include_dirs if os.path.isdir(d)]
|
||
trans.translator.LibraryPaths = _proj_inc if _proj_inc else ['.']
|
||
trans.translator._shared_doc_meta = self._shared_doc_meta
|
||
# 传递共享 vtable 状态
|
||
trans.translator._shared_class_vtable = self._shared_class_vtable
|
||
trans.translator._shared_cross_module_vtable = self._shared_cross_module_vtable
|
||
trans.translator._shared_class_methods = self._shared_class_methods
|
||
trans.translator._shared_cross_module_novtable = self._shared_cross_module_novtable
|
||
|
||
# 调用点热点统计与每函数计时(--hotspot-sites,Projectrans 多模块):
|
||
# 各模块处理器统一以追加方式写最终 <path>,注册策略为本模块
|
||
# @llvm.global_ctors 自注册(见 BaseGen._InstallHotspotSitesModuleLocal);
|
||
# self.hotspot_sites_file 为 None 时不设置任何属性 → 与未启用时逐字节一致。
|
||
if self.hotspot_sites_file:
|
||
trans.translator.HotspotSitesFile = self.hotspot_sites_file
|
||
trans.translator.HotspotSitesAll = self.hotspot_sites_all
|
||
trans.translator.HotspotCallees = self.hotspot_callees
|
||
trans.translator.HotspotSitesModuleLocal = True
|
||
|
||
if self._shared_symbol_table is not None:
|
||
trans.translator.SymbolTable = self._shared_symbol_table
|
||
# 确保共享符号表的 translator 指向当前 translator,以便 import 别名解析正确
|
||
if hasattr(self._shared_symbol_table, 'translator'):
|
||
self._shared_symbol_table.translator = trans.translator
|
||
trans.translator._source_module_sig_files = self._shared_source_module_sig_files
|
||
trans.translator._all_define_constants = self._shared_all_dc
|
||
trans.translator._define_decl_types = self._shared_all_decl_types
|
||
# 共享集合可能因 sig_files 过滤而缺少 include 模块的 extern 声明;
|
||
# 这里从本 translator 的 sig_files 合并补全(跨模块 extern 必须裸名)。
|
||
_combined_extern: dict[str, str] = dict(self._shared_export_extern_funcs)
|
||
_sig_paths: list[tuple[str, str]] = list(self.sig_files.items())
|
||
_temp_vpi_dir: str = getattr(self, 'temp_dir', '') or ''
|
||
if _temp_vpi_dir and os.path.isdir(_temp_vpi_dir):
|
||
for _fn in os.listdir(_temp_vpi_dir):
|
||
if _fn.endswith('.vpi'):
|
||
_sig_paths.append((_fn[:-4], os.path.join(_temp_vpi_dir, _fn)))
|
||
for _sha1_key, _sig_path in _sig_paths:
|
||
try:
|
||
_, _sig_tree = parse_python_file(_sig_path)
|
||
if _sig_tree is None:
|
||
continue
|
||
for _node in ast.iter_child_nodes(_sig_tree):
|
||
if isinstance(_node, ast.FunctionDef) and _IsExportExternFuncNode(_node):
|
||
_combined_extern[_node.name] = _sha1_key
|
||
except Exception:
|
||
continue
|
||
_extern_dict = getattr(trans.translator, '_export_extern_funcs', None)
|
||
if _extern_dict is None:
|
||
_extern_dict = {}
|
||
trans.translator._export_extern_funcs = _extern_dict
|
||
_extern_dict.clear()
|
||
_extern_dict.update(_combined_extern)
|
||
_llvm_gen = getattr(trans.translator, 'LlvmGen', None)
|
||
if _llvm_gen is not None:
|
||
_llvm_gen._export_extern_funcs = _extern_dict
|
||
if self._shared_generic_class_templates:
|
||
if not hasattr(trans.translator.ClassHandler, '_generic_class_templates'):
|
||
trans.translator.ClassHandler._generic_class_templates = {}
|
||
trans.translator.ClassHandler._generic_class_templates.update(self._shared_generic_class_templates)
|
||
else:
|
||
export_extern_funcs = {}
|
||
for sha1_key, sig_path in self.sig_files.items():
|
||
if sha1_key == sha1:
|
||
continue
|
||
try:
|
||
sig_content = _ReadTextCached(sig_path)
|
||
sig_tree = ast.parse(sig_content, sig_path)
|
||
for node in ast.iter_child_nodes(sig_tree):
|
||
if isinstance(node, ast.FunctionDef):
|
||
if _IsExportExternFuncNode(node):
|
||
export_extern_funcs[node.name] = sha1_key
|
||
except Exception as _e:
|
||
if _config_mode == "strict":
|
||
raise
|
||
_vlog().warning(f"收集导出外部函数声明失败: {_e}", "Exception")
|
||
if export_extern_funcs:
|
||
_extern_dict2 = getattr(trans.translator, '_export_extern_funcs', None)
|
||
if _extern_dict2 is None:
|
||
trans.translator._export_extern_funcs = dict(export_extern_funcs)
|
||
_extern_dict2 = trans.translator._export_extern_funcs
|
||
else:
|
||
_extern_dict2.update(export_extern_funcs)
|
||
_llvm_gen2 = getattr(trans.translator, 'LlvmGen', None)
|
||
if _llvm_gen2 is not None:
|
||
_llvm_gen2._export_extern_funcs = _extern_dict2
|
||
|
||
for includes_dir in self.include_dirs:
|
||
if os.path.isdir(includes_dir):
|
||
for pyi_file in os.listdir(includes_dir):
|
||
if pyi_file.endswith('.vpi') or pyi_file.endswith('.pyi'):
|
||
pyi_path = os.path.join(includes_dir, pyi_file)
|
||
module_name = os.path.splitext(pyi_file)[0]
|
||
trans.translator.SymbolTable.LoadModuleSymbols(pyi_path, module_name, lineno=0)
|
||
for py_file in os.listdir(includes_dir):
|
||
if (py_file.endswith('.vp') or py_file.endswith('.py')) and (not py_file.startswith('_') or py_file == '_list.vp' or py_file == '_list.py' or py_file == '_dict.vp' or py_file == '_dict.py'):
|
||
module_name = os.path.splitext(py_file)[0]
|
||
sha1_key = self.include_py_map.get(module_name)
|
||
if sha1_key and sha1_key in self.sig_files:
|
||
sig_path = self.sig_files[sha1_key]
|
||
trans.translator.SymbolTable.LoadModuleSymbols(sig_path, module_name, lineno=0)
|
||
# 收集需要重导出的包,等所有模块符号加载完后再处理
|
||
_pending_reexports = []
|
||
for sha1_key, sig_path in self.sig_files.items():
|
||
rel_path = self.sha1_map.get(sha1_key, sha1_key)
|
||
if rel_path.startswith('includes/'):
|
||
mod_name = os.path.splitext(rel_path)[0].replace(os.sep, '.').replace('/', '.')
|
||
mod_name = mod_name[len('includes/'):]
|
||
trans.translator.SymbolTable.LoadModuleSymbols(sig_path, mod_name, lineno=0)
|
||
if mod_name.endswith('.__init__'):
|
||
package_name = mod_name[:-len('.__init__')]
|
||
_pending_reexports.append((sig_path, package_name))
|
||
|
||
# 所有 includes 模块符号已加载,现在处理包重导出
|
||
for sig_path, package_name in _pending_reexports:
|
||
self._register_package_reexports(trans, sig_path, package_name)
|
||
|
||
for sha1_key, sig_path in self.sig_files.items():
|
||
if sha1_key == sha1:
|
||
continue
|
||
rel_path = self.sha1_map.get(sha1_key, sha1_key)
|
||
if rel_path.startswith('includes/'):
|
||
continue
|
||
module_name = os.path.splitext(rel_path)[0].replace(os.sep, '.').replace('/', '.')
|
||
trans.translator.SymbolTable.LoadModuleSymbols(sig_path, module_name, lineno=0)
|
||
trans.translator._source_module_sig_files[module_name] = sig_path
|
||
short_name = module_name.split('.')[-1] if '.' in module_name else module_name
|
||
if short_name != module_name:
|
||
trans.translator._source_module_sig_files[short_name] = sig_path
|
||
|
||
all_dc = {}
|
||
all_dt: dict[str, tuple[int, bool]] = {}
|
||
for sha1_key, stub_path in self.stub_files.items():
|
||
if sha1_key == sha1:
|
||
continue
|
||
if os.path.exists(stub_path):
|
||
try:
|
||
stub_content = _ReadTextCached(stub_path)
|
||
for line in stub_content.splitlines():
|
||
line = line.strip()
|
||
if line.startswith('@') and '= global' in line and 'i32' in line and not line.startswith('@llvm'):
|
||
var_name = line.split('=')[0].strip().lstrip('@')
|
||
val_part = line.split('i32')[-1].strip().rstrip(']').lstrip('[').lstrip('i32')
|
||
try:
|
||
val = int(val_part)
|
||
all_dc[var_name] = val
|
||
except Exception as e:
|
||
_vlog().warning(f"Phase2Translator: 忽略异常 {e}", exc_info=e)
|
||
except Exception as e:
|
||
_vlog().warning(f"Phase2Translator: 忽略异常 {e}", exc_info=e)
|
||
for sha1_key, pyi_path in self.sig_files.items():
|
||
if sha1_key == sha1:
|
||
continue
|
||
_ExtractCDefineConstantsFromPyi(pyi_path, all_dc, trans.translator.SymbolTable, all_dt)
|
||
if all_dc:
|
||
trans.translator._all_define_constants = all_dc
|
||
if all_dt:
|
||
trans.translator._define_decl_types = all_dt
|
||
|
||
for sym_name, sym_info in self.inline_func_symbols.items():
|
||
if sym_name not in trans.translator.SymbolTable:
|
||
trans.translator.SymbolTable[sym_name] = sym_info
|
||
else:
|
||
existing = trans.translator.SymbolTable[sym_name]
|
||
if not getattr(existing, 'InlineBody', None) and getattr(sym_info, 'InlineBody', None):
|
||
existing.IsInline = sym_info.IsInline
|
||
existing.InlineBody = sym_info.InlineBody
|
||
existing.InlineParams = sym_info.InlineParams
|
||
|
||
try:
|
||
result = trans.Convert(
|
||
OutputFilename=out_path,
|
||
SourceFilename=src_path,
|
||
target='llvm'
|
||
)
|
||
except Exception as e:
|
||
error_stack = getattr(trans.translator, '_ErrorStack', [])
|
||
if error_stack:
|
||
self._last_ErrorStack = error_stack
|
||
# 语句栈渲染:error_stack 按"由内向外"入栈(内层语句先 append),
|
||
# 这里反转为"外层 → 内层"的调用路径;**异常消息只在最内层印一次**,
|
||
# 其余帧只印位置——否则同一段长 fail-fast 报文会被每个帧重复一遍
|
||
# (旧格式把消息贴在每一帧后面,输出里同一句话出现 N 次,不可读)。
|
||
entries: list = [entry for entry in error_stack if len(entry) >= 3]
|
||
chain_lines: list[str] = []
|
||
for chain_idx, entry in enumerate(reversed(entries)):
|
||
exc_msg, line_info = entry[1], entry[2]
|
||
if chain_idx == len(entries) - 1:
|
||
chain_lines.append(" %s\n %s" % (line_info, exc_msg))
|
||
else:
|
||
chain_lines.append(" %s" % (line_info,))
|
||
if chain_lines:
|
||
enriched = str(e) + "\n" + "\n".join(chain_lines)
|
||
raise type(e)(enriched) from e
|
||
node_info = ""
|
||
if hasattr(trans.translator, 'LlvmGen') and trans.translator.LlvmGen:
|
||
node_info = trans.translator.LlvmGen._get_node_info()
|
||
if node_info:
|
||
enriched = "%s\n %s" % (str(e), node_info.strip())
|
||
raise type(e)(enriched) from e
|
||
raise
|
||
|
||
if result and isinstance(result, str):
|
||
for func_name, func_def in trans.translator.FunctionDefCache.items():
|
||
if hasattr(func_def, 'args'):
|
||
mangled = trans.translator.LlvmGen._mangle_func_name(func_name) if trans.translator.LlvmGen else func_name
|
||
# 提取参数名
|
||
if hasattr(func_def.args, 'args'):
|
||
param_names = [arg.arg for arg in func_def.args.args]
|
||
self.function_param_names[mangled] = param_names
|
||
self.function_param_names[func_name] = param_names
|
||
# 提取默认值
|
||
if hasattr(func_def.args, 'defaults') and func_def.args.defaults:
|
||
defaults = []
|
||
for d in func_def.args.defaults:
|
||
if isinstance(d, ast.Constant):
|
||
defaults.append(d.value)
|
||
else:
|
||
defaults.append(None)
|
||
self.function_default_args[mangled] = defaults
|
||
self.function_default_args[func_name] = defaults
|
||
for sym_name, sym_info in trans.translator.SymbolTable.items():
|
||
if isinstance(sym_info, CTypeInfo) and getattr(sym_info, 'IsInline', False) and getattr(sym_info, 'InlineBody', None):
|
||
self.inline_func_symbols[sym_name] = sym_info
|
||
|
||
out_dir = os.path.dirname(out_path)
|
||
if out_dir:
|
||
os.makedirs(out_dir, exist_ok=True)
|
||
# --traceback:核对注入钩子确实生效——本模块要求注入 @llvm.global_ctors
|
||
# 构造器(StructGen.finalize 里落成 .ll 文本之前),若产物中没有该全局
|
||
# 说明钩子不可用(如序列化路径被改动绕过),显式报错终止,不静默产出
|
||
# 缺注册的产物(那会让整个功能静默失效)。
|
||
if _traceback_ctor and 'llvm.global_ctors' not in result:
|
||
raise RuntimeError(
|
||
'--traceback: traceback 模块未注入 @llvm.global_ctors '
|
||
'(模块级收尾钩子未生效,不静默降级)')
|
||
# 修复 LLVM 22+: external global 不能有初值,改为 linkonce
|
||
result = _RE_EXTERNAL_GLOBAL_VALUE.sub(r'\1 = linkonce global \2 \3', result)
|
||
# 后处理:添加 llvmlite 不支持的 LLVM 属性(如 willreturn, mustprogress)
|
||
pending_str_attrs = getattr(trans.translator.LlvmGen, '_pending_str_attrs', None)
|
||
if pending_str_attrs:
|
||
for func_name, attrs in pending_str_attrs.items():
|
||
attrs_str = ' '.join(attrs)
|
||
# 匹配 define ... @func_name(...) ... { 并在 { 之前添加属性
|
||
pattern = rf'(define\s+[^@]*@{re.escape(func_name)}\s*\([^)]*\))([^{{]*)(\{{)'
|
||
result, n = re.subn(pattern, rf'\1\2 {attrs_str}\3', result, count=1)
|
||
if n == 0:
|
||
_vlog().warning(f"后处理添加属性失败: 未找到函数 {func_name} 的定义")
|
||
with open(out_path, 'w', encoding='utf-8', newline='\n') as f:
|
||
f.write(result)
|
||
_InvalidateTextCache(out_path)
|
||
|
||
stub_content, text_content = self._split_ll(result)
|
||
base = os.path.splitext(out_path)[0]
|
||
with open(base + '.stub.ll', 'w', encoding='utf-8', newline='\n') as f:
|
||
f.write(stub_content)
|
||
_InvalidateTextCache(base + '.stub.ll')
|
||
# 新 stub 已落盘 → 置脏,使本文件后续(及下个文件)的索引 miss 能重扫到它
|
||
from lib.core.Handles.HandlesImports import ImportHandle as _IH2
|
||
_IH2.MarkStubSetDirty()
|
||
with open(base + '.text.ll', 'w', encoding='utf-8', newline='\n') as f:
|
||
f.write(text_content)
|
||
_InvalidateTextCache(base + '.text.ll')
|
||
self._save_deps(sha1, ModuleSha1Map)
|
||
|
||
# 进度日志由调用方统一打印(主进程按文件粒度 [x/total]),此处不再重复输出。
|
||
else:
|
||
stub_path = self.stub_files.get(sha1)
|
||
if stub_path and os.path.exists(stub_path):
|
||
out_dir = os.path.dirname(out_path)
|
||
if out_dir:
|
||
os.makedirs(out_dir, exist_ok=True)
|
||
stub_content = _ReadTextCached(stub_path)
|
||
with open(out_path, 'w', encoding='utf-8', newline='\n') as f:
|
||
f.write(stub_content)
|
||
# 进度日志由调用方统一打印。
|
||
|
||
def _register_package_reexports(self, trans, init_pyi_path: str, package_name: str) -> None:
|
||
"""解析 __init__.pyi 中的 from .xxx import yyy 或 from __xxx import yyy,在包级别注册导出符号"""
|
||
try:
|
||
# 复用 parse_python_file 的 AST 缓存
|
||
_, tree = parse_python_file(init_pyi_path)
|
||
if tree is None:
|
||
return
|
||
for node in ast.walk(tree):
|
||
if isinstance(node, ast.ImportFrom) and node.module:
|
||
sub_module = node.module.lstrip('.')
|
||
for alias in node.names:
|
||
symbol_name = alias.name
|
||
exported_name = alias.asname if alias.asname else symbol_name
|
||
source_keys = [
|
||
f"{package_name}.{sub_module}.{symbol_name}",
|
||
f"{package_name}.__{sub_module}.{symbol_name}" if not sub_module.startswith('__') else None,
|
||
symbol_name,
|
||
]
|
||
target_key = f"{package_name}.{exported_name}"
|
||
if target_key not in trans.translator.SymbolTable:
|
||
for source_key in source_keys:
|
||
if source_key and source_key in trans.translator.SymbolTable:
|
||
trans.translator.SymbolTable[target_key] = trans.translator.SymbolTable[source_key]
|
||
break
|
||
if exported_name not in trans.translator.SymbolTable:
|
||
for source_key in source_keys:
|
||
if source_key and source_key in trans.translator.SymbolTable:
|
||
trans.translator.SymbolTable[exported_name] = trans.translator.SymbolTable[source_key]
|
||
break
|
||
except Exception as e:
|
||
_vlog().warning(f" [警告] 解析包重导出失败 {init_pyi_path}: {e}")
|
||
|
||
def _SanitizeIRFile(self, ll_path: str) -> str:
|
||
"""送入 opt/llc 之前,对该 .ll 做一次元数据规整(见 IRMetadataFix)。
|
||
|
||
App 模块的 IR 是「本模块 IR + 各依赖 .stub.ll 片段」直接拼接而成,而
|
||
--dwarf(--asan 自动开启)给每份片段都加了从 !0 开始编号的 DI 元数据,
|
||
合并文件里同一个 !N 被定义多次 → opt/llc 报 "Metadata id is already
|
||
used"、命名元数据 "defined more than once"。规整后编号全局唯一、
|
||
命名元数据只留一份。无元数据 / 已规整的文件逐字节不变(no-op),
|
||
原地覆盖没有额外产物。
|
||
"""
|
||
with open(ll_path, 'r', encoding='utf-8', newline='') as f:
|
||
text = f.read()
|
||
fixed = SanitizeIRMetadata(text)
|
||
if fixed != text:
|
||
with open(ll_path, 'w', encoding='utf-8', newline='') as f:
|
||
f.write(fixed)
|
||
_InvalidateTextCache(ll_path)
|
||
return ll_path
|
||
|
||
def _llc_compile(self, ll_path: str, obj_path: str,
|
||
cwd: str | None = None) -> subprocess.CompletedProcess:
|
||
"""编译单个 .ll 为 obj。
|
||
|
||
ASan 模式(--asan)下先对 IR 执行 opt 插桩:
|
||
检测 stdlib 堆/栈/全局越界写与垃圾指针解引用(MemBuddy pool 内部
|
||
由块尾 canary 覆盖,两者互补)。opt 失败返回其结果(returncode!=0)。
|
||
|
||
LLVM >= 20 起 opt -passes=asan 只对带 sanitize_address 函数属性的
|
||
函数插桩(clang -fsanitize=address 自动加;手写 IR 没有)——
|
||
先 forceattrs 批量加属性再 asan,否则只链接 runtime 不插桩。
|
||
"""
|
||
ll_path = self._SanitizeIRFile(ll_path)
|
||
compile_input = ll_path
|
||
if self.asan_mode:
|
||
asan_ll = ll_path[:-3] + '_asan.ll'
|
||
# -asan-globals=false:TPC 对全局变量(如各模块 _g_gen)用直接地址寻址,
|
||
# ASan 全局 redzone 重定位后引用错位会导致读到垃圾指针(如
|
||
# register_struct 的 self=0xbebebebe...)。先关闭全局插桩排除该因素。
|
||
opt_cmd = ['opt', '-passes=forceattrs,asan', '-force-attribute=sanitize_address',
|
||
'-asan-globals=false', ll_path, '-o', asan_ll]
|
||
opt_r = subprocess.run(opt_cmd, capture_output=True, text=True, cwd=cwd)
|
||
if opt_r.returncode != 0:
|
||
_vlog().error(f" opt ASan 插桩失败: {opt_r.stderr.strip()}")
|
||
return opt_r
|
||
compile_input = asan_ll
|
||
cmd = [self.compile_cmd] + self.compile_flags + ['-o', obj_path, compile_input]
|
||
if self.traceback_enabled:
|
||
# --traceback:让 llc 保留帧指针(-frame-pointer=all)。dbghelp 主路径
|
||
# 不依赖帧指针,但符号表不可用时要靠 RBP 链兜底——llc 默认
|
||
# omit-frame-pointer 会让 RBP 链只走出 1~2 帧(实测"共 2 帧")。
|
||
# 只在开启该开关时加,关闭时命令与改前逐字一致。
|
||
cmd = [self.compile_cmd] + self.compile_flags \
|
||
+ ['-frame-pointer=all', '-o', obj_path, compile_input]
|
||
return subprocess.run(cmd, capture_output=True, text=True, cwd=cwd)
|
||
|
||
def _compile_ll_files(self, active_sha1s: set[str]) -> None:
|
||
"""将 output_dir 中属于 active_sha1s 的 .ll 文件编译为 .obj(跳过已编译过的 include 文件)"""
|
||
# 第一次 walk:清理 stale 文件(.obj/.deps.json/纯 .ll,排除 .stub.ll/.text.ll)
|
||
stale_files = []
|
||
for root, dirs, files in os.walk(self.output_dir):
|
||
for file in files:
|
||
if file.endswith('.obj'):
|
||
sha1 = file[:-4]
|
||
if sha1 not in active_sha1s:
|
||
stale_files.append(os.path.join(root, file))
|
||
elif file.endswith('.deps.json'):
|
||
sha1 = file[:-10]
|
||
if sha1 not in active_sha1s:
|
||
stale_files.append(os.path.join(root, file))
|
||
elif file.endswith('.ll') and not file.endswith('.stub.ll') and not file.endswith('.text.ll'):
|
||
sha1 = file[:-3]
|
||
if sha1 not in active_sha1s:
|
||
stale_files.append(os.path.join(root, file))
|
||
if stale_files:
|
||
for fpath in stale_files:
|
||
try:
|
||
os.remove(fpath)
|
||
# 删除也是"本进程改变了该路径":不失效会让后续读拿到已删文件的旧文本
|
||
_InvalidateTextCache(fpath)
|
||
except Exception as e:
|
||
_vlog().warning(f"Phase2Translator: 忽略异常 {e}", exc_info=e)
|
||
_vlog().info(f"[清理] 删除 {len(stale_files)} 个过时文件")
|
||
|
||
# 第二次 walk:收集需要编译的 .ll 文件(排除 .stub.ll/.text.ll)
|
||
ll_files = []
|
||
for root, dirs, files in os.walk(self.output_dir):
|
||
for file in files:
|
||
if file.endswith('.ll') and not file.endswith('.stub.ll') and not file.endswith('.text.ll'):
|
||
sha1 = file[:-3]
|
||
if sha1 in active_sha1s:
|
||
fpath = os.path.join(root, file)
|
||
obj_path = os.path.join(root, file[:-3] + '.obj')
|
||
if not _CachedIsFile(obj_path):
|
||
ll_files.append(fpath)
|
||
if not ll_files:
|
||
_vlog().info("[编译] 无 .ll 文件需要重新编译")
|
||
else:
|
||
_vlog().info(f"\n[编译] 找到 {len(ll_files)} 个 .ll 文件需要编译")
|
||
|
||
# 第三次 walk:收集 all_ll_files(用于 alwaysinline 检测,排除 include sha1s)
|
||
all_ll_files = []
|
||
for root, dirs, files in os.walk(self.output_dir):
|
||
for file in files:
|
||
if file.endswith('.ll') and not file.endswith('.stub.ll') and not file.endswith('.text.ll'):
|
||
sha1 = file[:-3]
|
||
if sha1 in active_sha1s and sha1 not in self._include_sha1s:
|
||
all_ll_files.append(os.path.join(root, file))
|
||
|
||
has_inline = False
|
||
for ll_path in all_ll_files:
|
||
try:
|
||
content = _ReadTextCached(ll_path)
|
||
if 'alwaysinline' in content:
|
||
has_inline = True
|
||
break
|
||
except Exception as e:
|
||
_vlog().warning(f"Phase2Translator: 忽略异常 {e}", exc_info=e)
|
||
|
||
if has_inline:
|
||
llvm_link = shutil.which('llvm-link') or shutil.which('llvm-link.exe')
|
||
opt_cmd = shutil.which('opt') or shutil.which('opt.exe')
|
||
if llvm_link and opt_cmd:
|
||
_vlog().info(" [内联] 检测到 alwaysinline 函数,执行跨模块内联优化...")
|
||
for ll_path in all_ll_files:
|
||
try:
|
||
content = _ReadTextCached(ll_path)
|
||
type_defs = []
|
||
type_def_indices = set()
|
||
for i, line in enumerate(content.splitlines()):
|
||
stripped = line.strip()
|
||
if stripped.startswith('%') and '= type' in stripped:
|
||
type_defs.append(line)
|
||
type_def_indices.add(i)
|
||
if type_def_indices:
|
||
lines = content.splitlines(True)
|
||
content_no_types = ''.join(line for i, line in enumerate(lines) if i not in type_def_indices)
|
||
header_end = 0
|
||
for i, line in enumerate(content_no_types.splitlines()):
|
||
stripped = line.strip()
|
||
if stripped.startswith(';') or stripped.startswith('target ') or stripped.startswith('source_filename') or not stripped:
|
||
header_end = i + 1
|
||
continue
|
||
break
|
||
lines2 = content_no_types.splitlines(True)
|
||
fixed = ''.join(lines2[:header_end]) + '\n'.join(type_defs) + '\n' + ''.join(lines2[header_end:])
|
||
with open(ll_path, 'w', encoding='utf-8') as f:
|
||
f.write(fixed)
|
||
_InvalidateTextCache(ll_path)
|
||
except Exception as e:
|
||
_vlog().warning(f"Phase2Translator: 忽略异常 {e}", exc_info=e)
|
||
merged_ll = os.path.join(self.output_dir, '_merged.ll')
|
||
optimized_ll = os.path.join(self.output_dir, '_merged_opt.ll')
|
||
try:
|
||
link_cmd = [llvm_link] + all_ll_files + ['-o', merged_ll]
|
||
result = subprocess.run(link_cmd, capture_output=True, text=True)
|
||
if result.returncode != 0:
|
||
_vlog().warning(f" llvm-link 失败: {result.stderr.strip()},回退到单独编译")
|
||
has_inline = False
|
||
else:
|
||
opt_result_cmd = [opt_cmd, '-passes=always-inline', merged_ll, '-S', '-o', optimized_ll]
|
||
result = subprocess.run(opt_result_cmd, capture_output=True, text=True)
|
||
if result.returncode != 0:
|
||
_vlog().warning(f" opt 失败: {result.stderr.strip()},回退到单独编译")
|
||
has_inline = False
|
||
else:
|
||
_vlog().info(" [内联] 跨模块内联优化完成")
|
||
opt_content = _ReadTextCached(optimized_ll)
|
||
opt_content = _RE_DEFINE_DECLARE_LINKAGE.sub(r'\1 \2 dso_local ', opt_content)
|
||
opt_content = _RE_DEFINE_DECLARE_NO_LINKAGE_AT.sub(r'\1 dso_local ', opt_content)
|
||
# LLVM 22+: external 不允许在带初值的全局定义上显式写出,改为 linkonce 保留多模块合并语义
|
||
opt_content = _RE_EXTERNAL_GLOBAL_VALUE_PLAIN.sub(r'\1 = linkonce global \2 \3', opt_content)
|
||
opt_content = _RE_GLOBAL_LINKAGE.sub(r'\1 = \2 dso_local \3', opt_content)
|
||
opt_content = _RE_GLOBAL_DSOLOCAL.sub(r'\1 = dso_local \2', opt_content)
|
||
opt_content = _AddWeakComdat(opt_content)
|
||
with open(optimized_ll, 'w', encoding='utf-8') as f:
|
||
f.write(opt_content)
|
||
merged_obj = os.path.join(self.output_dir, '_merged.obj')
|
||
try:
|
||
result = self._llc_compile(optimized_ll, merged_obj, self.output_dir)
|
||
if result.returncode == 0:
|
||
_vlog().success(f" 合并模块编译完成")
|
||
for ll_path in ll_files:
|
||
obj_path = ll_path[:-3] + '.obj'
|
||
sha1_obj = os.path.join(self.output_dir, os.path.basename(ll_path)[:-3] + '.obj')
|
||
if _CachedIsFile(sha1_obj):
|
||
os.remove(sha1_obj)
|
||
return
|
||
else:
|
||
_vlog().warning(f" 合并模块编译失败: {result.stderr.strip()},回退到单独编译")
|
||
has_inline = False
|
||
except Exception as e:
|
||
_vlog().warning(f" 合并模块编译异常: {e},回退到单独编译")
|
||
has_inline = False
|
||
except FileNotFoundError:
|
||
_vlog().warning(f" 找不到 llvm-link 或 opt,回退到单独编译")
|
||
has_inline = False
|
||
else:
|
||
_vlog().warning(" 未找到 llvm-link/opt 工具,回退到单独编译(alwaysinline 可能不生效)")
|
||
|
||
def _compile_one(ll_path: str) -> tuple[str, str, str]:
|
||
rel = os.path.relpath(ll_path, self.output_dir)
|
||
obj_path = ll_path[:-3] + '.obj'
|
||
if _CachedIsFile(obj_path):
|
||
return (rel, 'cached', '')
|
||
try:
|
||
ll_content = _ReadTextCached(ll_path)
|
||
sha1 = os.path.basename(ll_path)[:-3]
|
||
deps = self._LoadDeps(sha1)
|
||
stub_prefix = ''
|
||
if deps:
|
||
# 去重:同一个全局变量如果同时有 external 声明和实际定义,
|
||
# 移除 external 声明,避免 "redefinition of global" 错误
|
||
# 注意:此去重必须在收集现有符号之前执行,否则行号偏移会导致
|
||
# lines_to_remove 误删其他行(如全局变量定义)
|
||
ll_lines_for_dedup = ll_content.splitlines()
|
||
external_globals: dict[str, int] = {}
|
||
non_external_globals: dict[str, int] = {}
|
||
for i, line in enumerate(ll_lines_for_dedup):
|
||
stripped = line.strip()
|
||
if stripped.startswith('@') and (' global ' in stripped or ' constant ' in stripped):
|
||
gm = _RE_AT_NAME.match(stripped)
|
||
if gm:
|
||
gname = gm.group(1)
|
||
is_external = _RE_AT_EXTERNAL.match(stripped) is not None
|
||
if is_external:
|
||
external_globals[gname] = i
|
||
else:
|
||
non_external_globals[gname] = i
|
||
lines_to_remove_global_dedup: set[int] = set()
|
||
for gname in external_globals:
|
||
if gname in non_external_globals:
|
||
lines_to_remove_global_dedup.add(external_globals[gname])
|
||
if lines_to_remove_global_dedup:
|
||
ll_content = '\n'.join(
|
||
line for i, line in enumerate(ll_lines_for_dedup)
|
||
if i not in lines_to_remove_global_dedup
|
||
) + '\n'
|
||
existing_symbols: dict[str, str] = {}
|
||
opaque_line_indices: dict[str, int] = {}
|
||
existing_declares: set[str] = set()
|
||
existing_declare_lines: dict[str, set[int]] = {}
|
||
for i, line in enumerate(ll_content.splitlines()):
|
||
stripped = line.strip()
|
||
if stripped.startswith('%') and '= type' in stripped:
|
||
name = Phase2Translator._extract_llvm_type_name(stripped)
|
||
if name:
|
||
if '= type opaque' in stripped:
|
||
existing_symbols[name] = 'opaque'
|
||
opaque_line_indices[name] = i
|
||
else:
|
||
existing_symbols[name] = 'full'
|
||
elif stripped.startswith('declare'):
|
||
dm = _RE_DECLARE_NAME.match(stripped)
|
||
if dm:
|
||
fname = dm.group(1)
|
||
existing_declares.add(fname)
|
||
existing_declare_lines.setdefault(fname, set()).add(i)
|
||
seen_stub_sha1: set[str] = set()
|
||
replaced_opaque_names: set[str] = set()
|
||
replaced_declare_names: set[str] = set()
|
||
all_stub_lines: list[str | None] = []
|
||
stub_type_map: dict[str, tuple[bool, int]] = {}
|
||
stub_declare_names: set[str] = set()
|
||
stub_declare_lines: dict[str, str] = {}
|
||
existing_defines: set[str] = set()
|
||
existing_global_defs: set[str] = set()
|
||
stub_global_defs: set[str] = set()
|
||
for line in ll_content.splitlines():
|
||
stripped = line.strip()
|
||
if stripped.startswith('define'):
|
||
dm = _RE_DEFINE_NAME.match(stripped)
|
||
if dm:
|
||
existing_defines.add(dm.group(1))
|
||
elif stripped.startswith('@') and (' global ' in stripped or ' constant ' in stripped):
|
||
gm = _RE_AT_NAME.match(stripped)
|
||
if gm:
|
||
existing_global_defs.add(gm.group(1))
|
||
for dep_name, dep_sha1 in deps.items():
|
||
if dep_sha1 in seen_stub_sha1:
|
||
continue
|
||
seen_stub_sha1.add(dep_sha1)
|
||
stub_path = os.path.join(self.output_dir, f"{dep_sha1}.stub.ll")
|
||
if not _CachedIsFile(stub_path):
|
||
stub_path = os.path.join(self.temp_dir, f"{dep_sha1}.stub.ll")
|
||
if _CachedIsFile(stub_path):
|
||
stub_content = _ReadTextCached(stub_path)
|
||
for line in stub_content.splitlines():
|
||
stripped = line.strip()
|
||
if stripped.startswith(';') or stripped.startswith('target ') or stripped.startswith('source_filename') or not stripped:
|
||
continue
|
||
if stripped.startswith('declare'):
|
||
dm = _RE_DECLARE_NAME.match(stripped)
|
||
if dm:
|
||
fname = dm.group(1)
|
||
if 'zdef_alloc' in fname:
|
||
_vlog().debug(f"[DEBUG] stub declare for {fname}: existing_declares={fname in existing_declares}, stub_declare_names={fname in stub_declare_names}, has_struct={'%' in stripped}, existing={existing_declare_lines.get(fname)}")
|
||
if fname in existing_defines:
|
||
continue
|
||
if fname in existing_declares or fname in stub_declare_names:
|
||
existing_decl_line = stub_declare_lines.get(fname)
|
||
if existing_decl_line is not None:
|
||
has_struct = '%' in stripped
|
||
existing_has_struct = '%' in existing_decl_line if existing_decl_line else False
|
||
if has_struct and not existing_has_struct:
|
||
for i, l in enumerate(all_stub_lines):
|
||
if l is not None and l.strip().startswith('declare'):
|
||
em = _RE_DECLARE_NAME.match(l.strip())
|
||
if em and em.group(1) == fname:
|
||
all_stub_lines[i] = line
|
||
break
|
||
continue
|
||
elif fname in existing_declares:
|
||
has_struct = '%' in stripped
|
||
if has_struct:
|
||
replaced_declare_names.add(fname)
|
||
stub_declare_names.add(fname)
|
||
stub_declare_lines[fname] = line
|
||
all_stub_lines.append(line)
|
||
continue
|
||
continue
|
||
stub_declare_names.add(fname)
|
||
stub_declare_lines[fname] = line
|
||
all_stub_lines.append(line)
|
||
continue
|
||
if stripped.startswith('%') and '= type' in stripped:
|
||
name = Phase2Translator._extract_llvm_type_name(stripped)
|
||
if name:
|
||
is_opaque = '= type opaque' in stripped
|
||
if name in existing_symbols:
|
||
if existing_symbols[name] == 'opaque' and not is_opaque:
|
||
replaced_opaque_names.add(name)
|
||
existing_symbols[name] = 'full'
|
||
idx = len(all_stub_lines)
|
||
all_stub_lines.append(line)
|
||
stub_type_map[name] = (is_opaque, idx)
|
||
continue
|
||
if name in stub_type_map:
|
||
prev_opaque, prev_idx = stub_type_map[name]
|
||
if prev_opaque and not is_opaque:
|
||
all_stub_lines[prev_idx] = None
|
||
idx = len(all_stub_lines)
|
||
all_stub_lines.append(line)
|
||
stub_type_map[name] = (is_opaque, idx)
|
||
continue
|
||
idx = len(all_stub_lines)
|
||
all_stub_lines.append(line)
|
||
stub_type_map[name] = (is_opaque, idx)
|
||
else:
|
||
all_stub_lines.append(line)
|
||
elif stripped.startswith('@') and (' global ' in stripped or ' constant ' in stripped):
|
||
gm = _RE_AT_NAME.match(stripped)
|
||
if gm and (gm.group(1) in existing_global_defs or gm.group(1) in stub_global_defs):
|
||
continue
|
||
if gm:
|
||
stub_global_defs.add(gm.group(1))
|
||
all_stub_lines.append(line)
|
||
else:
|
||
all_stub_lines.append(line)
|
||
filtered = [l for l in all_stub_lines if l is not None]
|
||
if filtered:
|
||
stub_prefix = '\n'.join(filtered) + '\n'
|
||
lines_to_remove = set()
|
||
if replaced_opaque_names:
|
||
for name in replaced_opaque_names:
|
||
if name in opaque_line_indices:
|
||
lines_to_remove.add(opaque_line_indices[name])
|
||
if replaced_declare_names:
|
||
_vlog().debug(f"[DEBUG] replaced_declare_names: {replaced_declare_names}")
|
||
for fname in replaced_declare_names:
|
||
if fname in existing_declare_lines:
|
||
lines_to_remove.update(existing_declare_lines[fname])
|
||
_vlog().debug(f"[DEBUG] removing lines {existing_declare_lines[fname]} for {fname}")
|
||
else:
|
||
_vlog().debug(f"[DEBUG] fname {fname} not in existing_declare_lines: {list(existing_declare_lines.keys())}")
|
||
if lines_to_remove:
|
||
ll_lines = ll_content.splitlines(True)
|
||
ll_content = ''.join(line for i, line in enumerate(ll_lines) if i not in lines_to_remove)
|
||
type_defs_in_ll = []
|
||
type_def_line_indices = set()
|
||
for i, line in enumerate(ll_content.splitlines()):
|
||
stripped = line.strip()
|
||
if stripped.startswith('%') and '= type' in stripped:
|
||
type_defs_in_ll.append(line)
|
||
type_def_line_indices.add(i)
|
||
if type_def_line_indices:
|
||
ll_lines = ll_content.splitlines(True)
|
||
ll_content = ''.join(line for i, line in enumerate(ll_lines) if i not in type_def_line_indices)
|
||
if type_defs_in_ll:
|
||
stub_prefix += '\n'.join(type_defs_in_ll) + '\n'
|
||
if stub_prefix:
|
||
header_end = 0
|
||
for i, line in enumerate(ll_content.splitlines()):
|
||
stripped = line.strip()
|
||
if stripped.startswith(';') or stripped.startswith('target ') or stripped.startswith('source_filename') or not stripped:
|
||
header_end = i + 1
|
||
continue
|
||
break
|
||
ll_lines = ll_content.splitlines(True)
|
||
ll_content = ''.join(ll_lines[:header_end]) + stub_prefix + ''.join(ll_lines[header_end:])
|
||
ll_content = _RE_DEFINE_DECLARE_LINKAGE.sub(r'\1 \2 dso_local ', ll_content)
|
||
ll_content = _RE_DEFINE_DECLARE_NO_LINKAGE_AT.sub(r'\1 dso_local ', ll_content)
|
||
# LLVM 22+: external 不允许在带初值的全局定义上显式写出,改为 linkonce 保留多模块合并语义
|
||
ll_content = _RE_EXTERNAL_GLOBAL_VALUE.sub(r'\1 = linkonce global \2 \3', ll_content)
|
||
ll_content = _RE_GLOBAL_LINKAGE_MULTILINE.sub(r'\1 = \2 dso_local \3', ll_content)
|
||
ll_content = _RE_GLOBAL_DSOLOCAL_MULTILINE.sub(r'\1 = dso_local \2', ll_content)
|
||
ll_content = _AddWeakComdat(ll_content)
|
||
# LLVM 22+: 移除同一模块中已有 define 的 declare 语句(LLVM 22 将 declare+define 视为重复定义)
|
||
defined_funcs: set[str] = set()
|
||
for m in _RE_DEFINE_HEAD.finditer(ll_content):
|
||
defined_funcs.add(m.group(1))
|
||
if defined_funcs:
|
||
lines: list[str] = ll_content.splitlines(True)
|
||
kept_lines: list[str] = []
|
||
for line in lines:
|
||
m = _RE_DECLARE_FULL.match(line)
|
||
if m and m.group(1) in defined_funcs:
|
||
continue
|
||
kept_lines.append(line)
|
||
ll_content = ''.join(kept_lines)
|
||
# 移除重复的 declare 语句(同一函数多次 declare 但没有 define)
|
||
seen_declares: set[str] = set()
|
||
dedup_lines: list[str] = ll_content.splitlines(True)
|
||
dedup_kept: list[str] = []
|
||
for line in dedup_lines:
|
||
dm = _RE_DECLARE_FULL.match(line)
|
||
if dm:
|
||
fname = dm.group(1)
|
||
if fname in seen_declares:
|
||
continue
|
||
seen_declares.add(fname)
|
||
dedup_kept.append(line)
|
||
ll_content = ''.join(dedup_kept)
|
||
final_type_defs = []
|
||
final_type_def_indices = set()
|
||
seen_type_names_main = set()
|
||
for i, line in enumerate(ll_content.splitlines()):
|
||
stripped = line.strip()
|
||
if stripped.startswith('%') and '= type' in stripped:
|
||
type_name_main = stripped.split('=')[0].strip()
|
||
final_type_def_indices.add(i)
|
||
if type_name_main in seen_type_names_main:
|
||
continue
|
||
seen_type_names_main.add(type_name_main)
|
||
final_type_defs.append(line)
|
||
if final_type_def_indices:
|
||
final_lines = ll_content.splitlines(True)
|
||
ll_content_no_types = ''.join(line for i, line in enumerate(final_lines) if i not in final_type_def_indices)
|
||
header_end = 0
|
||
for i, line in enumerate(ll_content_no_types.splitlines()):
|
||
stripped = line.strip()
|
||
if stripped.startswith(';') or stripped.startswith('target ') or stripped.startswith('source_filename') or not stripped:
|
||
header_end = i + 1
|
||
continue
|
||
break
|
||
final_lines2 = ll_content_no_types.splitlines(True)
|
||
ll_content = ''.join(final_lines2[:header_end]) + '\n'.join(final_type_defs) + '\n' + ''.join(final_lines2[header_end:])
|
||
# 写盘前先规整元数据:本文件的 IR 是「本模块 + 各依赖 .stub.ll/
|
||
# .text.ll 片段」拼接而成,每份片段都自带从 !0 起编号的 DI 元数据,
|
||
# 未规整直接落盘的话,任何读该文件的 opt/llc(含另一并发构建进程)
|
||
# 都会报 "Metadata id is already used"。规整后盘上产物本身即合法 IR,
|
||
# _llc_compile 内的规整随之成为幂等 no-op。
|
||
ll_content = SanitizeIRMetadata(ll_content)
|
||
# 拼接片段各自带一份同名 comdat 声明 ⇒ 落盘前统一去重:
|
||
# llc 对同一 comdat 声明出现两次即报 redefinition of comdat
|
||
ll_content = _DedupComdatDecls(ll_content)
|
||
dso_ll_path = ll_path[:-3] + '_dso.ll'
|
||
with open(dso_ll_path, 'w', encoding='utf-8') as f:
|
||
f.write(ll_content)
|
||
result = self._llc_compile(dso_ll_path, obj_path, os.path.dirname(ll_path) or '.')
|
||
if result.returncode == 0:
|
||
return (rel, 'ok', '')
|
||
else:
|
||
return (rel, 'error', result.stderr.strip())
|
||
except FileNotFoundError:
|
||
return (rel, 'error', f'找不到编译器: {self.compile_cmd}')
|
||
except Exception as e:
|
||
return (rel, 'error', str(e))
|
||
|
||
need_compile = []
|
||
for ll_path in ll_files:
|
||
obj_path = ll_path[:-3] + '.obj'
|
||
if not _CachedIsFile(obj_path):
|
||
need_compile.append(ll_path)
|
||
|
||
if need_compile:
|
||
n_workers = _PickWorkerCount(len(need_compile), 400)
|
||
_vlog().info(f" 并行编译 {len(need_compile)} 个文件 (workers={n_workers})")
|
||
errors = []
|
||
_compile_total = len(need_compile)
|
||
_compile_done = 0
|
||
with ThreadPoolExecutor(max_workers=n_workers) as executor:
|
||
futures = {}
|
||
for ll_path in need_compile:
|
||
futures[executor.submit(_compile_one, ll_path)] = (ll_path, time.time())
|
||
for future in as_completed(futures):
|
||
ll_path, _t_sub = futures[future]
|
||
rel, status, err = future.result()
|
||
_compile_done += 1
|
||
_elapsed_c = time.time() - _t_sub
|
||
_sha1_c = os.path.basename(ll_path)[:-3]
|
||
_src_c = self.sha1_map.get(_sha1_c, '?')
|
||
if status == 'ok':
|
||
_vlog().success(f" [{_compile_done}/{_compile_total}] {os.path.basename(ll_path)} {_src_c} ({_elapsed_c:.1f}s)")
|
||
elif status == 'error':
|
||
_vlog().error(f" [{_compile_done}/{_compile_total}] {rel}: {err}")
|
||
errors.append((rel, err))
|
||
elif status == 'cached':
|
||
pass
|
||
if errors:
|
||
_vlog().error(f"\n{len(errors)} 个文件编译失败")
|
||
sys.exit(1)
|
||
|
||
def _precollect_vtable_state(self) -> None:
|
||
"""预扫描 includes .py 文件,收集 @CVTable 类的虚表信息。
|
||
|
||
并行编译(ProcessPoolExecutor)创建独立内存的 worker 进程,
|
||
vtable 状态必须预收集并通过 pickle 共享。
|
||
复用 parse_python_file 的 AST 缓存。
|
||
"""
|
||
for includes_dir in self.include_dirs:
|
||
if not os.path.isdir(includes_dir):
|
||
continue
|
||
for root, dirs, files in os.walk(includes_dir):
|
||
dirs[:] = [d for d in dirs if not d.startswith('.') and d != '__pycache__']
|
||
for fname in files:
|
||
if not (fname.endswith('.vp') or fname.endswith('.py')):
|
||
continue
|
||
py_path: str = os.path.join(root, fname)
|
||
code, tree = parse_python_file(py_path)
|
||
# 字符串预过滤:不含 CVTable 的文件跳过 AST 遍历。
|
||
# `virtual class` / `virtual def` 修饰符在源码里写的是 `virtual`
|
||
# (AST 上才变成 @CVTable 装饰器),故预过滤必须同时放行
|
||
# `virtual`/`novirtual`,否则含修饰符的 includes 类会被整段跳过——
|
||
# 静默不预收集虚表状态(与本题"不得静默丢弃修饰符"同族问题)。
|
||
if tree is None or ('CVTable' not in code and 'virtual' not in code):
|
||
continue
|
||
try:
|
||
for node in ast.iter_child_nodes(tree):
|
||
if not isinstance(node, ast.ClassDef):
|
||
continue
|
||
has_cvtable: bool = False
|
||
for dec in node.decorator_list:
|
||
if isinstance(dec, ast.Name):
|
||
if dec.id == 'CVTable':
|
||
has_cvtable = True
|
||
# 方法级 virtual 触发类级 vtable(与
|
||
# HandlesClassDef._resolve_class_flags 同口径:类级
|
||
# NoVTable / 无真实父类两种可判定形态)。
|
||
# 记录"类级 CVTable"单独一份:它是 vtable 槽筛选的
|
||
# VtableByClassLevel(忽略方法级标记),两者不可混用。
|
||
class_level_cvtable: bool = has_cvtable
|
||
if not has_cvtable:
|
||
_has_real_parent: bool = False
|
||
for _b in node.bases:
|
||
_bn: str = None
|
||
if hasattr(_b, 'id'):
|
||
_bn = _b.id
|
||
elif hasattr(_b, 'attr'):
|
||
_bn = _b.attr
|
||
if _bn and _bn not in ('Object', 'CVTable', 'NoVTable', 'CStruct', 'CUnion', 'CEnum', 'REnum', 'Exception', 'Enum'):
|
||
_has_real_parent = True
|
||
break
|
||
if ClassHasFuncLevelCVTable(node) and not _has_real_parent:
|
||
has_cvtable = True
|
||
if not has_cvtable:
|
||
continue
|
||
class_name: str = node.name
|
||
has_methods: bool = False
|
||
if class_name not in self._shared_class_methods:
|
||
self._shared_class_methods[class_name] = []
|
||
for item in node.body:
|
||
if isinstance(item, ast.FunctionDef):
|
||
has_methods = True
|
||
method_name: str = item.name
|
||
# 构造函数不放入 vtable(避免子类与基类 vtable 大小不一致破坏多态分派)
|
||
if method_name in ('__new__', '__init__', '__before_init__'):
|
||
continue
|
||
# 方法级修饰符:`novirtual def`(⇒ @NoVTable)不进槽;
|
||
# 判据唯一源 = SymbolUtils.IsVtableSlotMethod,与
|
||
# HandlesClassDef / VPC _should_method_be_virtual 同口径。
|
||
if not IsVtableSlotMethod(item, class_level_cvtable):
|
||
continue
|
||
if method_name == '__init__':
|
||
func_full_name: str = f"{class_name}.__init__"
|
||
elif method_name == '__call__':
|
||
func_full_name = f"{class_name}.__call__"
|
||
else:
|
||
func_full_name = f"{class_name}.{method_name}"
|
||
if func_full_name not in self._shared_class_methods[class_name]:
|
||
self._shared_class_methods[class_name].append(func_full_name)
|
||
if has_methods:
|
||
self._shared_class_vtable.add(class_name)
|
||
self._shared_cross_module_vtable.add(class_name)
|
||
except Exception:
|
||
pass
|
||
|
||
def _build_include_py_index(self, includes_dir: str) -> set[str]:
|
||
"""预扫描 includes 目录,构建所有 .py 文件路径集合
|
||
|
||
用 set 查询替代 os.path.isfile,避免 3490 次磁盘 stat(83s→<0.1s)。
|
||
同时规范化路径为小写(Windows 不区分大小写)以加速查找。
|
||
"""
|
||
py_index: set[str] = set()
|
||
for root, dirs, files in os.walk(includes_dir):
|
||
dirs[:] = [d for d in dirs if not d.startswith('.') and d != '__pycache__']
|
||
for fname in files:
|
||
if fname.endswith('.vp') or fname.endswith('.py'):
|
||
py_index.add(os.path.normcase(os.path.join(root, fname)))
|
||
return py_index
|
||
|
||
def _scan_include_libraries(self) -> None:
|
||
"""扫描 includes 目录,检测被使用的模块对应的库文件和源文件"""
|
||
# --traceback:强制纳入 traceback 模块 —— 无任何用户代码 import 它,
|
||
# 不纳入就不会被翻译 ⇒ 产物没有 Install 符号可注入启动期构造器。
|
||
# 直接借用既有的 used_includes 收集路径(下面按同一套 py_index /
|
||
# 传递依赖逻辑把它加入 extra_py_files,再交给 _compile_include_py_files
|
||
# 编译并把 .obj 追加进 extra_link_files)。开关关闭时不触碰 used_includes
|
||
# → 本函数行为与改动前逐字一致(零影响)。
|
||
if self.traceback_enabled:
|
||
self.used_includes.add('traceback')
|
||
if not self.used_includes:
|
||
return
|
||
|
||
for includes_dir in self.include_dirs:
|
||
if not os.path.isdir(includes_dir):
|
||
continue
|
||
|
||
# 预构建 .py 文件索引,避免递归发现时反复 os.path.isfile
|
||
py_index = self._build_include_py_index(includes_dir)
|
||
|
||
for module_name in self.used_includes:
|
||
module_dir = os.path.join(includes_dir, module_name)
|
||
if os.path.isdir(module_dir):
|
||
self._scan_dir_for_libs(module_dir, module_name)
|
||
for root, dirs, files in os.walk(module_dir):
|
||
dirs[:] = [d for d in dirs if not d.startswith('.') and d != '__pycache__']
|
||
for fname in files:
|
||
if fname.endswith('.vp') or fname.endswith('.py'):
|
||
py_path = os.path.join(root, fname)
|
||
if py_path not in self.extra_py_files:
|
||
self.extra_py_files.append(py_path)
|
||
_vlog().info(f" [includes] 发现 Python 包文件: {os.path.relpath(py_path, includes_dir)}")
|
||
|
||
for ext in self.INCLUDE_LIB_EXTENSIONS:
|
||
lib_path = os.path.join(includes_dir, module_name + ext)
|
||
if _CachedIsFile(lib_path):
|
||
self.extra_link_files.append(lib_path)
|
||
_vlog().info(f" [includes] 发现库文件: {os.path.relpath(lib_path, includes_dir)}")
|
||
|
||
for ext in self.INCLUDE_SRC_EXTENSIONS:
|
||
src_path = os.path.join(includes_dir, module_name + ext)
|
||
if _CachedIsFile(src_path):
|
||
self.extra_compile_files.append(src_path)
|
||
_vlog().info(f" [includes] 发现源文件: {os.path.relpath(src_path, includes_dir)}")
|
||
|
||
py_path = os.path.join(includes_dir, module_name + '.vp')
|
||
if os.path.normcase(py_path) not in py_index:
|
||
# 回退到 .py(向后兼容)
|
||
py_path = os.path.join(includes_dir, module_name + '.py')
|
||
if os.path.normcase(py_path) in py_index:
|
||
self.extra_py_files.append(py_path)
|
||
_vlog().info(f" [includes] 发现 Python 源文件: {os.path.relpath(py_path, includes_dir)}")
|
||
|
||
discovered = set()
|
||
for ef in list(self.extra_py_files):
|
||
self._discover_transitive_deps(ef, includes_dir, discovered, py_index)
|
||
|
||
if self.extra_link_files:
|
||
_vlog().info(f"\n[includes] 需要链接的库文件: {len(self.extra_link_files)} 个")
|
||
if self.extra_compile_files:
|
||
_vlog().info(f"[includes] 需要编译的源文件: {len(self.extra_compile_files)} 个")
|
||
# --traceback:断言 traceback 源文件确实被纳入待编译集合。找不到就必然
|
||
# 得不到 Install 符号、构造器无处注入(功能静默失效)——显式报错终止,
|
||
# 不静默跳过(多半是 project.json 的 includes 未包含仓库的 includes 目录)。
|
||
if self.traceback_enabled:
|
||
_traceback_found: bool = any(
|
||
os.path.basename(_p) in _TRACEBACK_MODULE_FILES
|
||
for _p in self.extra_py_files)
|
||
if not _traceback_found:
|
||
raise RuntimeError(
|
||
'--traceback: 在 includes 目录中找不到 traceback.vp/.py,'
|
||
'无法编译并链接 Install()(请确认 project.json 的 includes '
|
||
f'包含仓库 includes 目录;已扫描: {self.include_dirs})')
|
||
|
||
def _scan_dir_for_libs(self, directory: str, module_name: str) -> None:
|
||
"""递归扫描目录中的库文件和源文件
|
||
|
||
模块目录含 `.no_autobuild` 标记时直接跳过:该模块的 C/C++ 源码
|
||
与库文件由 project.vpj linker.static_libs 显式链接(如 fltk 内嵌
|
||
完整 FLTK/C 源码树,避免误编译整个第三方库)。递归时跳过常见
|
||
构建/缓存目录(build、cmake-build*、.git、__pycache__、. 开头)。
|
||
"""
|
||
if _CachedIsFile(os.path.join(directory, '.no_autobuild')):
|
||
return
|
||
for root, dirs, files in os.walk(directory):
|
||
dirs[:] = [
|
||
d for d in dirs
|
||
if d not in ('.git', '__pycache__', 'build', 'cmake-build')
|
||
and not d.startswith('cmake-build-')
|
||
and not d.startswith('.')
|
||
]
|
||
for fname in files:
|
||
fpath = os.path.join(root, fname)
|
||
_, ext = os.path.splitext(fname)
|
||
ext = ext.lower()
|
||
if ext in self.INCLUDE_LIB_EXTENSIONS:
|
||
self.extra_link_files.append(fpath)
|
||
_vlog().info(f" [includes] 发现库文件: {os.path.relpath(fpath, directory)}")
|
||
elif ext in self.INCLUDE_SRC_EXTENSIONS:
|
||
self.extra_compile_files.append(fpath)
|
||
_vlog().info(f" [includes] 发现源文件: {os.path.relpath(fpath, directory)}")
|
||
|
||
def _discover_transitive_deps(self, py_path: str, includes_dir: str, discovered: set[str], py_index: set[str] | None = None) -> None:
|
||
"""递归发现 Python 文件的间接依赖
|
||
|
||
复用 parse_python_file 的 AST 缓存,避免重复 open+read+ast.parse。
|
||
py_index 为预构建的 .py 文件路径集合,避免 os.path.isfile 磁盘 stat。
|
||
"""
|
||
if py_path in discovered:
|
||
return
|
||
discovered.add(py_path)
|
||
_, tree = parse_python_file(py_path)
|
||
if tree is None:
|
||
return
|
||
try:
|
||
for node in ast.walk(tree):
|
||
if isinstance(node, ast.Import):
|
||
for alias in node.names:
|
||
self._try_add_include_dep(alias.name, includes_dir, discovered, py_index)
|
||
elif isinstance(node, ast.ImportFrom):
|
||
if node.module and node.level == 0:
|
||
self._try_add_include_dep(node.module, includes_dir, discovered, py_index)
|
||
except Exception as _e:
|
||
if _config_mode == "strict":
|
||
raise
|
||
_vlog().warning(f"发现 include 传递依赖失败: {_e}", "Exception")
|
||
|
||
def _try_add_include_dep(self, module_name: str, includes_dir: str, discovered: set, py_index: set[str] | None = None):
|
||
"""尝试将模块添加为 include 依赖(支持子包,如 win32.win32base)
|
||
|
||
py_index 非空时用 set 查询替代 os.path.isfile(O(1) vs 24ms/次)。
|
||
"""
|
||
# 候选路径:子包路径(win32/win32base.vp/.py)、顶层模块(win32.vp/.py)、子包 __init__.vp/__init__.py
|
||
candidates: list[str] = []
|
||
# 子包路径:win32.win32base → win32/win32base.vp / .py
|
||
candidates.append(os.path.join(includes_dir, module_name.replace('.', os.sep) + '.vp'))
|
||
candidates.append(os.path.join(includes_dir, module_name.replace('.', os.sep) + '.py'))
|
||
# 子包 __init__.vp/__init__.py:win32.win32base → win32/win32base/__init__.vp / .py
|
||
candidates.append(os.path.join(includes_dir, module_name.replace('.', os.sep), '__init__.vp'))
|
||
candidates.append(os.path.join(includes_dir, module_name.replace('.', os.sep), '__init__.py'))
|
||
# 顶层模块:取第一个组件 → win32.vp / .py
|
||
top_module = module_name.split('.')[0]
|
||
if top_module != module_name:
|
||
candidates.append(os.path.join(includes_dir, top_module + '.vp'))
|
||
candidates.append(os.path.join(includes_dir, top_module + '.py'))
|
||
|
||
for py_path in candidates:
|
||
exists: bool
|
||
if py_index is not None:
|
||
exists = os.path.normcase(py_path) in py_index
|
||
else:
|
||
exists = _CachedIsFile(py_path)
|
||
if exists and py_path not in self.extra_py_files:
|
||
self.extra_py_files.append(py_path)
|
||
_vlog().info(f" [includes] 发现间接依赖: {os.path.relpath(py_path, includes_dir)}")
|
||
self._discover_transitive_deps(py_path, includes_dir, discovered, py_index)
|
||
return # 找到第一个匹配即可
|
||
|
||
def _is_decl_only_file(self, src_path: str) -> bool:
|
||
"""复用 parse_python_file 的 AST 缓存"""
|
||
_, tree = parse_python_file(src_path)
|
||
if tree is None:
|
||
return False
|
||
try:
|
||
for node in ast.walk(tree):
|
||
if isinstance(node, ast.ClassDef):
|
||
return False
|
||
if isinstance(node, ast.FunctionDef):
|
||
body = node.body
|
||
if len(body) == 1:
|
||
stmt = body[0]
|
||
if isinstance(stmt, ast.Expr) and isinstance(stmt.value, ast.Constant) and stmt.value.value is ...:
|
||
continue
|
||
if isinstance(stmt, ast.Pass):
|
||
continue
|
||
return False
|
||
return True
|
||
except Exception as _e:
|
||
if _config_mode == "strict":
|
||
raise
|
||
_vlog().warning(f"判断是否为声明文件失败: {_e}", "Exception")
|
||
return False
|
||
|
||
def _sort_include_files_by_deps(self, file_list: list[str]) -> tuple[list[str], dict[str, set[str]]]:
|
||
"""按依赖关系对 include 文件进行拓扑排序,确保被依赖的文件先编译。
|
||
|
||
返回 (排序后的文件列表, 依赖字典)。
|
||
依赖字典: {fpath: set(依赖的fpath)},仅包含 file_list 中的依赖。
|
||
"""
|
||
# 构建文件名到路径的映射
|
||
name_to_path = {}
|
||
for fpath in file_list:
|
||
basename = os.path.splitext(os.path.basename(fpath))[0]
|
||
name_to_path[basename] = fpath
|
||
|
||
# 也支持包内模块: vpsdk/window -> path
|
||
for fpath in file_list:
|
||
# 尝试从 includes 目录推断模块全名
|
||
for includes_dir in self.include_dirs:
|
||
try:
|
||
rel = os.path.relpath(fpath, includes_dir)
|
||
mod_name = os.path.splitext(rel)[0].replace(os.sep, '.').replace('/', '.')
|
||
name_to_path[mod_name] = fpath
|
||
except ValueError:
|
||
pass
|
||
|
||
# 分析每个文件的 import 依赖(复用 parse_python_file 的 AST 缓存)
|
||
deps = {} # path -> set of paths it depends on
|
||
for fpath in file_list:
|
||
deps[fpath] = set()
|
||
try:
|
||
_, tree = parse_python_file(fpath)
|
||
if tree is None:
|
||
continue
|
||
for node in ast.iter_child_nodes(tree):
|
||
if isinstance(node, ast.Import):
|
||
for alias in node.names:
|
||
# 尝试完整路径和各级前缀,确保同包内子模块依赖能被正确解析
|
||
# 例如 import zlib.zhuff -> 应先匹配 zlib.zhuff,再回退到 zlib
|
||
parts = alias.name.split('.')
|
||
for i in range(len(parts), 0, -1):
|
||
mod = '.'.join(parts[:i])
|
||
if mod in name_to_path and name_to_path[mod] != fpath:
|
||
deps[fpath].add(name_to_path[mod])
|
||
break
|
||
elif isinstance(node, ast.ImportFrom):
|
||
if node.module and node.level == 0:
|
||
# 绝对导入:同样尝试完整路径和各级前缀
|
||
parts = node.module.split('.')
|
||
for i in range(len(parts), 0, -1):
|
||
mod = '.'.join(parts[:i])
|
||
if mod in name_to_path and name_to_path[mod] != fpath:
|
||
deps[fpath].add(name_to_path[mod])
|
||
break
|
||
elif node.level > 0:
|
||
# 相对导入:from .xxx import yyy 或 from ..xxx import yyy
|
||
# 根据当前文件位置推断目标模块的完整路径
|
||
current_dir = os.path.dirname(fpath)
|
||
# node.level == 1 表示当前包,node.level == 2 表示上一级包
|
||
package_dir = current_dir
|
||
for _ in range(node.level - 1):
|
||
package_dir = os.path.dirname(package_dir)
|
||
# 推断当前包名(从 includes 目录的相对路径)
|
||
package_name: str | None = None
|
||
for includes_dir in self.include_dirs:
|
||
try:
|
||
rel = os.path.relpath(package_dir, includes_dir)
|
||
if rel and rel != '.':
|
||
package_name = rel.replace(os.sep, '.').replace('/', '.')
|
||
break
|
||
except ValueError:
|
||
pass
|
||
# 构建目标模块名并查找
|
||
target_mod: str | None = None
|
||
if node.module:
|
||
target_mod = f"{package_name}.{node.module}" if package_name else node.module
|
||
else:
|
||
# from . import yyy:yyy 是子模块,在 names 中
|
||
target_mod = package_name
|
||
if target_mod:
|
||
parts = target_mod.split('.')
|
||
for i in range(len(parts), 0, -1):
|
||
mod = '.'.join(parts[:i])
|
||
if mod in name_to_path and name_to_path[mod] != fpath:
|
||
deps[fpath].add(name_to_path[mod])
|
||
break
|
||
# from . import yyy:yyy 可能是子模块
|
||
if node.level > 0 and not node.module:
|
||
for alias in node.names:
|
||
sub_mod = f"{package_name}.{alias.name}" if package_name else alias.name
|
||
if sub_mod in name_to_path and name_to_path[sub_mod] != fpath:
|
||
deps[fpath].add(name_to_path[sub_mod])
|
||
except Exception as _e:
|
||
if _config_mode == "strict":
|
||
raise
|
||
_vlog().warning(f"分析 include 文件导入依赖失败: {_e}", "Exception")
|
||
|
||
# 拓扑排序 (Kahn's algorithm)
|
||
# in_degree 只计入 file_list 中的依赖——decl-only 文件(如 stdint.py)
|
||
# 不在 file_list 中,不应计入入度,否则依赖它们的文件入度永远不为 0,
|
||
# 被误判为循环依赖通过 fallback 添加,导致编译顺序错误。
|
||
file_set = set(file_list)
|
||
in_degree = {fpath: sum(1 for dep in deps[fpath] if dep in file_set) for fpath in file_list}
|
||
# 反向邻接表:如果 A 依赖 B,则 B -> A
|
||
reverse_adj = {fpath: [] for fpath in file_list}
|
||
for fpath in file_list:
|
||
for dep in deps[fpath]:
|
||
if dep in reverse_adj:
|
||
reverse_adj[dep].append(fpath)
|
||
|
||
result = []
|
||
queue = [fpath for fpath in file_list if in_degree[fpath] == 0]
|
||
# 对队列排序以保持确定性
|
||
queue.sort(key=lambda x: x)
|
||
|
||
while queue:
|
||
current = queue.pop(0)
|
||
result.append(current)
|
||
for neighbor in reverse_adj[current]:
|
||
in_degree[neighbor] -= 1
|
||
if in_degree[neighbor] == 0:
|
||
queue.append(neighbor)
|
||
queue.sort(key=lambda x: x)
|
||
|
||
# 如果有循环依赖,把剩余文件也加入
|
||
for fpath in file_list:
|
||
if fpath not in result:
|
||
result.append(fpath)
|
||
|
||
# 仅保留 file_list 内的依赖(用于波次并行翻译)
|
||
file_set_result = set(file_list)
|
||
deps_in_set = {fpath: (deps[fpath] & file_set_result) for fpath in file_list}
|
||
return result, deps_in_set
|
||
|
||
def _get_precompiled_obj_path(self, src_path: str, sha1: str) -> str | None:
|
||
"""计算 include 文件在 includes.binary/ 中的预编译 .obj 路径。
|
||
|
||
预编译目录位于 include 目录同级,命名为 {basename}.binary/。
|
||
文件命名为 {sha1}.{triple_safe}.{rel_model}.obj,区分目标平台和重定位模型。
|
||
|
||
--traceback:include 模块也带 DWARF 行表(栈帧要能落到 includes/* 的源行),
|
||
此类 .obj 不得回写进跨项目共享的 includes.binary(会污染其它项目的预编译
|
||
缓存),改落**兄弟目录** {basename}.binary + 后缀(不新增层级,形态与
|
||
ASan 变体目录 includes.binary_asan 一致)。
|
||
关闭时后缀为空 → 路径与改动前逐字一致(含 --dwarf/--asan 单独开启的情形)。
|
||
--asan 与 --traceback 同开:ASan 插桩也会改变 include obj 内容(见
|
||
_llc_compile),两者产物不可互相复用,故再叠加 _asan 后缀区分
|
||
(优先级:是否隔离由 --traceback 决定,--asan 只决定后缀再叠加 _asan)。
|
||
"""
|
||
AbsSrcPath: str = os.path.abspath(src_path)
|
||
RelModel: str = 'static'
|
||
for _flag in self.compile_flags:
|
||
if _flag.startswith('-relocation-model='):
|
||
RelModel = _flag[len('-relocation-model='):]
|
||
break
|
||
for inc_dir in self.include_dirs:
|
||
abs_inc: str = os.path.abspath(inc_dir)
|
||
try:
|
||
rel: str = os.path.relpath(AbsSrcPath, abs_inc)
|
||
if rel.startswith('..'):
|
||
continue
|
||
inc_parent: str = os.path.dirname(abs_inc)
|
||
inc_base: str = os.path.basename(abs_inc)
|
||
_iso_suffix: str = ''
|
||
if self.traceback_enabled:
|
||
_iso_suffix = '_traceback_asan' if self.asan_mode else '_traceback'
|
||
precompiled_dir: str = os.path.join(inc_parent, inc_base + '.binary' + _iso_suffix)
|
||
triple_safe: str = (self.triple or 'default').replace(os.sep, '_').replace('/', '_').replace('\\', '_').replace(':', '_')
|
||
return os.path.join(precompiled_dir, f"{sha1}.{triple_safe}.{RelModel}.obj")
|
||
except ValueError:
|
||
continue
|
||
return None
|
||
|
||
def _compile_include_py_files(self, active_sha1s: set[str]) -> None:
|
||
"""通过 TransPyC 翻译器编译 includes 目录中发现的 .py 文件
|
||
|
||
优化流程:预注册符号 → 并行翻译 → 串行后处理 → 并行编译 → 注册+缓存
|
||
"""
|
||
if not self.extra_py_files:
|
||
return
|
||
|
||
# 按依赖关系排序 include 文件,确保被依赖的文件先编译
|
||
sorted_files, file_deps = self._sort_include_files_by_deps(self.extra_py_files)
|
||
_vlog().info(f"\n[includes 编译] 找到 {len(sorted_files)} 个 Python 源文件")
|
||
|
||
# --hotspot-sites:include 模块同样被插桩(hotspot_sites_file 经 worker 透传),
|
||
# 故启用时必须让旧 .obj 失效并强制重译,否则会复用上一次未插桩的目标文件,
|
||
# 使 include 模块的热点计数/计时 IR 静默缺失。未启用时 hotspot_on=False,
|
||
# 本函数行为与改动前逐字节一致(零影响)。
|
||
hotspot_on: bool = bool(self.hotspot_sites_file)
|
||
|
||
# --traceback:include 模块也必须带 DWARF 行表(否则运行时 Ctrl+C 的栈里
|
||
# 落在 includes/*(memhub/string/viperlib/win32…)的帧取不到源行,只能落到
|
||
# 邻行或 "+0x…(unknown)")。为此翻译时给 include 透传 dwarf_enabled=True
|
||
# (见下方 worker/串行四处的 dwarf_enabled=self.traceback_enabled),并把
|
||
# 旧目标文件(output 目录 {sha1}.obj 及隔离缓存 includes.binary_traceback
|
||
# 内的旧 .obj)一并失效重译——复用上一次无 DI 的产物会让行表静默缺失。
|
||
# 与 hotspot_on 处理方式一致;同时覆盖 traceback.vp(其 .obj 还须含
|
||
# @llvm.global_ctors 启动期构造器)。取舍:开启本开关时 include 目标文件每
|
||
# 次构建都重译(放弃隔离缓存的复用,换取"必然带 DWARF"的正确性保证),
|
||
# 仅调试模式可接受。开关关闭时 include_dwarf=False,本函数行为与改动前
|
||
# 逐字节一致(零影响,含 --dwarf/--asan 单独开启的情形)。
|
||
include_dwarf: bool = bool(self.traceback_enabled)
|
||
|
||
# 构建 include 文件的模块 SHA1 映射(预计算所有文件,构建完整映射)
|
||
include_ModuleSha1Map = self._build_current_ModuleSha1Map(None)
|
||
need_translate = [] # [(src_path, sha1, module_name, ll_path, obj_path, precompiled_obj)]
|
||
for src_path in sorted_files:
|
||
module_name = os.path.splitext(os.path.basename(src_path))[0]
|
||
# 复用 parse_python_file 缓存的 content,避免重复 open+read
|
||
py_content, _ = parse_python_file(src_path)
|
||
sha1 = compute_sha1(py_content)
|
||
self.include_py_map[module_name] = sha1
|
||
active_sha1s.add(sha1)
|
||
self._include_sha1s.add(sha1)
|
||
|
||
for includes_dir in self.include_dirs:
|
||
try:
|
||
rel = os.path.relpath(src_path, includes_dir)
|
||
mod_full = os.path.splitext(rel)[0].replace(os.sep, '.').replace('/', '.')
|
||
include_ModuleSha1Map[mod_full] = sha1
|
||
include_ModuleSha1Map[module_name] = sha1
|
||
# 包入口文件:注册父包名(如 'os.__init__' -> 'os')
|
||
if module_name == '__init__' and '.' in mod_full:
|
||
parent_pkg = mod_full.rsplit('.', 1)[0]
|
||
include_ModuleSha1Map[parent_pkg] = sha1
|
||
except ValueError:
|
||
pass
|
||
include_ModuleSha1Map[module_name] = sha1
|
||
ll_path = os.path.join(self.output_dir, f"{sha1}.ll")
|
||
obj_path = os.path.join(self.output_dir, f"{sha1}.obj")
|
||
precompiled_obj = self._get_precompiled_obj_path(src_path, sha1)
|
||
|
||
if self._is_decl_only_file(src_path):
|
||
_vlog().info(f" 跳过(声明文件): {module_name}.py")
|
||
# 声明文件(如 stdint.py)虽不编译为 .obj,但其 typedef/类型别名
|
||
# 必须注册到共享符号表,否则依赖它的 include 文件编译时
|
||
# 类型解析失败(如 UINT32PTR 被当作命名结构体而非 i32*)
|
||
self._register_compiled_include(src_path, sha1, None, include_ModuleSha1Map)
|
||
self._collect_inline_symbols(src_path)
|
||
continue
|
||
|
||
# --hotspot-sites / --traceback:include 的 .py 未变时下面两处会因旧
|
||
# .obj 存在而直接跳过,复用上一次(未插桩 / 不含 DI)的目标文件。
|
||
# 启用时先让这两类旧 .obj 一并失效,使其必然走入 need_translate 重新
|
||
# 翻译并嵌入热点计数/计时 IR 或 DWARF 行表。
|
||
# 用 os.path.isfile 而非 _CachedIsFile:避免把"已存在"写入存在性缓存后又被
|
||
# 本处删除,使下面两处判定读到过期缓存而漏编(与 App 主循环同一写法)。
|
||
if hotspot_on or include_dwarf:
|
||
for _stale_obj in (precompiled_obj, obj_path):
|
||
if _stale_obj and os.path.isfile(_stale_obj):
|
||
try:
|
||
os.remove(_stale_obj)
|
||
except OSError as e:
|
||
# 不中断构建,但真实错误(如权限不足)必须打印告警,不静默吞掉
|
||
# (报文保持原样:关闭开关时与改动前逐字一致)
|
||
_vlog().warning(
|
||
f"Phase2Translator: 删除热点插桩 include 模块的旧目标文件失败: {_stale_obj}: {e}")
|
||
|
||
# 优先检查 includes.binary 预编译缓存(跨项目共享)
|
||
if not self.force_recompile_includes and precompiled_obj and _CachedIsFile(precompiled_obj):
|
||
_vlog().info(f" 跳过(预编译): {module_name}.py -> {os.path.basename(precompiled_obj)}")
|
||
# 预编译跳过的文件仍需注册 .pyi/.stub.ll 声明和符号,
|
||
# 否则依赖它的 include 文件(如 vpui.py import vpsdk.window)
|
||
# 在 _EmitModuleDeclarationsLlvm 中找不到声明,导致 Undefined symbol
|
||
self._register_compiled_include(src_path, sha1, None, include_ModuleSha1Map)
|
||
self._collect_inline_symbols(src_path)
|
||
self.extra_link_files.append(precompiled_obj)
|
||
continue
|
||
|
||
if not self.force_recompile_includes and _CachedIsFile(obj_path):
|
||
_vlog().info(f" 跳过(缓存): {module_name}.py -> {sha1}.obj")
|
||
self._register_compiled_include(src_path, sha1, None, include_ModuleSha1Map)
|
||
self._collect_inline_symbols(src_path)
|
||
self.extra_link_files.append(obj_path)
|
||
continue
|
||
|
||
need_translate.append((src_path, sha1, module_name, ll_path, obj_path, precompiled_obj))
|
||
|
||
if not need_translate:
|
||
return
|
||
|
||
# 预注册所有待翻译文件的符号(从 Phase1 缓存加载 .pyi / .stub.ll)
|
||
# 这样并行翻译时所有跨模块符号都已可用
|
||
for src_path, sha1, module_name, ll_path, obj_path, precompiled_obj in need_translate:
|
||
self._register_compiled_include(src_path, sha1, None, include_ModuleSha1Map)
|
||
|
||
# 预收集 stub_decls(所有文件共用,避免重复扫描 .stub.ll)
|
||
stub_decls = []
|
||
for other_sha1, other_stub in self.stub_files.items():
|
||
if os.path.exists(other_stub):
|
||
try:
|
||
for sline in _ReadTextCached(other_stub).splitlines(True):
|
||
sl = sline.strip()
|
||
if sl.startswith('%') and '= type' in sl and 'opaque' not in sl:
|
||
struct_name = Phase2Translator._extract_llvm_type_name(sl)
|
||
if struct_name:
|
||
stub_decls.append(sl)
|
||
except Exception as e:
|
||
_vlog().warning(f"Phase2Translator: 忽略异常 {e}", exc_info=e)
|
||
all_type_defs = {}
|
||
for sdecl in stub_decls:
|
||
tname = Phase2Translator._extract_llvm_type_name(sdecl)
|
||
if tname:
|
||
all_type_defs[tname] = sdecl
|
||
|
||
# ========== 阶段1:并行翻译 ==========
|
||
_vlog().info(f" 翻译 {len(need_translate)} 个文件")
|
||
if len(need_translate) > 1:
|
||
# 序列化共享符号表供 worker 使用
|
||
shared_pickle_path = os.path.join(self.output_dir, '_shared_sym_includes.pickle')
|
||
try:
|
||
# symbol_table 只以 pickle 字节传递(symbol_table_bytes),不重复序列化
|
||
# 对象本身(原双重序列化使文件体积/时间翻倍,4.65MB 中近半冗余)。
|
||
shared_data = {
|
||
'symbol_table_bytes': pickle.dumps(self._shared_symbol_table),
|
||
'source_module_sig_files': self._shared_source_module_sig_files,
|
||
'all_dc': self._shared_all_dc,
|
||
'all_decl_types': self._shared_all_decl_types,
|
||
'export_extern_funcs': self._shared_export_extern_funcs,
|
||
'inline_func_symbols': self.inline_func_symbols,
|
||
'function_default_args': self.function_default_args,
|
||
'function_param_names': self.function_param_names,
|
||
'generic_class_templates': self._shared_generic_class_templates,
|
||
'shared_doc_meta': self._shared_doc_meta,
|
||
'class_vtable': self._shared_class_vtable,
|
||
'cross_module_vtable': self._shared_cross_module_vtable,
|
||
'class_methods': self._shared_class_methods,
|
||
'cross_module_novtable': self._shared_cross_module_novtable,
|
||
}
|
||
with open(shared_pickle_path, 'wb') as f:
|
||
f.write(pickle.dumps(shared_data))
|
||
except Exception as e:
|
||
_vlog().warning(f"符号表序列化失败({e}),include将串行翻译")
|
||
shared_pickle_path = ''
|
||
|
||
if shared_pickle_path and os.path.exists(shared_pickle_path):
|
||
n_workers = _PickWorkerCount(len(need_translate))
|
||
errors = []
|
||
# 波次并行翻译:按依赖关系分批提交,确保被依赖的文件先翻译完成。
|
||
# 根因:_TryLoadStructFromStub 扫描 output 目录加载跨模块 struct 定义,
|
||
# 若依赖文件尚未完成翻译(output stub 未写入),只能取到 Phase1 temp
|
||
# 中的不完整 stub(缺少跨模块继承字段),导致运行时崩溃。
|
||
item_by_path = {item[0]: item for item in need_translate}
|
||
translate_paths = set(item_by_path.keys())
|
||
# 每个文件的依赖(仅限本轮待翻译集合内)
|
||
translate_deps = {
|
||
p: (file_deps.get(p, set()) & translate_paths)
|
||
for p in translate_paths
|
||
}
|
||
# 复用 self._executor_pool(与 App 源文件阶段共享),避免 GC 阻塞
|
||
if self._executor_pool is None:
|
||
# 新池:刷新 pyi pickle 缓存,把 _register_compiled_include 新增的
|
||
# include .vpi 一并落盘,使 worker 预热包含它们(签名未变时零开销)
|
||
if os.environ.get('TPC_WORKER_WARMUP', '1') == '1':
|
||
_BuildPyiCacheFile(self.temp_dir)
|
||
self._executor_pool = ProcessPoolExecutor(max_workers=n_workers,
|
||
initializer=_PyiCacheWorkerInitializer,
|
||
initargs=(self.temp_dir,))
|
||
executor = self._executor_pool
|
||
try:
|
||
completed_paths: set[str] = set()
|
||
remaining = list(need_translate) # 保持拓扑序
|
||
in_progress: dict = {} # future -> item
|
||
done_count = 0
|
||
|
||
while remaining or in_progress:
|
||
# 提交所有依赖已完成的文件
|
||
ready = [
|
||
item for item in remaining
|
||
if translate_deps[item[0]] <= completed_paths
|
||
]
|
||
for item in ready:
|
||
remaining.remove(item)
|
||
src_path, sha1, module_name, ll_path, obj_path, precompiled_obj = item
|
||
future = executor.submit(
|
||
_parallel_translate_worker,
|
||
src_path, ll_path,
|
||
self.src_root, self.temp_dir, self.output_dir,
|
||
self.include_dirs, self.triple, self.datalayout,
|
||
self.sha1_map, self.sig_files, self.stub_files,
|
||
self.include_py_map, self.slice_level,
|
||
shared_pickle_path, self.struct_sha1_map,
|
||
include_ModuleSha1Map,
|
||
hotspot_sites_file=self.hotspot_sites_file,
|
||
hotspot_sites_all=self.hotspot_sites_all,
|
||
hotspot_callees=self.hotspot_callees,
|
||
traceback_enabled=self.traceback_enabled,
|
||
dwarf_enabled=self.traceback_enabled
|
||
)
|
||
in_progress[future] = item
|
||
|
||
if not in_progress:
|
||
# 无可提交且无进行中:循环依赖 fallback
|
||
for item in remaining:
|
||
src_path, sha1, module_name, ll_path, obj_path, precompiled_obj = item
|
||
future = executor.submit(
|
||
_parallel_translate_worker,
|
||
src_path, ll_path,
|
||
self.src_root, self.temp_dir, self.output_dir,
|
||
self.include_dirs, self.triple, self.datalayout,
|
||
self.sha1_map, self.sig_files, self.stub_files,
|
||
self.include_py_map, self.slice_level,
|
||
shared_pickle_path, self.struct_sha1_map,
|
||
include_ModuleSha1Map,
|
||
hotspot_sites_file=self.hotspot_sites_file,
|
||
hotspot_sites_all=self.hotspot_sites_all,
|
||
hotspot_callees=self.hotspot_callees,
|
||
traceback_enabled=self.traceback_enabled,
|
||
dwarf_enabled=self.traceback_enabled
|
||
)
|
||
in_progress[future] = item
|
||
remaining.clear()
|
||
|
||
# 等待至少一个完成
|
||
done_set, _ = wait(in_progress, return_when=FIRST_COMPLETED)
|
||
for future in done_set:
|
||
item = in_progress.pop(future)
|
||
src_path, sha1, module_name, ll_path, obj_path, precompiled_obj = item
|
||
completed_paths.add(src_path)
|
||
done_count += 1
|
||
try:
|
||
status, _, err = future.result()
|
||
except ReportedCompileError:
|
||
raise
|
||
except Exception as e:
|
||
_vlog().error(f" {module_name}.py: {e}")
|
||
for f2 in in_progress:
|
||
f2.cancel()
|
||
if self._executor_pool is not None:
|
||
self._executor_pool.shutdown(wait=False, cancel_futures=True)
|
||
self._executor_pool = None
|
||
raise ReportedCompileError(
|
||
"include worker 异常终止,编译立即停止") from None
|
||
if status == 'ok':
|
||
_vlog().info(f" [{done_count}/{len(need_translate)}] 翻译完成: {module_name}.py")
|
||
else:
|
||
# worker 只回传错误、不写 stderr:完整错误(含源码定位)
|
||
# 只在这里输出一次,随后立即终止(fail-fast)。
|
||
_vlog().error(f"\n文件翻译失败: {module_name}.py\n{err}")
|
||
for f2 in in_progress:
|
||
f2.cancel()
|
||
if self._executor_pool is not None:
|
||
self._executor_pool.shutdown(wait=False, cancel_futures=True)
|
||
self._executor_pool = None
|
||
raise ReportedCompileError(
|
||
"include 翻译失败,编译立即停止") from None
|
||
except Exception:
|
||
if self._executor_pool is not None:
|
||
self._executor_pool.shutdown(wait=False, cancel_futures=True)
|
||
self._executor_pool = None
|
||
raise
|
||
|
||
if errors:
|
||
_vlog().error(f"\n{len(errors)} 个 include 文件翻译失败")
|
||
sys.exit(1)
|
||
else:
|
||
# 串行回退
|
||
for item in need_translate:
|
||
src_path, sha1, module_name, ll_path, obj_path, precompiled_obj = item
|
||
_vlog().info(f" 翻译: {module_name}.py -> {sha1}.ll")
|
||
try:
|
||
self._translate_file(src_path, ll_path, prebuilt_ModuleSha1Map=include_ModuleSha1Map, dwarf_enabled=self.traceback_enabled)
|
||
except Exception as e:
|
||
# 单点输出:完整错误(含源码定位)只打印这一次
|
||
_vlog().error(f"\n文件翻译失败: {module_name}.py\n{type(e).__name__}: {e}")
|
||
raise ReportedCompileError(
|
||
"include 翻译失败,编译立即停止") from None
|
||
else:
|
||
item = need_translate[0]
|
||
src_path, sha1, module_name, ll_path, obj_path, precompiled_obj = item
|
||
_vlog().info(f" 翻译: {module_name}.py -> {sha1}.ll")
|
||
try:
|
||
self._translate_file(src_path, ll_path, prebuilt_ModuleSha1Map=include_ModuleSha1Map, dwarf_enabled=self.traceback_enabled)
|
||
except Exception as e:
|
||
# 单点输出:完整错误(含源码定位)只打印这一次
|
||
_vlog().error(f"\n文件翻译失败: {module_name}.py\n{type(e).__name__}: {e}")
|
||
raise ReportedCompileError(
|
||
"include 翻译失败,编译立即停止") from None
|
||
|
||
# ========== 阶段2:串行后处理(dso_local + stub_decls 注入)==========
|
||
compile_tasks = [] # [(dso_ll_path, obj_path, src_path, sha1, module_name, ll_path, precompiled_obj, ll_content)]
|
||
for item in need_translate:
|
||
src_path, sha1, module_name, ll_path, obj_path, precompiled_obj = item
|
||
if not _CachedIsFile(ll_path):
|
||
# 翻译成功(worker 返回 ok)却没有产物 = 翻译失败:立即终止,
|
||
# 不允许跳过该模块继续后处理/编译/链接(否则只会得到链接期
|
||
# undefined symbol,真因与现场脱节)。
|
||
_vlog().error(f" 翻译未生成 .ll 文件: {module_name}.py")
|
||
raise ReportedCompileError(
|
||
f"include 翻译未生成 .ll 文件: {module_name}.py,编译立即停止")
|
||
|
||
ll_content = _ReadTextCached(ll_path)
|
||
|
||
# --traceback 提醒:include 模块若发射了 define 却不带 DI(!llvm.dbg.cu),
|
||
# 其 .debug_line 行为空 ⇒ 运行时 Ctrl+C 栈里该 include 帧取不到源行
|
||
# (会退化为 +0x… (unknown))。
|
||
# **只警告、不报错**:像 includes/stdarg.vp 这类模块的 define 是"合成"的
|
||
# (由 extern/sugar 派生、无 .vp 函数体),本就不会有源级 DI —— 按 fail-fast
|
||
# 报错会把这类模块全部误杀;而"行号缺失"只降低可读性,不是编译正确性问题。
|
||
if self.traceback_enabled and _RE_DEFINE_LINE.search(ll_content) \
|
||
and '!llvm.dbg.cu' not in ll_content \
|
||
and _SourceHasRealDef(src_path):
|
||
_vlog().warning(
|
||
f"--traceback: include 模块 {module_name}.py 发射了 define 但 IR 未携带 "
|
||
f"DWARF (!llvm.dbg.cu)——该模块内联帧在 Ctrl+C 栈里将只显示地址: {ll_path}")
|
||
|
||
# dso_local 正则标注(使用 ^ + MULTILINE 避免匹配字符串常量内容中的 define/declare)
|
||
ll_content = _RE_DEFINE_DECLARE_LINKAGE.sub(r'\1 \2 dso_local ', ll_content)
|
||
ll_content = _RE_DEFINE_DECLARE_NO_LINKAGE.sub(r'\1 dso_local ', ll_content)
|
||
# LLVM 22+: external 不允许在带初值的全局定义上显式写出,改为 linkonce 保留多模块合并语义
|
||
ll_content = _RE_EXTERNAL_GLOBAL_VALUE.sub(r'\1 = linkonce global \2 \3', ll_content)
|
||
ll_content = _RE_GLOBAL_LINKAGE_MULTILINE.sub(r'\1 = \2 dso_local \3', ll_content)
|
||
ll_content = _RE_GLOBAL_DSOLOCAL_MULTILINE.sub(r'\1 = dso_local \2', ll_content)
|
||
ll_content = _AddWeakComdat(ll_content)
|
||
|
||
# stub_decls 注入(复用预收集的 stub_decls)
|
||
if stub_decls:
|
||
for sdecl in stub_decls:
|
||
sname = Phase2Translator._extract_llvm_type_name(sdecl)
|
||
if sname:
|
||
short = sname.split('.')[-1] if '.' in sname else sname
|
||
ll_content = re.sub(
|
||
r'%"?' + re.escape(sname) + r'"?\s*=\s*type\s*opaque',
|
||
sdecl, ll_content)
|
||
if short != sname:
|
||
ll_content = re.sub(
|
||
r'%"?' + re.escape(short) + r'"?\s*=\s*type\s*opaque',
|
||
sdecl, ll_content)
|
||
|
||
max_iterations = 10
|
||
for _ in range(max_iterations):
|
||
defined_types = set()
|
||
for line in ll_content.splitlines():
|
||
sl = line.strip()
|
||
if sl.startswith('%') and '= type' in sl:
|
||
tname = Phase2Translator._extract_llvm_type_name(sl)
|
||
if tname:
|
||
defined_types.add(tname)
|
||
# 同时添加短名,防止 missing 循环因全名/短名不匹配而重复添加
|
||
if '.' in tname:
|
||
parts = tname.split('.', 1)
|
||
if len(parts[0]) == 16 and all(c in '0123456789abcdef' for c in parts[0]):
|
||
defined_types.add(parts[1])
|
||
|
||
referenced_types = Phase2Translator._find_all_type_refs(ll_content)
|
||
missing = referenced_types - defined_types
|
||
new_defs = []
|
||
for tname in sorted(missing):
|
||
if tname in all_type_defs:
|
||
new_defs.append(all_type_defs[tname])
|
||
else:
|
||
short_key = tname.split('.')[-1] if '.' in tname else tname
|
||
for full_name, defn in all_type_defs.items():
|
||
if full_name.split('.')[-1] == short_key:
|
||
if short_key not in defined_types:
|
||
new_defs.append(defn)
|
||
break
|
||
else:
|
||
# 未在 stub 中找到定义(如泛型类 list[T]),
|
||
# 声明为 opaque 以满足 llc 的类型前向声明要求
|
||
new_defs.append(f'%"{tname}" = type opaque')
|
||
|
||
if not new_defs:
|
||
break
|
||
|
||
inject_block = '\n'.join(new_defs) + '\n'
|
||
lines = ll_content.splitlines(True)
|
||
insert_pos = 0
|
||
for i, line in enumerate(lines):
|
||
stripped = line.strip()
|
||
if stripped and not stripped.startswith(';') and not stripped.startswith('target ') and not stripped.startswith('source_filename') and not stripped.startswith('declare ') and not stripped.startswith('@') and not stripped.startswith('define ') and not stripped.startswith('attributes ') and not stripped.startswith('!') and not stripped.startswith('module '):
|
||
insert_pos = i
|
||
break
|
||
if insert_pos == 0:
|
||
insert_pos = len(lines)
|
||
ll_content = ''.join(lines[:insert_pos]) + inject_block + ''.join(lines[insert_pos:])
|
||
|
||
# 将所有类型定义移到 declares 之前
|
||
# (llc 要求 by-value struct 参数的类型在 declare 前定义,否则报 invalid type for function argument)
|
||
lines = ll_content.splitlines(True)
|
||
type_def_lines = []
|
||
other_lines = []
|
||
seen_type_names_inc = set()
|
||
for line in lines:
|
||
stripped = line.strip()
|
||
if stripped.startswith('%') and '= type' in stripped:
|
||
type_name_inc = stripped.split('=')[0].strip()
|
||
if type_name_inc in seen_type_names_inc:
|
||
continue
|
||
seen_type_names_inc.add(type_name_inc)
|
||
type_def_lines.append(line)
|
||
else:
|
||
other_lines.append(line)
|
||
if type_def_lines:
|
||
insert_pos = 0
|
||
for i, line in enumerate(other_lines):
|
||
stripped = line.strip()
|
||
if stripped and not stripped.startswith(';') and not stripped.startswith('target ') and not stripped.startswith('source_filename') and not stripped.startswith('!'):
|
||
insert_pos = i
|
||
break
|
||
if insert_pos == 0:
|
||
insert_pos = len(other_lines)
|
||
ll_content = ''.join(other_lines[:insert_pos]) + ''.join(type_def_lines) + ''.join(other_lines[insert_pos:])
|
||
|
||
# 同 App 路径:写盘前规整元数据(拼接片段各自带 !0..!N 的 DI 元数据),
|
||
# 让盘上产物本身即合法 IR,不依赖 _llc_compile 里那次事后规整。
|
||
ll_content = SanitizeIRMetadata(ll_content)
|
||
# 同 App 路径:拼接片段各自带一份同名 comdat 声明 ⇒ 落盘前统一去重
|
||
ll_content = _DedupComdatDecls(ll_content)
|
||
dso_ll_path = ll_path[:-3] + '_dso.ll'
|
||
with open(dso_ll_path, 'w', encoding='utf-8') as f:
|
||
f.write(ll_content)
|
||
_InvalidateTextCache(dso_ll_path)
|
||
compile_tasks.append((dso_ll_path, obj_path, src_path, sha1, module_name, ll_path, precompiled_obj, ll_content))
|
||
|
||
if not compile_tasks:
|
||
return
|
||
|
||
# ========== 阶段3:并行编译(llc)==========
|
||
_vlog().info(f" 编译 {len(compile_tasks)} 个 .ll 文件")
|
||
compile_results = {} # task -> result
|
||
|
||
def _compile_one_include(task):
|
||
dso_ll_path, obj_path = task[0], task[1]
|
||
result = self._llc_compile(dso_ll_path, obj_path, self.output_dir)
|
||
return (task, result)
|
||
|
||
if len(compile_tasks) > 1:
|
||
n_workers = _PickWorkerCount(len(compile_tasks), 400)
|
||
with ThreadPoolExecutor(max_workers=n_workers) as executor:
|
||
futures = {executor.submit(_compile_one_include, task): task for task in compile_tasks}
|
||
done_count = 0
|
||
for future in as_completed(futures):
|
||
done_count += 1
|
||
try:
|
||
task, result = future.result()
|
||
module_name = task[4]
|
||
if result.returncode == 0:
|
||
_vlog().success(f" [{done_count}/{len(compile_tasks)}] 编译成功: {module_name}.py")
|
||
else:
|
||
_vlog().warning(f" [{done_count}/{len(compile_tasks)}] 编译失败: {module_name}.py: {result.stderr.strip()}")
|
||
compile_results[task] = result
|
||
except Exception as e:
|
||
task = futures[future]
|
||
module_name = task[4]
|
||
_vlog().error(f" [{done_count}/{len(compile_tasks)}] 编译异常: {module_name}.py: {e}")
|
||
compile_results[task] = None
|
||
else:
|
||
task = compile_tasks[0]
|
||
task_result, result = _compile_one_include(task)
|
||
module_name = task[4]
|
||
if result.returncode == 0:
|
||
_vlog().success(f" 编译成功: {module_name}.py")
|
||
else:
|
||
_vlog().warning(f" 编译失败: {module_name}.py: {result.stderr.strip()}")
|
||
compile_results[task] = result
|
||
|
||
# ========== 阶段4:注册 + 缓存 ==========
|
||
has_error = False
|
||
for task in compile_tasks:
|
||
dso_ll_path, obj_path, src_path, sha1, module_name, ll_path, precompiled_obj, ll_content = task
|
||
result = compile_results.get(task)
|
||
if result and result.returncode == 0:
|
||
self.extra_link_files.append(obj_path)
|
||
# 复制到 includes.binary 预编译目录(--traceback 时 precompiled_obj
|
||
# 指向隔离目录 includes.binary_traceback,不会污染共享缓存)
|
||
if precompiled_obj:
|
||
precompiled_dir = os.path.dirname(precompiled_obj)
|
||
try:
|
||
if not os.path.exists(precompiled_dir):
|
||
os.makedirs(precompiled_dir, exist_ok=True)
|
||
shutil.copy2(obj_path, precompiled_obj)
|
||
except Exception as _e:
|
||
# --traceback fail-fast:隔离目录创建 / 写回失败必须显式终止,
|
||
# 不静默降级(否则本次构建的 include obj 拿不到、或落到共享缓存
|
||
# 之外的位置,DWARF 行表与后续复用的前提都被破坏)。
|
||
if self.traceback_enabled:
|
||
raise RuntimeError(
|
||
f"--traceback: include 预编译缓存写入隔离目录失败: "
|
||
f"{precompiled_obj}: {_e}") from _e
|
||
if _config_mode == "strict":
|
||
_vlog().warning(f"预编译缓存写入失败: {_e}")
|
||
# 注册符号
|
||
self._register_compiled_include(src_path, sha1, ll_content, include_ModuleSha1Map)
|
||
# 清理临时文件(仅成功时清理,失败时保留以便调试)
|
||
# DEBUG: 保留 .ll 文件以便静态分析
|
||
# for ext in ['.ll', '_dso.ll']:
|
||
# p = ll_path[:-3] + ext
|
||
# if _CachedIsFile(p):
|
||
# os.remove(p)
|
||
else:
|
||
has_error = True
|
||
_vlog().warning(f" 保留失败文件以便调试: {ll_path}")
|
||
|
||
if has_error:
|
||
_vlog().error(f"\nincludes 文件编译失败")
|
||
sys.exit(1)
|
||
|
||
# 从 include 文件源码中提取函数参数名,供主项目编译时 keyword 参数匹配使用
|
||
self._extract_include_param_names(sorted_files)
|
||
|
||
def _extract_include_param_names_from_dirs(self) -> None:
|
||
"""从 include 目录的 Python 源码中提取函数参数名与默认值。
|
||
|
||
顶层 .vp/.py 直接解析;包子目录(如 includes/json)只在该包被
|
||
used_includes 引用时递归解析——避免为每个工程全量递归 includes
|
||
(fltk/vqt6/openssl 等上百个文件)带来的解析开销。
|
||
默认值一并注册(跨模块缺参调用点据此填充,见 HandlesExprCall
|
||
._HandleClosureCallLlvm),此前只注册参数名,导致 include 模块的
|
||
尾随默认参数在调用点被零值填充。
|
||
"""
|
||
for includes_dir in self.include_dirs:
|
||
if not os.path.isdir(includes_dir):
|
||
continue
|
||
for entry in os.listdir(includes_dir):
|
||
entry_path = os.path.join(includes_dir, entry)
|
||
targets: list[str] = []
|
||
if os.path.isdir(entry_path):
|
||
if not self.used_includes or entry not in self.used_includes:
|
||
continue
|
||
for root, dirs, files in os.walk(entry_path):
|
||
dirs[:] = [d for d in dirs if not d.startswith('.') and d != '__pycache__']
|
||
targets.extend(os.path.join(root, f) for f in files
|
||
if f.endswith('.vp') or f.endswith('.py'))
|
||
elif entry.endswith('.vp') or entry.endswith('.py'):
|
||
targets.append(entry_path)
|
||
for src_path in targets:
|
||
try:
|
||
source, tree = parse_python_file(src_path)
|
||
if tree is None:
|
||
continue
|
||
sha1 = compute_sha1(source)
|
||
self._collect_function_params(tree, sha1)
|
||
except Exception as e:
|
||
_vlog().warning(f" 提取参数名失败 {src_path}: {e}")
|
||
|
||
def _collect_function_params(self, tree: ast.AST, sha1: str | None) -> None:
|
||
"""单次递归遍历 AST 收集函数参数名(含类方法)与默认值,O(N) 复杂度
|
||
|
||
遍历时维护 current_class 上下文:遇到 ClassDef 进入类作用域,
|
||
遇到 FunctionDef 记录参数名并标注所属类,函数内部的嵌套定义
|
||
不再属于外层类。
|
||
|
||
默认值只登记非方法(模块级函数)的裸名键,减少与其它模块同名函数的
|
||
误配;带 sha1 / 类名限定的键始终登记,查找时精确键优先。
|
||
"""
|
||
def visit(node: ast.AST, current_class: str | None) -> None:
|
||
for child in ast.iter_child_nodes(node):
|
||
if isinstance(child, ast.ClassDef):
|
||
# 进入类作用域,递归处理类体
|
||
visit(child, child.name)
|
||
elif isinstance(child, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
||
func_name = child.name
|
||
param_names = [arg.arg for arg in child.args.args]
|
||
self.function_param_names[func_name] = param_names
|
||
if sha1:
|
||
self.function_param_names[f'{sha1}.{func_name}'] = param_names
|
||
if current_class:
|
||
self.function_param_names[f'{current_class}.{func_name}'] = param_names
|
||
if sha1:
|
||
self.function_param_names[f'{sha1}.{current_class}.{func_name}'] = param_names
|
||
defaults = getattr(child.args, 'defaults', []) or []
|
||
if defaults:
|
||
default_vals = []
|
||
for d in defaults:
|
||
if isinstance(d, ast.Constant):
|
||
default_vals.append(d.value)
|
||
else:
|
||
default_vals.append(None)
|
||
if sha1:
|
||
self.function_default_args[f'{sha1}.{func_name}'] = default_vals
|
||
if current_class:
|
||
self.function_default_args[f'{current_class}.{func_name}'] = default_vals
|
||
if sha1:
|
||
self.function_default_args[f'{sha1}.{current_class}.{func_name}'] = default_vals
|
||
else:
|
||
self.function_default_args[func_name] = default_vals
|
||
# 函数内部的嵌套定义不属于外层类
|
||
visit(child, None)
|
||
else:
|
||
visit(child, current_class)
|
||
visit(tree, None)
|
||
|
||
def _extract_app_default_args(self, need_translate: list) -> None:
|
||
"""从 App 源文件中提取函数默认参数,存入 function_default_args
|
||
|
||
在 Phase 2 并行编译开始前调用,确保跨模块调用参数检查能正确识别默认参数。
|
||
need_translate 中每个元素是 (i, src_path, out_path, sha1, current_ModuleSha1Map)
|
||
复用 parse_python_file 的 AST 缓存(run() 循环已 parse 过)。
|
||
"""
|
||
for entry in need_translate:
|
||
src_path = entry[1]
|
||
sha1 = entry[3]
|
||
_, tree = parse_python_file(src_path)
|
||
if tree is None:
|
||
continue
|
||
try:
|
||
for node in ast.walk(tree):
|
||
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
||
func_name = node.name
|
||
defaults = getattr(node.args, 'defaults', [])
|
||
if defaults:
|
||
default_vals = []
|
||
for d in defaults:
|
||
if isinstance(d, ast.Constant):
|
||
default_vals.append(d.value)
|
||
else:
|
||
default_vals.append(None)
|
||
# 存储多种键格式,确保跨模块调用检查能找到
|
||
self.function_default_args[func_name] = default_vals
|
||
self.function_default_args[f'{sha1}.{func_name}'] = default_vals
|
||
# 也更新参数名(预扫描时补充)
|
||
param_names = [arg.arg for arg in node.args.args]
|
||
if func_name not in self.function_param_names:
|
||
self.function_param_names[func_name] = param_names
|
||
if f'{sha1}.{func_name}' not in self.function_param_names:
|
||
self.function_param_names[f'{sha1}.{func_name}'] = param_names
|
||
except Exception as e:
|
||
_vlog().warning(f" 提取默认参数失败 {src_path}: {e}")
|
||
|
||
def _extract_include_param_names(self, sorted_files: list[str]) -> None:
|
||
"""从 include 文件的 Python 源码中提取函数参数名,存入 function_param_names
|
||
|
||
优化:复用 _collect_function_params 的 O(N) 单次遍历 + parse_python_file 缓存。
|
||
"""
|
||
for src_path in sorted_files:
|
||
if self._is_decl_only_file(src_path):
|
||
continue
|
||
try:
|
||
_, tree = parse_python_file(src_path)
|
||
if tree is None:
|
||
continue
|
||
module_name = os.path.splitext(os.path.basename(src_path))[0]
|
||
sha1 = self.include_py_map.get(module_name)
|
||
self._collect_function_params(tree, sha1)
|
||
except Exception as e:
|
||
_vlog().warning(f" 提取参数名失败 {src_path}: {e}")
|
||
|
||
def _register_compiled_include(self, src_path: str, sha1: str, ll_content: str, include_ModuleSha1Map: dict[str, str]) -> None:
|
||
"""编译 include 文件成功后,生成签名和 stub 文件并注册到符号表中"""
|
||
module_name = os.path.splitext(os.path.basename(src_path))[0]
|
||
|
||
# 确定完整模块名(如 vpsdk.window)
|
||
mod_full = module_name
|
||
for includes_dir in self.include_dirs:
|
||
try:
|
||
rel = os.path.relpath(src_path, includes_dir)
|
||
mod_full = os.path.splitext(rel)[0].replace(os.sep, '.').replace('/', '.')
|
||
break
|
||
except ValueError:
|
||
pass
|
||
|
||
# 1. 生成 .pyi 签名文件(如果还没有的话)
|
||
sig_path = os.path.join(self.temp_dir, f"{sha1}.vpi")
|
||
if not _CachedIsFile(sig_path):
|
||
# 全局缓存检查(不被 --clean 清除)
|
||
_TryGlobalCache(sha1, 'pyi', sig_path)
|
||
if not _CachedIsFile(sig_path):
|
||
try:
|
||
py_content = _ReadTextCached(src_path)
|
||
sig_content = PythonToStubConverter.convert(py_content, mod_full)
|
||
os.makedirs(self.temp_dir, exist_ok=True)
|
||
with open(sig_path, 'w', encoding='utf-8', newline='\n') as f:
|
||
f.write(sig_content)
|
||
_InvalidateTextCache(sig_path)
|
||
_SaveToGlobalCache(sha1, 'pyi', sig_path)
|
||
except Exception as e:
|
||
if _config_mode == "strict":
|
||
raise
|
||
_vlog().warning(f"签名生成失败不阻塞编译: {e}", "Exception")
|
||
|
||
# 注册到 sig_files 和 _source_module_sig_files
|
||
if _CachedIsFile(sig_path):
|
||
self.sig_files[sha1] = sig_path
|
||
self.sha1_map[sha1] = f"includes/{mod_full.replace('.', '/')}.vp"
|
||
if self._shared_source_module_sig_files is not None:
|
||
self._shared_source_module_sig_files[mod_full] = sig_path
|
||
self._shared_source_module_sig_files[module_name] = sig_path
|
||
|
||
# 将签名信息加载到共享符号表中
|
||
if self._shared_symbol_table is not None:
|
||
try:
|
||
self._shared_symbol_table.LoadModuleSymbols(sig_path, mod_full, lineno=0)
|
||
except Exception as _e:
|
||
if _config_mode == "strict":
|
||
raise
|
||
_vlog().warning(f"加载模块签名到共享符号表失败: {_e}", "Exception")
|
||
|
||
# 2. 生成 .stub.ll 文件
|
||
stub_path = os.path.join(self.output_dir, f"{sha1}.stub.ll")
|
||
if not _CachedIsFile(stub_path):
|
||
# 全局缓存检查(不被 --clean 清除)
|
||
_TryGlobalCache(sha1, 'stub.ll', stub_path)
|
||
if not _CachedIsFile(stub_path) and ll_content:
|
||
try:
|
||
stub_content, _ = self._split_ll(ll_content)
|
||
with open(stub_path, 'w', encoding='utf-8', newline='\n') as f:
|
||
f.write(stub_content)
|
||
_SaveToGlobalCache(sha1, 'stub.ll', stub_path)
|
||
except Exception as _e:
|
||
if _config_mode == "strict":
|
||
raise
|
||
_vlog().warning(f"生成 .stub.ll 文件失败: {_e}", "Exception")
|
||
|
||
# 注册到 stub_files
|
||
if _CachedIsFile(stub_path):
|
||
self.stub_files[sha1] = stub_path
|
||
# output/<sha1>.stub.ll 存在(本步刚落盘或从全局缓存拷入)→ 置脏,
|
||
# 让后续索引 miss 重扫到新增的 *.stub.ll(见 _build_stub_index 快路径)
|
||
from lib.core.Handles.HandlesImports import ImportHandle as _IH3
|
||
_IH3.MarkStubSetDirty()
|
||
elif _CachedIsFile(sig_path):
|
||
# 如果 stub 文件不存在,也尝试从 temp 目录查找
|
||
temp_stub = os.path.join(self.temp_dir, f"{sha1}.stub.ll")
|
||
if _CachedIsFile(temp_stub):
|
||
self.stub_files[sha1] = temp_stub
|
||
else:
|
||
# 最后尝试从全局缓存直接读取
|
||
_TryGlobalCache(sha1, 'stub.ll', temp_stub)
|
||
if _CachedIsFile(temp_stub):
|
||
self.stub_files[sha1] = temp_stub
|
||
|
||
def _precollect_inline_symbols(self) -> None:
|
||
"""在主翻译循环之前,扫描includes目录中used_includes相关的模块收集内联函数AST"""
|
||
if not self.used_includes:
|
||
return
|
||
for includes_dir in self.include_dirs:
|
||
if not os.path.isdir(includes_dir):
|
||
continue
|
||
for module_name in self.used_includes:
|
||
module_dir = os.path.join(includes_dir, module_name)
|
||
if os.path.isdir(module_dir):
|
||
for root, dirs, files in os.walk(module_dir):
|
||
dirs[:] = [d for d in dirs if not d.startswith('.') and d != '__pycache__']
|
||
for fname in files:
|
||
if fname.endswith('.vp') or fname.endswith('.py'):
|
||
src_path = os.path.join(root, fname)
|
||
self._collect_inline_symbols(src_path)
|
||
py_path = os.path.join(includes_dir, module_name + '.vp')
|
||
if not _CachedIsFile(py_path):
|
||
# 回退到 .py(向后兼容)
|
||
py_path = os.path.join(includes_dir, module_name + '.py')
|
||
if _CachedIsFile(py_path):
|
||
self._collect_inline_symbols(py_path)
|
||
for ef in self.extra_py_files:
|
||
self._collect_inline_symbols(ef)
|
||
|
||
def _collect_inline_symbols(self, src_path: str):
|
||
"""解析源文件,收集内联函数的AST信息到inline_func_symbols
|
||
|
||
复用 parse_python_file 的 AST 缓存,避免重复 open+read+ast.parse。
|
||
"""
|
||
try:
|
||
_, tree = parse_python_file(src_path)
|
||
if tree is None:
|
||
return
|
||
module_name = os.path.splitext(os.path.basename(src_path))[0]
|
||
for node in ast.iter_child_nodes(tree):
|
||
if isinstance(node, ast.FunctionDef) and node.returns:
|
||
# 新语法 `inline` 由解析器写入返回注解的 Markers。
|
||
ReturnMarkers: tuple[str, ...] = getattr(node.returns, 'Markers', None) or ()
|
||
is_inline: bool = 'inline' in ReturnMarkers
|
||
if isinstance(node.returns, ast.BinOp) and isinstance(node.returns.op, ast.BitOr):
|
||
_Line: int = getattr(node.returns, 'lineno', 0) or 0
|
||
raise TypeError(
|
||
f"[TPC] 不支持类型联合 'T1 | T2'(第 {_Line} 行);"
|
||
f"内联标记请写 `-> inline T`,类型注解只能是单一类型")
|
||
if is_inline:
|
||
func_name = node.name
|
||
sym_key = f"{module_name}.{func_name}"
|
||
info = CTypeInfo()
|
||
info.Name = func_name
|
||
info.IsFunction = True
|
||
info.IsInline = True
|
||
info.InlineBody = node.body
|
||
info.InlineParams = [arg.arg for arg in node.args.args]
|
||
info.Storage = TypeSystem.CInline()
|
||
self.inline_func_symbols[func_name] = info
|
||
self.inline_func_symbols[sym_key] = info
|
||
except TypeError:
|
||
# 类型注解 fail-fast(BinOp(BitOr) 联合)不得被 strict 开关吞掉
|
||
raise
|
||
except Exception as _e:
|
||
if _config_mode == "strict":
|
||
raise
|
||
_vlog().warning(f"预收集内联函数符号失败: {_e}", "Exception")
|
||
|
||
def _build_shared_symbol_data(self) -> None:
|
||
"""一次性构建所有文件共享的符号表数据,避免每个文件重复加载"""
|
||
t0 = time.time()
|
||
|
||
_vlog().info("[缓存] 构建共享符号表数据...")
|
||
|
||
base_trans = TransPyC.TransPyC(code="pass", triple=self.triple, datalayout=self.datalayout)
|
||
base_trans.translator.LlvmGen = None
|
||
base_trans.translator._ModuleSha1Map = {}
|
||
base_trans.translator._global_function_default_args = self.function_default_args
|
||
base_trans.translator._global_function_param_names = self.function_param_names
|
||
|
||
# ---- 两阶段加载:先收集全部模块,再「①声明」全跑完 / 「②签名」全跑完 ----
|
||
# 声明阶段把类/枚举/typedef 灌满符号表,签名阶段才解析函数签名 ⇒ 模块间
|
||
# 类型引用(`from X import *` 的裸名如 posix 的 `INT`、跨模块类如
|
||
# `rawptr[Constant]`)不再依赖 os.listdir 的字典序(此前 posix 排在 stdint
|
||
# 之前 ⇒ `INT` 解析不到,旧口径静默降级成 i32,fail-fast 后必须消除)。
|
||
staged: list[tuple[str, str]] = [] # (声明文件路径, 模块名)
|
||
# 需要重导出的包(在全部模块符号加载完后统一处理;提到循环外避免多个
|
||
# include_dir 时被反复覆盖/重复收集)
|
||
_pending_reexports: list[tuple[str, str]] = []
|
||
for includes_dir in self.include_dirs:
|
||
if os.path.isdir(includes_dir):
|
||
for pyi_file in os.listdir(includes_dir):
|
||
if pyi_file.endswith('.vpi') or pyi_file.endswith('.pyi'):
|
||
pyi_path = os.path.join(includes_dir, pyi_file)
|
||
module_name = os.path.splitext(pyi_file)[0]
|
||
staged.append((pyi_path, module_name))
|
||
for py_file in os.listdir(includes_dir):
|
||
if (py_file.endswith('.vp') or py_file.endswith('.py')) and (not py_file.startswith('_') or py_file == '_list.vp' or py_file == '_list.py' or py_file == '_dict.vp' or py_file == '_dict.py'):
|
||
module_name = os.path.splitext(py_file)[0]
|
||
sha1_key = self.include_py_map.get(module_name)
|
||
if sha1_key and sha1_key in self.sig_files:
|
||
staged.append((self.sig_files[sha1_key], module_name))
|
||
# 收集需要重导出的包,等所有模块符号加载完后再处理
|
||
for sha1_key, sig_path in self.sig_files.items():
|
||
rel_path = self.sha1_map.get(sha1_key, sha1_key)
|
||
if rel_path.startswith('includes/'):
|
||
mod_name = os.path.splitext(rel_path)[0].replace(os.sep, '.').replace('/', '.')
|
||
mod_name = mod_name[len('includes/'):]
|
||
_entry_mod: tuple[str, str] = (sig_path, mod_name)
|
||
if _entry_mod not in staged:
|
||
staged.append(_entry_mod)
|
||
if mod_name.endswith('.__init__'):
|
||
package_name = mod_name[:-len('.__init__')]
|
||
_entry_pkg: tuple[str, str] = (sig_path, package_name)
|
||
if _entry_pkg not in _pending_reexports:
|
||
_pending_reexports.append(_entry_pkg)
|
||
|
||
for sha1_key, sig_path in self.sig_files.items():
|
||
rel_path = self.sha1_map.get(sha1_key, sha1_key)
|
||
if rel_path.startswith('includes/'):
|
||
continue
|
||
module_name = os.path.splitext(rel_path)[0].replace(os.sep, '.').replace('/', '.')
|
||
staged.append((sig_path, module_name))
|
||
base_trans.translator._source_module_sig_files[module_name] = sig_path
|
||
short_name = module_name.split('.')[-1] if '.' in module_name else module_name
|
||
if short_name != module_name:
|
||
base_trans.translator._source_module_sig_files[short_name] = sig_path
|
||
|
||
# ① 声明阶段(全部模块;多轮直到"本轮没有新增可解析的声明")
|
||
# 单轮仍会与模块顺序有关:typedef 右侧可以引用**其它模块**的 typedef,
|
||
# 例 win32base 的 `typedef HANDLE = VOIDPTR`,VOIDPTR 在 stdint
|
||
# ⇒ 本模块先 stage 时右侧解析为空;下一轮把它重 stage 一遍(重 stage 覆盖
|
||
# 登记,条目随依赖入表而变完整)。无进展即停,最多再跑一轮。
|
||
_stage_map: dict[tuple[str, str], ModuleSymbols] = {}
|
||
_round_pending = list(staged)
|
||
_prev_miss: int | None = None
|
||
while _round_pending:
|
||
_miss_total: int = 0
|
||
_next_round: list[tuple[str, str]] = []
|
||
for _stage_path, _stage_mod in _round_pending:
|
||
_stage_ms = base_trans.translator.SymbolTable.StageModuleSymbols(_stage_path, _stage_mod)
|
||
if _stage_ms is None:
|
||
continue
|
||
_stage_map[(_stage_path, _stage_mod)] = _stage_ms
|
||
_miss: int = _CountUnresolvedTypedefs(_stage_ms)
|
||
_miss_total += _miss
|
||
if _miss:
|
||
_next_round.append((_stage_path, _stage_mod))
|
||
if not _next_round or (_prev_miss is not None and _miss_total >= _prev_miss):
|
||
break
|
||
_prev_miss = _miss_total
|
||
_round_pending = _next_round
|
||
|
||
# ② 签名阶段(全部模块;此时任一模块的签名都能看到全部声明)
|
||
for (_commit_path, _commit_mod), _commit_ms in _stage_map.items():
|
||
base_trans.translator.SymbolTable.CommitModuleSymbols(_commit_ms, _commit_mod, _commit_path)
|
||
|
||
# 所有 includes 模块符号已加载,现在处理包重导出
|
||
for sig_path, package_name in _pending_reexports:
|
||
self._register_package_reexports(base_trans, sig_path, package_name)
|
||
|
||
all_dc = {}
|
||
all_dt: dict[str, tuple[int, bool]] = {}
|
||
for sha1_key, stub_path in self.stub_files.items():
|
||
if os.path.exists(stub_path):
|
||
try:
|
||
stub_content = _ReadTextCached(stub_path)
|
||
for line in stub_content.splitlines():
|
||
line = line.strip()
|
||
if line.startswith('@') and '= global' in line and 'i32' in line and not line.startswith('@llvm'):
|
||
var_name = line.split('=')[0].strip().lstrip('@')
|
||
val_part = line.split('i32')[-1].strip().rstrip(']').lstrip('[').lstrip('i32')
|
||
try:
|
||
val = int(val_part)
|
||
all_dc[var_name] = val
|
||
except Exception as e:
|
||
_vlog().warning(f"Phase2Translator: 忽略异常 {e}", exc_info=e)
|
||
except Exception as e:
|
||
_vlog().warning(f"Phase2Translator: 忽略异常 {e}", exc_info=e)
|
||
|
||
for sha1_key, pyi_path in self.sig_files.items():
|
||
_ExtractCDefineConstantsFromPyi(pyi_path, all_dc, base_trans.translator.SymbolTable, all_dt)
|
||
|
||
export_extern_funcs = {}
|
||
for sha1_key, sig_path in self.sig_files.items():
|
||
try:
|
||
# 复用 parse_python_file 的 AST 缓存(sig 文件被多处解析)
|
||
_, sig_tree = parse_python_file(sig_path)
|
||
if sig_tree is None:
|
||
continue
|
||
for node in ast.iter_child_nodes(sig_tree):
|
||
if isinstance(node, ast.FunctionDef):
|
||
if _IsExportExternFuncNode(node):
|
||
export_extern_funcs[node.name] = sha1_key
|
||
except Exception as _e:
|
||
if _config_mode == "strict":
|
||
raise
|
||
_vlog().warning(f"收集共享导出外部函数失败: {_e}", "Exception")
|
||
|
||
for sym_name, sym_info in self.inline_func_symbols.items():
|
||
if sym_name not in base_trans.translator.SymbolTable:
|
||
base_trans.translator.SymbolTable[sym_name] = sym_info
|
||
else:
|
||
existing = base_trans.translator.SymbolTable[sym_name]
|
||
if not getattr(existing, 'InlineBody', None) and getattr(sym_info, 'InlineBody', None):
|
||
existing.IsInline = sym_info.IsInline
|
||
existing.InlineBody = sym_info.InlineBody
|
||
existing.InlineParams = sym_info.InlineParams
|
||
|
||
self._shared_symbol_table = base_trans.translator.SymbolTable
|
||
self._shared_source_module_sig_files = dict(base_trans.translator._source_module_sig_files)
|
||
self._shared_all_dc = all_dc
|
||
self._shared_all_decl_types = all_dt
|
||
self._shared_export_extern_funcs = export_extern_funcs
|
||
|
||
generic_class_templates = {}
|
||
for includes_dir in self.include_dirs:
|
||
if os.path.isdir(includes_dir):
|
||
abs_includes = os.path.join(self.src_root, includes_dir) if not os.path.isabs(includes_dir) else includes_dir
|
||
if not os.path.isdir(abs_includes):
|
||
abs_includes = includes_dir
|
||
for py_file in os.listdir(abs_includes):
|
||
if (py_file.endswith('.vp') or py_file.endswith('.py')) and (not py_file.startswith('_') or py_file == '_list.vp' or py_file == '_list.py' or py_file == '_dict.vp' or py_file == '_dict.py'):
|
||
py_path = os.path.join(abs_includes, py_file)
|
||
try:
|
||
# 复用 parse_python_file 的 AST 缓存
|
||
py_content, py_tree = parse_python_file(py_path)
|
||
if py_tree is None:
|
||
continue
|
||
# 计算 include 文件的 SHA1,供特化时统一命名空间
|
||
py_sha1: str = compute_sha1(py_content)
|
||
for node in py_tree.body:
|
||
if isinstance(node, ast.ClassDef) and hasattr(node, 'type_params') and node.type_params:
|
||
type_params = [tp.name for tp in node.type_params]
|
||
generic_class_templates[node.name] = {
|
||
'node': node,
|
||
'type_params': type_params,
|
||
'sha1': py_sha1,
|
||
}
|
||
except Exception as _e:
|
||
if _config_mode == "strict":
|
||
raise
|
||
_vlog().warning(f"收集泛型类模板失败: {_e}", "Exception")
|
||
self._shared_generic_class_templates = generic_class_templates
|
||
|
||
# 加载所有 {sha1}.doc.json 到 _shared_doc_meta,供跨模块 __doc__ 查询
|
||
shared_doc_meta: dict[str, dict[str, str]] = {}
|
||
if os.path.isdir(self.temp_dir):
|
||
for fname in os.listdir(self.temp_dir):
|
||
if fname.endswith('.doc.json'):
|
||
sha1_key = fname[:-len('.doc.json')]
|
||
try:
|
||
with open(os.path.join(self.temp_dir, fname), 'r', encoding='utf-8') as f:
|
||
data = json.load(f)
|
||
if isinstance(data, dict):
|
||
shared_doc_meta[sha1_key] = {str(k): str(v) for k, v in data.items()}
|
||
except Exception as _e:
|
||
if _config_mode == "strict":
|
||
raise
|
||
_vlog().warning(f"加载 {fname} 失败: {_e}", "Exception")
|
||
# 也从全局缓存加载 .doc.json(temp_dir 被 --clean 清空时补充)
|
||
if os.path.isdir(_GLOBAL_CACHE_DIR):
|
||
for fname in os.listdir(_GLOBAL_CACHE_DIR):
|
||
if fname.endswith('.doc.json'):
|
||
sha1_key = fname[:-len('.doc.json')]
|
||
if sha1_key in shared_doc_meta:
|
||
continue
|
||
try:
|
||
data = json.loads(_ReadTextCached(os.path.join(_GLOBAL_CACHE_DIR, fname)))
|
||
if isinstance(data, dict):
|
||
shared_doc_meta[sha1_key] = {str(k): str(v) for k, v in data.items()}
|
||
except Exception as _e:
|
||
if _config_mode == "strict":
|
||
raise
|
||
_vlog().warning(f"加载全局缓存 {fname} 失败: {_e}", "Exception")
|
||
self._shared_doc_meta = shared_doc_meta
|
||
|
||
t1 = time.time()
|
||
_vlog().info(f"[缓存] 共享符号表构建完成 ({t1-t0:.2f}s, {len(self._shared_symbol_table)} 符号, {len(generic_class_templates)} 泛型模板, {len(shared_doc_meta)} doc 元数据)")
|
||
|
||
def _compile_include_sources(self):
|
||
"""扫描项目目录中的汇编存根,编译 includes 目录中发现的 C/C++ 源文件"""
|
||
extra_sources = list(self.extra_compile_files)
|
||
scan_dirs = []
|
||
if self.src_root and os.path.isdir(self.src_root):
|
||
scan_dirs.append(self.src_root)
|
||
# 不再扫描 output_dir 的父目录:当 output_dir 位于顶层时,
|
||
# 父目录是整个仓库根,会误扫到 ViperOS/includes/vqt6 等无关 C/C++ 源文件。
|
||
# 项目特定的 C/汇编文件应放在 src_root 下,或通过 extra_compile_files 指定。
|
||
for scan_dir in scan_dirs:
|
||
for root, dirs, files in os.walk(scan_dir):
|
||
dirs[:] = [d for d in dirs if not d.startswith('.') and d != '__pycache__']
|
||
for fname in files:
|
||
if fname.endswith(('.S', '.s', '.c', '.cpp')):
|
||
if fname.startswith('test_'):
|
||
continue
|
||
fpath = os.path.join(root, fname)
|
||
if fpath not in extra_sources:
|
||
extra_sources.append(fpath)
|
||
if not extra_sources:
|
||
return
|
||
|
||
cc_map = {
|
||
'.c': 'gcc',
|
||
'.cpp': 'g++', '.cc': 'g++', '.cxx': 'g++',
|
||
'.m': 'clang', '.mm': 'clang++',
|
||
'.s': 'gcc', '.S': 'gcc',
|
||
}
|
||
|
||
_vlog().info(f"\n[includes 编译] 找到 {len(extra_sources)} 个源文件")
|
||
|
||
for src_path in extra_sources:
|
||
_, ext = os.path.splitext(src_path)
|
||
ext = ext.lower()
|
||
obj_path = os.path.join(self.output_dir, os.path.splitext(os.path.basename(src_path))[0] + '.obj')
|
||
if ext in ('.s', '.S') and any('oformat' in f for f in self.linker_flags):
|
||
cc = 'clang'
|
||
compile_src = src_path
|
||
extra_flags = ['-c', '-target', 'x86_64-none-elf', '-o', obj_path, compile_src]
|
||
else:
|
||
cc = cc_map.get(ext, 'gcc')
|
||
extra_flags = ['-c', '-o', obj_path, src_path]
|
||
|
||
_vlog().info(f" 编译: {os.path.basename(src_path)} -> {os.path.basename(obj_path)}")
|
||
|
||
try:
|
||
cmd = [cc] + extra_flags
|
||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||
if result.returncode == 0:
|
||
_vlog().success(" 编译成功")
|
||
self.extra_link_files.append(obj_path)
|
||
else:
|
||
_vlog().error(f" 编译失败: {result.stderr.strip()}")
|
||
_vlog().error(f"\nC/C++ 源文件编译失败,立即终止编译。")
|
||
sys.exit(1)
|
||
except FileNotFoundError:
|
||
_vlog().error(f" 找不到编译器: {cc}")
|
||
_vlog().error(f"\n找不到编译器,立即终止编译。")
|
||
sys.exit(1)
|
||
except Exception as e:
|
||
_vlog().error(f" {e}")
|
||
_vlog().error(f"\n编译异常,立即终止编译。")
|
||
sys.exit(1)
|
||
|
||
def _strip_debug_sections(self, exe_path: str) -> None:
|
||
"""剥离 .exe 中的调试段(跳过裸机二进制)"""
|
||
if not os.path.exists(exe_path):
|
||
return
|
||
ext = os.path.splitext(exe_path)[1].lower()
|
||
if ext not in ('.exe', '.dll', '.obj', '.o', '.elf'):
|
||
_vlog().info(f" [剥离] 跳过({ext} 格式无需剥离)")
|
||
return
|
||
for tool in ['llvm-strip', 'strip']:
|
||
strip_cmd = shutil.which(tool)
|
||
if strip_cmd:
|
||
try:
|
||
result = subprocess.run(
|
||
[strip_cmd, '--strip-debug', exe_path],
|
||
capture_output=True, text=True
|
||
)
|
||
if result.returncode == 0:
|
||
_vlog().info(f" [剥离] 调试段已移除 ({tool})")
|
||
else:
|
||
_vlog().warning(f" [剥离] 警告: {result.stderr.strip()}")
|
||
except Exception as _e:
|
||
if _config_mode == "strict":
|
||
raise
|
||
_vlog().warning(f"剥离可执行文件调试段失败: {_e}", "Exception")
|
||
break
|
||
|
||
def _WriteStackMap(self, exe_path: str) -> None:
|
||
"""--traceback:链接成功后导出"地址→源行"旁文件 <exe>.stackmap。
|
||
|
||
用 llvm-dwarfdump 解析最终 PE 的 .debug_line(地址→文件/行)与
|
||
.debug_info(DISubprogram 的 low/high_pc + DW_AT_name → 函数名),
|
||
换算为 RVA 后落盘。旁文件规格(全部小端 little-endian):
|
||
|
||
头部 20 字节 = magic(4)="TPCS" + version(u32=1) + entry_count(u32)
|
||
+ str_count(u32) + str_bytes(u32)
|
||
记录 entry_count × 20 字节(按 rva 升序):
|
||
rva(u64) + file_off(u32) + line(u32) + func_off(u32)
|
||
字符串池 str_bytes 字节(紧接记录区,UTF-8,全部以 '\\0' 结尾)
|
||
|
||
口径(必须与运行期严格一致):运行期 traceback.vp 取
|
||
`rva = 帧地址 − GetModuleHandleA(NULL)`,即"链接期 ImageBase 之上的偏移",
|
||
故写端唯一正确口径是 **`RVA = dwarfdump 打印的地址 − PE.ImageBase`**。
|
||
|
||
⚠ 实测 viperc.exe 的 DWARF 里同时存在两个地址域:
|
||
* 绝大多数行/子程序地址是**已重定位的 VA**(0x14000xxxx,含 ImageBase);
|
||
* 另有 22638 条行记录 / 756 个 subprogram 的地址**恰为 0**——它们是**被
|
||
链接器按 COMDAT 去重丢弃的泛型实例副本**(实测 36 个 `list[T].*` 名字,
|
||
每个都有 1 份 VA 副本 + 15~21 份 0 地址副本),并非解析错误。
|
||
旧实现按"整段最小地址是否 >= ImageBase"**整表**判定:只要有一行 0,就整表都不
|
||
减基址 ⇒ 全部 VA 行留在 0x140000000+ 域、与运行期 RVA 域相差一个 ImageBase,
|
||
运行期下界查找对任意帧都落到同一条记录上(用户实测所有帧都显示同一行)。
|
||
现改为**逐条**归一化:地址落在映像 VA 区间 [ImageBase, ImageBase+SizeOfImage)
|
||
的记录保留(RVA = 地址 − ImageBase);其余(未重定位的 0、越界值)一律**丢弃并
|
||
统计**(不静默留垃圾条目)。同一 RVA 只保留行号最大的一条。
|
||
|
||
写出前自检(任一不满足即 fail-fast 抛异常,遵循 --traceback 既有风格):
|
||
记录数 / min~max RVA 与 SizeOfImage 的关系 / 所有 RVA ∈ [0, SizeOfImage) /
|
||
RVA 严格升序 / 至少若干条落在运行期可能出现的 RVA 区间
|
||
(粗判据 [.text 节 RVA, SizeOfImage))。
|
||
|
||
任何一步失败都直接抛异常终止编译(--traceback 的硬要求,无兜底)。
|
||
"""
|
||
import struct
|
||
import bisect
|
||
|
||
def _Fail(msg: str) -> None:
|
||
full: str = msg + '(这是 --traceback 的要求)'
|
||
_vlog().error(full)
|
||
raise RuntimeError(full)
|
||
|
||
layout = _ReadPEImageLayout(exe_path)
|
||
if layout is None:
|
||
_Fail(f"--traceback 只支持 Windows PE 产物,但 {exe_path} 不是有效 PE")
|
||
image_base, size_of_image, sections = layout
|
||
if size_of_image <= 0:
|
||
_Fail(f"--traceback 读到的 PE.SizeOfImage 非正({size_of_image}),无法界定 RVA 域")
|
||
img_lo: int = image_base
|
||
img_hi: int = image_base + size_of_image
|
||
# 运行期可能出现的 RVA 区间的**粗判据**下界:.text 节 RVA(缺失时退化为最小节 RVA)
|
||
text_rva: int = min((v[0] for v in sections.values()), default=0x1000)
|
||
if '.text' in sections:
|
||
text_rva = sections['.text'][0]
|
||
max_sec_end: int = max((v[0] + v[1] for v in sections.values()), default=size_of_image)
|
||
|
||
dwarfdump: str | None = shutil.which('llvm-dwarfdump') or shutil.which('llvm-dwarfdump.exe')
|
||
if not dwarfdump:
|
||
_Fail("--traceback 找不到 llvm-dwarfdump(请确认 PATH 内含 LLVM 工具)")
|
||
|
||
try:
|
||
proc = subprocess.run(
|
||
[dwarfdump, '--debug-line', '--debug-info', exe_path],
|
||
capture_output=True)
|
||
except Exception as e:
|
||
_Fail(f"--traceback 调用 llvm-dwarfdump 失败: {e}")
|
||
if proc.returncode != 0:
|
||
_Fail(f"--traceback 的 llvm-dwarfdump 返回 {proc.returncode}: "
|
||
+ proc.stderr.decode('utf-8', errors='replace').strip())
|
||
dump_text: str = proc.stdout.decode('utf-8', errors='replace')
|
||
|
||
rows: list[tuple[int, str, int]] = _ParseDwarfLineRows(dump_text)
|
||
if not rows:
|
||
_Fail(f"--traceback 未能从 {exe_path} 解析出任何 DWARF 行记录(.debug_line 为空?)")
|
||
|
||
# ---- 行记录:逐条归一化为"运行期 RVA";不能归一的一律丢弃并统计 ----
|
||
dropped_rows: int = 0
|
||
dropped_row_samples: list[int] = []
|
||
best: dict[int, tuple[str, int]] = {}
|
||
for addr, path, line in rows:
|
||
if not (img_lo <= addr < img_hi):
|
||
dropped_rows += 1
|
||
if len(dropped_row_samples) < 5:
|
||
dropped_row_samples.append(addr)
|
||
continue
|
||
rva: int = addr - image_base
|
||
cur = best.get(rva)
|
||
if cur is None or line > cur[1]:
|
||
best[rva] = (path, line)
|
||
if dropped_rows:
|
||
_vlog().warning(
|
||
f" [--traceback] 丢弃 {dropped_rows} 条无法归一化的 DWARF 行记录"
|
||
f"(地址不在映像 VA 区间 [0x{img_lo:x}, 0x{img_hi:x}) 内;判定原因:多为被连接器"
|
||
f" COMDAT 去重丢弃的泛型副本,其地址恰为 0)示例地址: "
|
||
+ ", ".join(f"0x{a:x}" for a in dropped_row_samples))
|
||
if not best:
|
||
_Fail(f"--traceback 从 {exe_path} 得到的行记录全部无法归一到运行期 RVA 域")
|
||
|
||
# ---- 子程序(函数名):同样逐条归一化;不能归一的丢弃 ----
|
||
funcs: list[tuple[int, int, str]] = _ParseDwarfSubprograms(dump_text)
|
||
func_ranges: list[tuple[int, int, str]] = []
|
||
dropped_funcs: int = 0
|
||
for low, high, name in funcs:
|
||
if not (img_lo <= low < img_hi):
|
||
dropped_funcs += 1
|
||
continue
|
||
lo_rva: int = low - image_base
|
||
hi_rva: int = high - image_base
|
||
if hi_rva <= lo_rva:
|
||
hi_rva = lo_rva + 1
|
||
if hi_rva > size_of_image:
|
||
hi_rva = size_of_image
|
||
func_ranges.append((lo_rva, hi_rva, name))
|
||
if dropped_funcs:
|
||
_vlog().warning(
|
||
f" [--traceback] 丢弃 {dropped_funcs} 个无法归一化的 DWARF 子程序"
|
||
f"(low_pc 不在映像 VA 区间 [0x{img_lo:x}, 0x{img_hi:x}) 内;同上为去重副本)")
|
||
func_ranges.sort(key=lambda f: f[0])
|
||
func_lows: list[int] = [f[0] for f in func_ranges]
|
||
|
||
def _FuncNameFor(rva: int) -> str | None:
|
||
i: int = bisect.bisect_right(func_lows, rva) - 1
|
||
if i >= 0:
|
||
low, high, name = func_ranges[i]
|
||
if low <= rva < high:
|
||
return name
|
||
return None
|
||
|
||
# 字符串池:偏移 0 固定为一个空串('\0')哨兵,使 func_off=0 可无歧义表示
|
||
# "无函数名";真实串偏移恒 >= 1。
|
||
pool: bytearray = bytearray(b'\x00')
|
||
str_offsets: dict[str, int] = {'': 0}
|
||
|
||
def _Intern(s: str) -> int:
|
||
off: int | None = str_offsets.get(s)
|
||
if off is None:
|
||
off = len(pool)
|
||
pool.extend(s.encode('utf-8'))
|
||
pool.append(0)
|
||
str_offsets[s] = off
|
||
return off
|
||
|
||
records: list[tuple[int, int, int, int]] = []
|
||
for rva in sorted(best.keys()):
|
||
path, line = best[rva]
|
||
name = _FuncNameFor(rva)
|
||
records.append((rva, _Intern(path), line, _Intern(name) if name else 0))
|
||
|
||
# ---- 写端自检(fail-fast;与运行期 LoadMapFile 的校验口径一一对应)----
|
||
rvas: list[int] = [r[0] for r in records]
|
||
min_rva: int = rvas[0]
|
||
max_rva: int = rvas[-1]
|
||
for i in range(1, len(rvas)):
|
||
if rvas[i] <= rvas[i - 1]:
|
||
_Fail(f"--traceback 自检失败:记录 RVA 非严格升序"
|
||
f"(第 {i} 条 0x{rvas[i]:x} <= 前一条 0x{rvas[i - 1]:x},会破坏运行期二分查找)")
|
||
outside: int = sum(1 for r in rvas if not (0 <= r < size_of_image))
|
||
if outside:
|
||
_Fail(f"--traceback 自检失败:有 {outside} 条记录的 RVA 落在 [0, SizeOfImage=0x{size_of_image:x}) 之外")
|
||
# 运行期可能出现的 RVA 区间(粗判据):[.text 节 RVA, SizeOfImage)。一条都没有 ⇒ 口径又错了。
|
||
plausible: int = sum(1 for r in rvas if text_rva <= r < size_of_image)
|
||
if plausible <= 0:
|
||
_Fail(f"--traceback 自检失败:没有任何记录的 RVA 落在运行期可能出现的区间"
|
||
f" [0x{text_rva:x}(.text RVA), 0x{size_of_image:x}(SizeOfImage)) 内"
|
||
f"(RVA 口径疑似又错了)")
|
||
_vlog().info(
|
||
f" [--traceback] 自检: 记录={len(records)} RVA范围=[0x{min_rva:x}, 0x{max_rva:x}]"
|
||
f" PE.SizeOfImage=0x{size_of_image:x}({size_of_image})"
|
||
f" 各节最高结束 RVA=0x{max_sec_end:x}"
|
||
f" | 全部∈[0,SizeOfImage)=是 严格升序=是"
|
||
f" | 落在[.text 0x{text_rva:x},SizeOfImage)内={plausible} 条")
|
||
|
||
out: bytearray = bytearray()
|
||
out.extend(b'TPCS')
|
||
out.extend(struct.pack('<IIII', 1, len(records), len(str_offsets), len(pool)))
|
||
for rva, file_off, line, func_off in records:
|
||
out.extend(struct.pack('<QIII', rva, file_off, line, func_off))
|
||
out.extend(pool)
|
||
|
||
map_path: str = exe_path + '.stackmap'
|
||
try:
|
||
with open(map_path, 'wb') as f:
|
||
f.write(out)
|
||
except Exception as e:
|
||
_Fail(f"--traceback 写出旁文件失败 {map_path}: {e}")
|
||
_vlog().success(
|
||
f" [--traceback] 已写出地址→源行旁文件: {map_path}"
|
||
f"({len(records)} 条记录,{len(out)} 字节)")
|
||
|
||
def _link_obj_files(self, active_sha1s: set[str]) -> None:
|
||
"""将 active_sha1s 对应的 .obj 文件和 includes 库文件链接为可执行文件"""
|
||
obj_files = []
|
||
merged_obj = os.path.join(self.output_dir, '_merged.obj')
|
||
has_merged: bool = _CachedIsFile(merged_obj)
|
||
if has_merged:
|
||
# _merged.obj 只合并了非 include 模块,include 的 .obj 仍需单独链接
|
||
obj_files.append(merged_obj)
|
||
for root, dirs, files in os.walk(self.output_dir):
|
||
for file in files:
|
||
if file.endswith('.obj'):
|
||
sha1 = file[:-4]
|
||
if sha1 not in active_sha1s:
|
||
continue
|
||
if has_merged and sha1 not in self._include_sha1s:
|
||
continue
|
||
if not has_merged and sha1 in self._include_sha1s:
|
||
continue
|
||
obj_files.append(os.path.join(root, file))
|
||
|
||
if not obj_files and not self.extra_link_files:
|
||
_vlog().info("[链接] 无文件可链接")
|
||
return
|
||
|
||
_vlog().info(f"\n[链接] 找到 {len(obj_files)} 个 .obj 文件")
|
||
|
||
all_link_files = obj_files + self.extra_link_files
|
||
seen = set()
|
||
all_link_files = [f for f in all_link_files if not (f in seen or seen.add(f))]
|
||
if self.extra_link_files:
|
||
_vlog().info(f"[链接] 额外库文件: {len(self.extra_link_files)} 个")
|
||
for lib in self.extra_link_files:
|
||
_vlog().info(f" - {os.path.basename(lib)}")
|
||
|
||
if not self.linker_cmd:
|
||
_vlog().info("[链接] 未配置链接器")
|
||
return
|
||
|
||
exe_path = os.path.join(self.output_dir, self.linker_output) if self.linker_output else os.path.join(self.output_dir, 'a.exe')
|
||
is_binary = self.linker_output and self.linker_output.endswith('.bin')
|
||
|
||
# 检查是否直接输出二进制(使用 --oformat binary / -oformat binary 标志)
|
||
is_direct_binary = is_binary and any('oformat' in f for f in self.linker_flags)
|
||
|
||
if is_binary and not is_direct_binary:
|
||
link_output = exe_path[:-4] + '_pe.exe'
|
||
else:
|
||
link_output = exe_path
|
||
|
||
# 自动复制或生成 linker.ld 到输出目录
|
||
linker_ld_src = os.path.join(os.path.dirname(self.output_dir), 'linker.ld')
|
||
linker_ld_dst = os.path.join(self.output_dir, 'linker.ld')
|
||
if _CachedIsFile(linker_ld_src) and not _CachedIsFile(linker_ld_dst):
|
||
shutil.copy2(linker_ld_src, linker_ld_dst)
|
||
_vlog().info(f" [链接] 使用链接脚本: linker.ld")
|
||
elif not _CachedIsFile(linker_ld_dst) and any('-T' in f or '-T' in f.replace(',', ' ') for f in self.linker_flags):
|
||
default_ld = """ENTRY(_start)
|
||
|
||
SECTIONS {
|
||
. = 0x100000;
|
||
|
||
.text : {
|
||
*(.text)
|
||
*(.text.*)
|
||
}
|
||
|
||
.rodata : {
|
||
*(.rodata)
|
||
*(.rodata.*)
|
||
}
|
||
|
||
.data : {
|
||
*(.data)
|
||
*(.data.*)
|
||
}
|
||
|
||
.bss : {
|
||
*(.bss)
|
||
*(.bss.*)
|
||
}
|
||
|
||
/DISCARD/ : {
|
||
*(.comment)
|
||
*(.note)
|
||
*(.eh_frame)
|
||
*(.eh_frame_hdr)
|
||
*(.reloc)
|
||
*(.rela)
|
||
*(.debug*)
|
||
}
|
||
}
|
||
"""
|
||
with open(linker_ld_dst, 'w', encoding='utf-8', newline='\n') as f:
|
||
f.write(default_ld)
|
||
_InvalidateTextCache(linker_ld_dst)
|
||
_vlog().info(f" [链接] 自动创建默认链接脚本: linker.ld")
|
||
|
||
try:
|
||
# 将 linker_flags 拆分为非库标志和库标志(-l/-L),库标志放在目标文件之后
|
||
# 这样 Unix 风格链接器才能正确解析库中的符号
|
||
# -Wl, 选项是链接器全局选项,放在目标文件之前
|
||
non_lib_flags = []
|
||
lib_flags = []
|
||
i = 0
|
||
while i < len(self.linker_flags):
|
||
flag = self.linker_flags[i]
|
||
if flag in ('-l', '-L') and i + 1 < len(self.linker_flags):
|
||
# -l xxx / -L xxx 分开的形式
|
||
lib_flags.extend([flag, self.linker_flags[i + 1]])
|
||
i += 2
|
||
elif flag.startswith('-l') or flag.startswith('-L'):
|
||
# -lxxx / -Lxxx 合并的形式
|
||
lib_flags.append(flag)
|
||
i += 1
|
||
else:
|
||
# 其他所有标志(包括 -Wl, 选项)放在目标文件之前
|
||
non_lib_flags.append(flag)
|
||
i += 1
|
||
# ASan 模式:链接 ASan 运行库(配合各 .ll 的 opt -passes=asan 插桩)
|
||
# 不能传 -fsanitize=address:那会让 clang++ 驱动去找
|
||
# <resource-dir>/lib/x86_64-w64-windows-gnu/libclang_rt.asan_dynamic.dll.a,
|
||
# 而该目录是空的(上游 LLVM 不为这个 triple 构建 ASan runtime),
|
||
# 直接报 "could not open ... No such file or directory"。
|
||
# 改为显式链接 clang64 的那两份(与 VPC / README §8.1 同口径)。
|
||
# 它们是库,必须排在目标文件之后,所以追加到 lib_flags 而不是 non_lib_flags。
|
||
if self.asan_mode:
|
||
if self.asan_rt_dir is None:
|
||
_vlog().error(
|
||
"ASan 模式在 PATH 里找不到 clang64 的 ASan 运行库目录 "
|
||
"(<MSYS2>\\clang64\\lib\\clang\\<ver>\\lib\\windows\\"
|
||
"libclang_rt.asan_dynamic-x86_64.dll.a)\n"
|
||
" 请确认 msys2 的 mingw-w64-clang-x86_64-compiler-rt 已安装,"
|
||
"且 <MSYS2>\\clang64(或其 bin)已在 PATH 中。")
|
||
sys.exit(1)
|
||
lib_flags.append('-L' + self.asan_rt_dir)
|
||
lib_flags.append('-l:libclang_rt.asan_dynamic-x86_64.dll.a')
|
||
lib_flags.append('-l:libclang_rt.asan_dynamic_runtime_thunk-x86_64.a')
|
||
# Ctrl+C 栈转储(--traceback):includes/traceback.vp 的主回溯路径用
|
||
# StackWalk64 / SymInitialize / SymFunctionTableAccess64 / SymGetModuleBase64
|
||
# (均在 dbghelp),故开启该开关时必须显式链接 -ldbghelp,否则链接期报未定义符号。
|
||
# 与 ASan 同理:库要排在目标文件之后 ⇒ 追加到 lib_flags。
|
||
# (若确实无法链 dbghelp:把 includes/traceback.vp 的 TRACEBACK_USE_DBGHELP 置 0,
|
||
# 退化为纯 RBP 链回溯,此时不该再带本 flag。)
|
||
if self.traceback_enabled:
|
||
lib_flags.append('-ldbghelp')
|
||
cmd = [self.linker_cmd] + non_lib_flags + ['-o', link_output] + all_link_files + lib_flags
|
||
_vlog().info(f" 执行: {' '.join(cmd)}")
|
||
result = subprocess.run(
|
||
cmd,
|
||
capture_output=True,
|
||
text=True,
|
||
cwd=self.output_dir or '.'
|
||
)
|
||
if result.returncode == 0:
|
||
if is_binary and not is_direct_binary:
|
||
objcopy = shutil.which('llvm-objcopy') or shutil.which('objcopy')
|
||
if objcopy:
|
||
conv = subprocess.run(
|
||
[objcopy, '-O', 'binary', link_output, exe_path],
|
||
capture_output=True, text=True
|
||
)
|
||
if conv.returncode == 0:
|
||
os.remove(link_output)
|
||
_vlog().success(f" 生成裸机二进制: {exe_path}")
|
||
file_size = os.path.getsize(exe_path)
|
||
_vlog().info(f" [大小] {file_size} 字节")
|
||
else:
|
||
_vlog().error(f" 二进制转换失败: {conv.stderr.strip()}")
|
||
sys.exit(1)
|
||
else:
|
||
_vlog().error(" 找不到 llvm-objcopy,无法生成裸机二进制")
|
||
sys.exit(1)
|
||
else:
|
||
_vlog().success(f" 生成: {exe_path}")
|
||
file_size = os.path.getsize(exe_path)
|
||
_vlog().info(f" [大小] {file_size} 字节")
|
||
# --dwarf 构建(含 --asan 自动开启)必须保留 DWARF:
|
||
# 该模式存在的唯一意义是崩溃时反查 ".vp:行",
|
||
# 而 _strip_debug_sections 会 llvm-strip --strip-debug 掉全部
|
||
# .debug_* 段(实测 exe 体积 13378048 → 11895296,
|
||
# llvm-symbolizer 对含 main 在内的所有地址返回 ??:0:0)。
|
||
# 非 dwarf 构建保持原有剥离行为(产物零变化)。
|
||
if not is_binary and not self.dwarf_enabled:
|
||
self._strip_debug_sections(exe_path)
|
||
else:
|
||
_vlog().error(f" 链接失败: {result.stderr.strip()}")
|
||
_vlog().error(f"\n链接失败,立即终止编译。")
|
||
sys.exit(1)
|
||
except FileNotFoundError:
|
||
_vlog().error(f" 找不到链接器: {self.linker_cmd}")
|
||
_vlog().error(f"\n找不到链接器,立即终止编译。")
|
||
sys.exit(1)
|
||
except Exception as e:
|
||
_vlog().error(f" {e}")
|
||
_vlog().error(f"\n链接异常,立即终止编译。")
|
||
sys.exit(1)
|
||
|
||
# --traceback:链接成功后导出"地址→源行"旁文件(<exe>.stackmap)。
|
||
# 刻意放在上面的链接 try/except 之外,使导出阶段的 fail-fast RuntimeError
|
||
# 直接向上抛出终止编译,而不被"链接异常兜底"吞掉。
|
||
if self.traceback_enabled:
|
||
self._WriteStackMap(exe_path)
|