from __future__ import annotations import sys import os import re import shutil import subprocess import ast import traceback import json import pickle import time 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.includes import t from lib.core.SymbolUtils import AnnotationContainsName from lib.Projectrans.Utils import compute_sha1, _check_annotation_for_export, 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 def _ExtractCDefineConstantsFromPyi(pyi_path: str, all_dc: dict[str, object]) -> None: """从 .pyi 文件中提取 CDefine 常量到 all_dc。文件不存在时静默跳过。""" 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 for node in ast.iter_child_nodes(tree): if isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name): if AnnotationContainsName(node.annotation, 'CDefine') and node.value: val = None if isinstance(node.value, ast.Constant): val = node.value.value elif isinstance(node.value, ast.Call) and isinstance(node.value.func, ast.Name) and node.value.func.id == 'float' and node.value.args and isinstance(node.value.args[0], ast.Constant): val = node.value.args[0].value if val is not None: all_dc[f"{node.target.id}"] = val except Exception as e: _vlog().warning(f"Phase2Translator: 忽略异常 {e}", exc_info=e) 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) -> tuple[str, str, str]: try: trans = Phase2Translator(src_root, temp_dir, output_dir, compile_cmd='llc', include_dirs=include_dirs, target_triple=triple, target_datalayout=datalayout) 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 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) trans._shared_symbol_table = shared_data['symbol_table'] trans._shared_source_module_sig_files = shared_data['source_module_sig_files'] trans._shared_all_dc = shared_data['all_dc'] 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) return ('ok', src_path, '') except Exception as e: return ('error', src_path, f'{e}\n{traceback.format_exc()}') def _count_braces_outside_strings(text: str) -> int: """统计花括号差值,跳过 c"..." 字符串字面量内部的括号""" 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 class Phase2Translator: """阶段二:使用声明接口翻译源文件""" INCLUDE_LIB_EXTENSIONS: tuple[str, ...] = ('.dll', '.ll', '.so', '.o', '.a', '.lib') INCLUDE_SRC_EXTENSIONS: tuple[str, ...] = ('.c', '.cpp', '.cc', '.cxx', '.m', '.mm') INCLUDE_DIRS: list[str] = [ os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), 'includes'), ] 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, 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) -> 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 self.slice_level = 3 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] = [] 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 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_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 = [d for d in self.INCLUDE_DIRS if os.path.isdir(d)] 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 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('.pyi'): sha1 = fname[:-4] self.sig_files[sha1] = fpath elif fname.endswith('.stub.ll'): sha1 = fname[:-8] self.stub_files[sha1] = fpath # 从全局缓存补充 temp_dir 中缺失的 .pyi / .stub.ll if os.path.isdir(_GLOBAL_CACHE_DIR): for fname in os.listdir(_GLOBAL_CACHE_DIR): if fname.startswith('_'): continue if fname.endswith('.pyi'): sha1 = fname[:-4] if 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 not in self.stub_files: self.stub_files[sha1] = os.path.join(_GLOBAL_CACHE_DIR, fname) sha1_file_path = os.path.join(self.temp_dir, '_sha1_map.txt') if os.path.exists(sha1_file_path): with open(sha1_file_path, 'r', encoding='utf-8') as f: for line in f: line = line.strip() if ':' in line: sha1, rel = line.split(':', 1) 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: for sha1, stub_path in self.stub_files.items(): self.sha1_map[sha1] = sha1 def _build_struct_sha1_map(self) -> dict[str, str]: struct_sha1_map = {} valid_sha1_keys = set(self.sha1_map.keys()) # 收集 temp_dir 和全局缓存中的 .pyi 文件 pyi_dirs: list[str] = [self.temp_dir] if os.path.isdir(_GLOBAL_CACHE_DIR): pyi_dirs.append(_GLOBAL_CACHE_DIR) 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('.pyi'): continue sha1_key = fname.replace('.pyi', '') if sha1_key not in valid_sha1_keys: continue if sha1_key in struct_sha1_map: continue 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.walk(tree): if isinstance(node, ast.ClassDef): struct_sha1_map[node.name] = sha1_key return struct_sha1_map 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.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('.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} self.sig_files = {k: v for k, v in self.sig_files.items() if k in valid_sha1s} 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 = [] 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]) # 检测 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") if os.path.isfile(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") else: 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 os.path.isfile(stub_cache) and os.path.isfile(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 = min(8, 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: shared_data = { 'symbol_table': self._shared_symbol_table, 'source_module_sig_files': self._shared_source_module_sig_files, 'all_dc': self._shared_all_dc, '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 = '' errors = [] # 复用 self._executor_pool:避免局部变量 GC 时 finalizer 阻塞 ~99s # (join_executor_internals 在 GC 时同步等待 worker 退出队列) if self._executor_pool is None: self._executor_pool = ProcessPoolExecutor(max_workers=n_workers) executor = self._executor_pool try: futures = {} for i, src_path, out_path, sha1, msm in need_translate: rel = os.path.relpath(src_path, self.src_root) future = executor.submit( _parallel_translate_worker, src_path, out_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, prebuilt_ModuleSha1Map=msm ) futures[future] = (i, rel) done_count = 0 for future in as_completed(futures): i, rel = futures[future] done_count += 1 try: status, _, err = future.result() if status == 'ok': _vlog().info(f" [{done_count}/{len(need_translate)}] 完成: {rel}") else: _vlog().error(f" {rel}: {err}") errors.append((rel, err)) except Exception as e: _vlog().error(f" {rel}: {e}") errors.append((rel, str(e))) 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 if errors: _vlog().error(f"\n{len(errors)} 个文件翻译失败,继续编译 includes") 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) t1 = time.time() _vlog().info(f" [{idx+1}/{len(need_translate)}] {rel} ({t1-t0:.1f}s)") except Exception as e: _vlog().error(f" {rel}: {e}") traceback.print_exc() 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] + '.py' if os.path.isfile(py_path): with open(py_path, 'r', encoding='utf-8') as f: py_sha1 = compute_sha1(f.read()) 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): stripped = line.strip() if stripped.startswith('define '): dm = re.match(r'define\s+[^@]*@"?([a-f0-9]+)\.', stripped) if dm and module_sha1 is None: module_sha1 = dm.group(1) dm2 = re.match(r'define\s+[^@]*@"?([^"\s(]+)"?', 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): stripped = line.strip() if in_global_def: text_lines.append(line) global_type_parts.append(stripped) global_brace_depth += _count_braces_outside_strings(stripped) 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) brace_depth += _count_braces_outside_strings(stripped) if brace_depth <= 0: in_define = False continue if stripped.startswith('define '): text_lines.append(line) in_define = True brace_depth = _count_braces_outside_strings(stripped) decl_line = re.sub(r'^define\s+', 'declare ', stripped) decl_line = re.sub(r'\b(linkonce_odr|weak_odr|linkonce|weak|common|appending|internal|private)\s+', '', decl_line) decl_line = re.sub(r'\balwaysinline\s+', '', 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 '): decl_line = re.sub(r'^define\s+', 'declare ', prev_stripped) decl_line = re.sub(r'\b(linkonce_odr|weak_odr|linkonce|weak|common|appending|internal|private)\s+', '', decl_line) decl_line = re.sub(r'\balwaysinline\s+', '', 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) if ' internal ' not in stripped and ' private ' not in stripped: name_match = re.match(r'(@\S+)', 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.match(r'declare\s+[^@]*@"?([^"\s(]+)"?', 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.match(r'^[a-f0-9]{16}\.', 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) def _LoadDeps(self, sha1: str) -> dict[str, str] | None: """加载依赖指纹""" deps_path = os.path.join(self.output_dir, f"{sha1}.deps.json") if os.path.isfile(deps_path): try: with open(deps_path, 'r', encoding='utf-8') as f: return json.load(f) 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 os.path.isfile(stub_path) or not os.path.isfile(text_path): return False with open(stub_path, 'r', encoding='utf-8') as f: stub_content = f.read() with open(text_path, 'r', encoding='utf-8') as f: text_content = f.read() 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) self._save_deps(sha1, current_ModuleSha1Map) obj_path = os.path.join(self.output_dir, f"{sha1}.obj") if os.path.isfile(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 处理功能)。 检测模式: - 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) """ try: tree = ast.parse(code) except SyntaxError: return code # 检查是否已经导入了 _list / _dict / json has_list_import: bool = False has_dict_import: bool = False has_json_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 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 if has_list_import and has_dict_import and has_json_import: break # 检查是否使用了 list / dict 容器 uses_list: bool = False uses_dict: bool = False for node in ast.walk(tree): 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' if prefix: return prefix + code return code def _translate_file(self, src_path: str, out_path: str, prebuilt_ModuleSha1Map: dict[str, str] | None = None) -> None: """翻译单个源文件,嵌入相关 .stub.ll 声明""" with open(src_path, 'r', encoding='utf-8') as f: code = f.read() # SHA1 必须在自动导入注入之前计算,以保持与 Phase1 生成的文件名/函数名前缀一致 sha1 = compute_sha1(code) # 自动导入: 检测 list 容器使用,自动注入 import _list code = self._inject_auto_imports(code, src_path) 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 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._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 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 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._export_extern_funcs = self._shared_export_extern_funcs 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: with open(sig_path, 'r', encoding='utf-8') as f: sig_content = f.read() sig_tree = ast.parse(sig_content) for node in ast.iter_child_nodes(sig_tree): if isinstance(node, ast.FunctionDef): if _check_annotation_for_export(node.returns): 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: trans.translator._export_extern_funcs = export_extern_funcs 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('.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('.py') and (not py_file.startswith('_') or py_file == '_list.py' 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 = {} for sha1_key, stub_path in self.stub_files.items(): if sha1_key == sha1: continue if os.path.exists(stub_path): try: with open(stub_path, 'r', encoding='utf-8') as f: stub_content = f.read() 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) if all_dc: trans.translator._all_define_constants = all_dc 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 chain_lines = [] for entry in reversed(error_stack): if len(entry) >= 3: exc_msg, line_info = entry[1], entry[2] chain_lines.append(" %s\n %s" % (line_info, exc_msg)) 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) # 修复 LLVM 22+: external global 不能有初值,改为 linkonce result = re.sub(r'^(@[\w.]+)\s*=\s*external\s+global\s+(\w[\w*]*)\s+(-?\d+|null|zeroinitializer|true|false)$', r'\1 = linkonce global \2 \3', result, flags=re.MULTILINE) # 后处理:添加 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) 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) with open(base + '.text.ll', 'w', encoding='utf-8', newline='\n') as f: f.write(text_content) self._save_deps(sha1, ModuleSha1Map) _vlog().info(f" -> {os.path.basename(out_path)}") 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) with open(stub_path, 'r', encoding='utf-8') as f: stub_content = f.read() with open(out_path, 'w', encoding='utf-8', newline='\n') as f: f.write(stub_content) _vlog().info(f" -> {os.path.basename(out_path)} (仅声明)") 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 _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) 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 os.path.isfile(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: with open(ll_path, 'r', encoding='utf-8') as f: content = f.read() 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: with open(ll_path, 'r', encoding='utf-8') as f: content = f.read() 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) 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(" [内联] 跨模块内联优化完成") with open(optimized_ll, 'r', encoding='utf-8') as f: opt_content = f.read() opt_content = re.sub(r'^(define|declare)\s+(private|internal|available_externally|linkonce|weak|common|appending|extern_weak|linkonce_odr|weak_odr|external)\s+', r'\1 \2 dso_local ', opt_content, flags=re.MULTILINE) opt_content = re.sub(r'^(define|declare)\s+(?!private|internal|available_externally|linkonce|weak|common|appending|extern_weak|linkonce_odr|weak_odr|external|dso_local|@)', r'\1 dso_local ', opt_content, flags=re.MULTILINE) # LLVM 22+: external 不允许在带初值的全局定义上显式写出,改为 linkonce 保留多模块合并语义 opt_content = re.sub(r'(@[\w.]+)\s*=\s*external\s+global\s+(\w[\w*]*)\s+(-?\d+|null|zeroinitializer|true|false)$', r'\1 = linkonce global \2 \3', opt_content, flags=re.MULTILINE) opt_content = re.sub(r'(@[\w.]+)\s*=\s*(private|internal|available_externally|linkonce|weak|common|appending|extern_weak|linkonce_odr|weak_odr)\s+(global|constant)', r'\1 = \2 dso_local \3', opt_content) opt_content = re.sub(r'(@[\w.]+)\s*=\s+(?!private|internal|available_externally|linkonce|weak|common|appending|extern_weak|linkonce_odr|weak_odr|dso_local)(global|constant)', r'\1 = dso_local \2', 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: cmd = [self.compile_cmd] + self.compile_flags + ['-o', merged_obj, optimized_ll] result = subprocess.run(cmd, capture_output=True, text=True, cwd=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 os.path.isfile(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 os.path.isfile(obj_path): return (rel, 'cached', '') try: with open(ll_path, 'r', encoding='utf-8') as f: ll_content = f.read() 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.match(r'(@\S+)', stripped) if gm: gname = gm.group(1) is_external = re.match(r'@\S+\s*=\s*external\s+', 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.match(r'declare\s+[^@]*@"?([^"\s(]+)"?', 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.match(r'define\s+[^@]*@"?([^"\s(]+)"?', stripped) if dm: existing_defines.add(dm.group(1)) elif stripped.startswith('@') and (' global ' in stripped or ' constant ' in stripped): gm = re.match(r'(@\S+)', 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 os.path.isfile(stub_path): stub_path = os.path.join(self.temp_dir, f"{dep_sha1}.stub.ll") if os.path.isfile(stub_path): with open(stub_path, 'r', encoding='utf-8') as sf: stub_content = sf.read() 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.match(r'declare\s+[^@]*@"?([^"\s(]+)"?', 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.match(r'declare\s+[^@]*@"?([^"\s(]+)"?', 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.match(r'(@\S+)', 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.sub(r'^(define|declare)\s+(private|internal|available_externally|linkonce|weak|common|appending|extern_weak|linkonce_odr|weak_odr|external)\s+', r'\1 \2 dso_local ', ll_content, flags=re.MULTILINE) ll_content = re.sub(r'^(define|declare)\s+(?!private|internal|available_externally|linkonce|weak|common|appending|extern_weak|linkonce_odr|weak_odr|external|dso_local|@)', r'\1 dso_local ', ll_content, flags=re.MULTILINE) # LLVM 22+: external 不允许在带初值的全局定义上显式写出,改为 linkonce 保留多模块合并语义 ll_content = re.sub(r'^(@[\w.]+)\s*=\s*external\s+global\s+(\w[\w*]*)\s+(-?\d+|null|zeroinitializer|true|false)$', r'\1 = linkonce global \2 \3', ll_content, flags=re.MULTILINE) ll_content = re.sub(r'^(@[\w.]+)\s*=\s*(private|internal|available_externally|linkonce|weak|common|appending|extern_weak|linkonce_odr|weak_odr)\s+(global|constant)', r'\1 = \2 dso_local \3', ll_content, flags=re.MULTILINE) ll_content = re.sub(r'^(@[\w.]+)\s*=\s+(?!private|internal|available_externally|linkonce|weak|common|appending|extern_weak|linkonce_odr|weak_odr|dso_local)(global|constant)', r'\1 = dso_local \2', ll_content, flags=re.MULTILINE) # LLVM 22+: 移除同一模块中已有 define 的 declare 语句(LLVM 22 将 declare+define 视为重复定义) defined_funcs: set[str] = set() for m in re.finditer(r'^define\s+(?:dso_local\s+)?[^@]*@\"?([^"\s\(]+)\"?\s*\(', ll_content, re.MULTILINE): 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.match(r'^declare\s+(?:dso_local\s+)?[^@]*@\"?([^"\s\(]+)\"?\s*\(', 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.match(r'^declare\s+(?:dso_local\s+)?[^@]*@\"?([^"\s\(]+)\"?\s*\(', 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:]) dso_ll_path = ll_path[:-3] + '_dso.ll' with open(dso_ll_path, 'w', encoding='utf-8') as f: f.write(ll_content) cmd = [self.compile_cmd] + self.compile_flags + ['-o', obj_path, dso_ll_path] result = subprocess.run(cmd, capture_output=True, text=True, cwd=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 os.path.isfile(obj_path): need_compile.append(ll_path) if need_compile: n_workers = min(os.cpu_count() or 4, len(need_compile), 8) _vlog().info(f" 并行编译 {len(need_compile)} 个文件 (workers={n_workers})") errors = [] with ThreadPoolExecutor(max_workers=n_workers) as executor: futures = {executor.submit(_compile_one, ll_path): ll_path for ll_path in need_compile} for future in as_completed(futures): rel, status, err = future.result() if status == 'ok': _vlog().success(f" {rel}") elif status == 'error': _vlog().error(f" {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 文件,收集 @t.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('.py'): continue py_path: str = os.path.join(root, fname) code, tree = parse_python_file(py_path) # 字符串预过滤:不含 CVTable 的文件跳过 AST 遍历 if tree is None or 'CVTable' 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.Attribute): if getattr(dec.value, 'id', None) == 't' and dec.attr == 'CVTable': has_cvtable = True elif isinstance(dec, ast.Name): if dec.id == 'CVTable': 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 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('.py'): py_index.add(os.path.normcase(os.path.join(root, fname))) return py_index def _scan_include_libraries(self) -> None: """扫描 includes 目录,检测被使用的模块对应的库文件和源文件""" 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('.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 os.path.isfile(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 os.path.isfile(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 + '.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)} 个") def _scan_dir_for_libs(self, directory: str, module_name: str) -> None: """递归扫描目录中的库文件和源文件""" for root, dirs, files in os.walk(directory): 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 依赖(支持子包,如 w32.win32base) py_index 非空时用 set 查询替代 os.path.isfile(O(1) vs 24ms/次)。 """ # 候选路径:子包路径(w32/win32base.py)、顶层模块(w32.py)、子包 __init__.py candidates: list[str] = [] # 子包路径:w32.win32base → w32/win32base.py candidates.append(os.path.join(includes_dir, module_name.replace('.', os.sep) + '.py')) # 子包 __init__.py:w32.win32base → w32/win32base/__init__.py candidates.append(os.path.join(includes_dir, module_name.replace('.', os.sep), '__init__.py')) # 顶层模块:取第一个组件 → w32.py top_module = module_name.split('.')[0] if top_module != module_name: 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 = os.path.isfile(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,区分目标平台和重定位模型。 """ 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) precompiled_dir: str = os.path.join(inc_parent, inc_base + '.binary') 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 源文件") # 构建 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 # 优先检查 includes.binary 预编译缓存(跨项目共享) if not self.force_recompile_includes and precompiled_obj and os.path.isfile(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 os.path.isfile(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: with open(other_stub, 'r', encoding='utf-8') as sf: for sline in sf: 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: shared_data = { 'symbol_table': self._shared_symbol_table, 'source_module_sig_files': self._shared_source_module_sig_files, 'all_dc': self._shared_all_dc, '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 = min(8, 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: self._executor_pool = ProcessPoolExecutor(max_workers=n_workers) 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 ) 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 ) 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() if status == 'ok': _vlog().info(f" [{done_count}/{len(need_translate)}] 翻译完成: {module_name}.py") else: _vlog().error(f" {module_name}.py: {err}") errors.append((module_name, err)) except Exception as e: _vlog().error(f" {module_name}.py: {e}") errors.append((module_name, str(e))) 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") self._translate_file(src_path, ll_path, prebuilt_ModuleSha1Map=include_ModuleSha1Map) 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") self._translate_file(src_path, ll_path, prebuilt_ModuleSha1Map=include_ModuleSha1Map) # ========== 阶段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 os.path.isfile(ll_path): _vlog().error(f" 翻译未生成 .ll 文件: {module_name}.py") continue with open(ll_path, 'r', encoding='utf-8') as f: ll_content = f.read() # dso_local 正则标注(使用 ^ + MULTILINE 避免匹配字符串常量内容中的 define/declare) ll_content = re.sub(r'^(define|declare)\s+(private|internal|available_externally|linkonce|weak|common|appending|extern_weak|linkonce_odr|weak_odr|external)\s+', r'\1 \2 dso_local ', ll_content, flags=re.MULTILINE) ll_content = re.sub(r'^(define|declare)\s+(?!private|internal|available_externally|linkonce|weak|common|appending|extern_weak|linkonce_odr|weak_odr|external|dso_local)', r'\1 dso_local ', ll_content, flags=re.MULTILINE) # LLVM 22+: external 不允许在带初值的全局定义上显式写出,改为 linkonce 保留多模块合并语义 ll_content = re.sub(r'^(@[\w.]+)\s*=\s*external\s+global\s+(\w[\w*]*)\s+(-?\d+|null|zeroinitializer|true|false)$', r'\1 = linkonce global \2 \3', ll_content, flags=re.MULTILINE) ll_content = re.sub(r'^(@[\w.]+)\s*=\s*(private|internal|available_externally|linkonce|weak|common|appending|extern_weak|linkonce_odr|weak_odr)\s+(global|constant)', r'\1 = \2 dso_local \3', ll_content, flags=re.MULTILINE) ll_content = re.sub(r'^(@[\w.]+)\s*=\s+(?!private|internal|available_externally|linkonce|weak|common|appending|extern_weak|linkonce_odr|weak_odr|dso_local)(global|constant)', r'\1 = dso_local \2', ll_content, flags=re.MULTILINE) # 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:]) dso_ll_path = ll_path[:-3] + '_dso.ll' with open(dso_ll_path, 'w', encoding='utf-8') as f: f.write(ll_content) 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] cmd = [self.compile_cmd] + self.compile_flags + ['-o', obj_path, dso_ll_path] result = subprocess.run(cmd, capture_output=True, text=True, cwd=self.output_dir) return (task, result) if len(compile_tasks) > 1: n_workers = min(8, len(compile_tasks)) 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 预编译目录 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: 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 os.path.isfile(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 源码中提取函数参数名,存入 function_param_names 优化:单次递归遍历 AST,维护当前类上下文,O(N) 复杂度。 复用 parse_python_file 的 AST 缓存。 """ for includes_dir in self.include_dirs: if not os.path.isdir(includes_dir): continue for filename in os.listdir(includes_dir): if not filename.endswith('.py'): continue src_path = os.path.join(includes_dir, filename) 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 记录参数名并标注所属类,函数内部的嵌套定义 不再属于外层类。 """ 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 # 函数内部的嵌套定义不属于外层类 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}.pyi") if not os.path.isfile(sig_path): # 全局缓存检查(不被 --clean 清除) _TryGlobalCache(sha1, 'pyi', sig_path) if not os.path.isfile(sig_path): try: with open(src_path, 'r', encoding='utf-8') as f: py_content = f.read() 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) _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 os.path.isfile(sig_path): self.sig_files[sha1] = sig_path self.sha1_map[sha1] = f"includes/{mod_full.replace('.', os.sep)}.py" 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 os.path.isfile(stub_path): # 全局缓存检查(不被 --clean 清除) _TryGlobalCache(sha1, 'stub.ll', stub_path) if not os.path.isfile(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 os.path.isfile(stub_path): self.stub_files[sha1] = stub_path elif os.path.isfile(sig_path): # 如果 stub 文件不存在,也尝试从 temp 目录查找 temp_stub = os.path.join(self.temp_dir, f"{sha1}.stub.ll") if os.path.isfile(temp_stub): self.stub_files[sha1] = temp_stub else: # 最后尝试从全局缓存直接读取 _TryGlobalCache(sha1, 'stub.ll', temp_stub) if os.path.isfile(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('.py'): src_path = os.path.join(root, fname) self._collect_inline_symbols(src_path) py_path = os.path.join(includes_dir, module_name + '.py') if os.path.isfile(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: is_inline = False if isinstance(node.returns, ast.BinOp) and isinstance(node.returns.op, ast.BitOr): for part in [node.returns.left, node.returns.right]: if isinstance(part, ast.Attribute) and part.attr == 'CInline': is_inline = True break if isinstance(part, ast.Name) and part.id == 'CInline': is_inline = True break elif isinstance(node.returns, ast.Attribute) and node.returns.attr == 'CInline': is_inline = True elif isinstance(node.returns, ast.Name) and node.returns.id == 'CInline': is_inline = True 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 = t.CInline() self.inline_func_symbols[func_name] = info self.inline_func_symbols[sym_key] = info 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 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('.pyi'): pyi_path = os.path.join(includes_dir, pyi_file) module_name = os.path.splitext(pyi_file)[0] base_trans.translator.SymbolTable.LoadModuleSymbols(pyi_path, module_name, lineno=0) for py_file in os.listdir(includes_dir): if py_file.endswith('.py') and (not py_file.startswith('_') or py_file == '_list.py' 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] base_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/'):] base_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(base_trans, sig_path, package_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/'): continue module_name = os.path.splitext(rel_path)[0].replace(os.sep, '.').replace('/', '.') base_trans.translator.SymbolTable.LoadModuleSymbols(sig_path, module_name, lineno=0) 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 all_dc = {} for sha1_key, stub_path in self.stub_files.items(): if os.path.exists(stub_path): try: with open(stub_path, 'r', encoding='utf-8') as f: stub_content = f.read() 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) 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 _check_annotation_for_export(node.returns): 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_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('.py') and (not py_file.startswith('_') or py_file == '_list.py' or py_file == '_dict.py'): py_path = os.path.join(abs_includes, py_file) try: # 复用 parse_python_file 的 AST 缓存 _, py_tree = parse_python_file(py_path) if py_tree is None: continue 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, } 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: with open(os.path.join(_GLOBAL_CACHE_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") 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) project_dir = os.path.dirname(self.output_dir) if project_dir and os.path.isdir(project_dir) and project_dir not in scan_dirs: scan_dirs.append(project_dir) 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 _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') if os.path.isfile(merged_obj): obj_files.append(merged_obj) else: for root, dirs, files in os.walk(self.output_dir): for file in files: if file.endswith('.obj'): sha1 = file[:-4] if sha1 in active_sha1s and sha1 not in self._include_sha1s: 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 os.path.isfile(linker_ld_src) and not os.path.isfile(linker_ld_dst): shutil.copy2(linker_ld_src, linker_ld_dst) _vlog().info(f" [链接] 使用链接脚本: linker.ld") elif not os.path.isfile(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) _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 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} 字节") if not is_binary: 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)