Files
TransPyC/lib/Projectrans/Config.py
T
TermiNexus 43e7cdd2cc feat(tpc): 函数级 DWARF 调试元数据 + 合并 IR 元数据规整
DWARF: 新增 LLVMCG/DebugInfo.py(DebugInfoEmitter)——每模块一个 DIFile/DICompileUnit、每个 define 一个 DISubprogram 并在函数入口首条指令挂 DILocation,模块落盘前补 !llvm.dbg.cu 与 !llvm.module.flags(Debug Info Version 3)。开关:新增 --dwarf(默认关闭),dwarf_enabled = args.dwarf or args.asan,沿 Phase2Translator → _translate_file → Translator.DwarfEnabled → LlvmCodeGenerator.dwarf_enabled 下传。define 之外的 declare 不挂。include 库刻意不给 DI(其 .obj 进跨项目共享的 includes.binary 缓存,注入会污染缓存)

关键约束(踩过的坑,已在探针中实测):DICompileUnit 与 DISubprogram 必须 is_distinct=True;emissionKind 用 FullDebug 而非 Full;spFlags 用 DISPFlagDefinition;DISubprogram 必带 type=DISubroutineType,缺了 llc 会崩 0xC0000005;模块级必须有 !llvm.dbg.cu + Debug Info Version 3

IRMetadataFix: 新增 SanitizeIRMetadata,在 _llc_compile 入口统一规整 IR——BuildCombinedIR 是多模块 IR 文本直接拼接,--dwarf 后每个片段自带 !0..!N 与两份命名元数据,22383 行样本里 !0/!1 各重复 67 次导致 opt/llc 报 Metadata id is already used。实现:按块重编号 !N 并改写 !dbg/命名元数据引用、!llvm.dbg.cu 全量聚合、其它命名元数据全同去重;无元数据文件逐字节 no-op、幂等(1.7MB/0.30s)

顺带修 _AddWeakComdat:函数头正则跨行吞换行,--dwarf 把 !dbg 放到 define 行尾后 comdat 被插到 ! 附着之后,llc 报 expected { in function body(现场 52 个 define 中 36 个中招);改为插在附着之前,无 !dbg 时产物逐字节不变

已自证:llc/opt RC=0、行表含 .vp 文件名与行号、opt -passes=forceattrs,asan 与 llvm-link/always-inline 均不丢 !dbg。已知待办:llc 现报 inlinable function call ... must have a !dbg location ⇒ 需补语句/调用点级 !dbg(否则警告 ignoring invalid debug info 会丢弃整份 DI)
2026-09-16 19:29:51 +08:00

364 lines
19 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import os
import sys
import json
import glob as glob_module
import argparse
import shutil
import subprocess
from typing import Any
from lib.core.VLogger import get_logger as _vlog
from lib.Projectrans.Phase1Generator import Phase1Generator
from lib.Projectrans.Phase2Translator import Phase2Translator
# ============================================================
# 全局 SHA1 映射内存存储器
#
# Phase1 生成 sha1_map 后写入此处(save_sha1_map),
# Phase2 从此处读取(Load_sha1_map / Phase2Translator._LoadSha1Map)。
# _sha1_map.txt 文件仅作为人类可读输出,不再作为机器分析数据源。
# ============================================================
_g_sha1_map_store: dict[str, str] = {}
def get_sha1_map_store() -> dict[str, str]:
"""返回全局 SHA1 映射内存存储器(供 Phase2Translator 直接访问)"""
return _g_sha1_map_store
def clear_sha1_map_store() -> None:
"""清空全局 SHA1 映射内存存储器"""
_g_sha1_map_store.clear()
def save_sha1_map(temp_dir: str, sha1_map: dict[str, str]) -> None:
"""将 SHA1 映射表保存到内存存储器,并输出人类可读的 _sha1_map.txt
机器分析使用内存存储器(_g_sha1_map_store),
_sha1_map.txt 仅作为人类可读输出,不再被机器读取。
"""
# 写入内存存储器(机器分析数据源)
_g_sha1_map_store.clear()
for sha1, rel in sha1_map.items():
_g_sha1_map_store[sha1] = rel
# 写入文件(仅人类可读输出)
map_path: str = os.path.join(temp_dir, '_sha1_map.txt')
with open(map_path, 'w', encoding='utf-8') as f:
for sha1, rel in sorted(sha1_map.items()):
f.write(f"{sha1}:{rel}\n")
def Load_sha1_map(temp_dir: str) -> dict[str, str]:
"""从内存存储器加载 SHA1 映射表
temp_dir 参数保留用于签名兼容性,但不再读取文件。
机器分析使用内存存储器(_g_sha1_map_store)。
"""
result: dict[str, str] = {}
for sha1, rel in _g_sha1_map_store.items():
result[sha1] = rel
return result
def Load_project_config(project_file: str) -> dict[str, Any]:
"""从 project.json 加载配置"""
with open(project_file, 'r', encoding='utf-8') as f:
return json.load(f)
def resolve_paths(config: dict[str, Any], project_file: str) -> dict[str, Any]:
"""将配置中的相对路径转换为绝对路径"""
project_dir: str = os.path.dirname(os.path.abspath(project_file))
result: dict[str, Any] = dict(config)
def resolve(p: str | None) -> str | None:
if p is None:
return None
if os.path.isabs(p):
return p
return os.path.normpath(os.path.join(project_dir, p))
if 'source_dir' in result:
result['source_dir'] = resolve(result.get('source_dir'))
elif 'sources' in result:
resolved_sources: list[str] = []
for src in result['sources']:
if '*' in src or '?' in src:
for matched in glob_module.glob(os.path.join(project_dir, src), recursive=True):
if os.path.isfile(matched):
resolved_sources.append(matched)
else:
resolved_sources.append(resolve(src))
result['sources'] = [s for s in resolved_sources if os.path.isfile(s)]
# 直接使用 output_dir / temp_dir(与新版本 TPV 一致,不再支持 build_dir)
result['temp_dir'] = resolve(config.get('temp_dir', './temp'))
result['output_dir'] = resolve(config.get('output_dir', './output'))
if 'includes' in result:
result['includes'] = [resolve(inc) for inc in result['includes']]
result['_project_dir'] = project_dir
return result
def main() -> None:
parser: argparse.ArgumentParser = argparse.ArgumentParser(
description='TransPyC 工程翻译器 - 基于 project.json 的两阶段翻译',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog='''
示例:
# 从 project.json 读取配置并执行(默认查找当前目录)
python ProjectTrans.py
# 指定 project.json 文件
python ProjectTrans.py --project ./TestProject/project.json
# 使用命令行参数覆盖 project.json 配置
python ProjectTrans.py --src ./src --temp ./decl --output ./out --phase all
# 仅运行阶段一
python ProjectTrans.py --phase 1
# 仅运行阶段二
python ProjectTrans.py --phase 2
# 清理临时目录
python ProjectTrans.py --clean
# 编译并立即执行
python Projectrans.py --project ./TestProject/project.json --clean --run
# 删除 includes.binary 预编译缓存并重新编译所有 includes
python Projectrans.py --project ./TestProject/project.json --rebuild-includes
'''
)
parser.add_argument('--project', help='project.json 路径(默认查找当前目录)')
parser.add_argument('--src', help='源文件目录(覆盖 project.json)')
parser.add_argument('--temp', help='声明接口临时目录(覆盖 project.json)')
parser.add_argument('--output', help='输出目录(覆盖 project.json)')
parser.add_argument('--phase', choices=['1', '2', 'all'], help='阶段: 1=生成声明, 2=翻译+编译, all=全部')
parser.add_argument('--cc', help='LLVM 编译器命令(覆盖 project.json)')
parser.add_argument('--clean', action='store_true', help='清理 output 和 temp 目录')
parser.add_argument('--run', action='store_true', help='编译成功后立即执行生成的可执行文件')
parser.add_argument('--rebuild-includes', action='store_true', help='删除 includes.binary 预编译缓存并重新编译所有 includes')
parser.add_argument('--clear-cache', action='store_true', help='清除 .transpyc_cache 全局缓存(.vpi/.stub.ll/.doc.json)')
parser.add_argument('--asan', action='store_true', help='ASan 模式:对全部 .ll 做 opt -passes=asan 插桩,并显式链接 clang64 的 ASan 运行库(mingw64 的 clang 没有 x86_64-w64-windows-gnu 目标的 ASan runtime,不能用 -fsanitize=address)')
parser.add_argument('--dwarf', action='store_true', help='函数级 DWARF 调试元数据:每模块一份 DICompileUnit/DIFile,每个 define 一份 DISubprogram 并挂 !dbg,函数入口第一条指令挂 DILocation。使 ASan/llvm-symbolizer 能把崩溃地址反查为 "<文件>.vp:函数起始行"(默认关闭;--asan 时自动开启)。开启时强制重译 App 模块(不复用不含 !llvm.dbg.cu 的旧 .ll/.obj),.ll/.obj 体积与编译时间略增')
parser.add_argument('--nowarn-register', action='store_true', help='抑制"外部函数返回类型未登记"类 WARN 输出(透传给自举编译的 TransPyV.exe)')
parser.add_argument('--hotspot-sites', nargs='?', const='', default=None, help='调用点热点统计:为每个匹配白名单的直接 call 点插入计数器,运行时 Ctrl+C 各模块按真实次数降序写入 <path>(默认为 hotspot_sites.txt);同时为每个函数定义加计时器,按自身耗时(self)降序写入同目录 hotspot_time.txt,行为 `<函数名> <self> <inc> <进入次数>`(单位 tick,x86_64 上为序列化 lfence; rdtsc);self 已扣除被插桩子调用的 inc,但不含未插桩被调方(间接调用、strcmp/memcpy 等无定义外部函数),它们计入调用者的 self')
parser.add_argument('--hotspot-callees', default=None, help='--hotspot-sites 的白名单被调函数基名(逗号分隔),默认内存/字符串族 strcmp,strncmp,strchr,strlen,strstr,memcpy,memset,memmove,strcpy,strcat')
parser.add_argument('--hotspot-sites-all', action='store_true', help='--hotspot-sites 对全部直接 call 点插桩(忽略白名单;体积/开销大,用于深度排查)')
args: argparse.Namespace = parser.parse_args()
# ASan 模式(--asan):直接经 args 传给 Phase2Translator(compile_ll_to_obj
# 先 opt -passes=asan 插桩,链接显式 -l: clang64 的 libclang_rt.asan_dynamic*)。
# 不再用 TPC_ASAN 环境变量透传——环境变量在 worker 子进程/外部调用链下不可靠。
if args.asan:
_vlog().info('ASan 模式已启用(opt -passes=asan + clang64 libclang_rt.asan_dynamic*)', 'asan')
# 函数级 DWARF 调试元数据(--dwarf):默认关闭(硬要求——普通构建不承担
# 任何风险,生成的 .ll 与改动前逐字节一致)。--asan 时自动开启:ASan 报告
# 本身就需要行号才能定位,且 ASan 构建已是显式选择的非默认路径。
dwarf_enabled: bool = bool(args.dwarf or args.asan)
if dwarf_enabled:
_vlog().info(
'函数级 DWARF 调试元数据已启用:每个 .vp 模块一份 DICompileUnit + 每函数 DISubprogram/DILocation'
+ ('(由 --asan 自动开启)' if not args.dwarf else ''),
'dwarf')
project_file: str | None = args.project
if not project_file:
candidates: list[str] = ['project.json', './project.json']
for c in candidates:
if os.path.exists(c):
project_file = c
break
if project_file and os.path.exists(project_file):
_vlog().info(f"读取配置文件: {project_file}")
config: dict[str, Any] = Load_project_config(project_file)
project_dir: str = os.path.dirname(os.path.abspath(project_file))
_vlog().info(f"项目目录: {project_dir}")
else:
_vlog().error("未找到 project.json,请使用 --project 指定")
sys.exit(1)
resolved: dict[str, Any] = resolve_paths(config, project_file)
src: str | list[str] | None = args.src or resolved.get('source_dir') or resolved.get('sources')
temp_dir: str | None = args.temp or resolved.get('temp_dir')
output_dir: str | None = args.output or resolved.get('output_dir')
phase: str = args.phase or 'all'
cc_cmd: str = args.cc or resolved.get('compiler', {}).get('cmd', 'llc')
cc_flags: list[str] = resolved.get('compiler', {}).get('flags', ['-filetype=obj'])
slice_level: int = resolved.get('options', {}).get('slice_level', 3)
project_include_dirs: list[str] = resolved.get('includes', [])
# includes 只认项目配置:同步到 Phase2Translator.PROJECT_INCLUDE_DIRS,
# 供 builtins.vp 扫描等静态路径在 phase1/2 全程使用(禁止仓库硬编码回退)
try:
from lib.Projectrans.Phase2Translator import Phase2Translator
Phase2Translator.PROJECT_INCLUDE_DIRS = [os.path.abspath(d) for d in project_include_dirs if os.path.isdir(os.path.join(project_dir, d)) or os.path.isdir(d)]
except Exception:
pass
target_triple: str | None = resolved.get('target', {}).get('triple')
target_datalayout: str | None = resolved.get('target', {}).get('datalayout')
# 调用点热点统计(--hotspot-sites):None=未启用;''=启用但未给路径(默认
# hotspot_sites.txt);其余为显式路径。一律绝对化,保证运行时(exe 工作目录为
# output_dir)各模块处理器追加写入的路径与预期一致。
hotspot_sites_file: str | None = args.hotspot_sites
if hotspot_sites_file is not None:
if hotspot_sites_file == '':
hotspot_sites_file = 'hotspot_sites.txt'
hotspot_sites_file = os.path.abspath(hotspot_sites_file)
# 白名单覆盖:逗号分隔 → 去空白后的基名列表(None=用 BaseGen 默认白名单)
hotspot_callees: list[str] | None = None
if args.hotspot_callees:
hotspot_callees = [c.strip() for c in args.hotspot_callees.split(',') if c.strip()]
if isinstance(src, list):
if len(src) == 1 and os.path.isdir(src[0]):
src = src[0]
else:
src = src[0] if len(src) == 1 else os.path.dirname(os.path.commonpath(src)) if all(os.path.exists(p) for p in src) else src[0]
_vlog().info(f"源目录: {src}")
_vlog().info(f"临时目录: {temp_dir}")
_vlog().info(f"输出目录: {output_dir}")
_vlog().info(f"阶段: {phase}")
_vlog().info(f"编译器: {cc_cmd} {' '.join(cc_flags)}")
if target_triple:
_vlog().info(f"目标三元组: {target_triple}")
if target_datalayout:
_vlog().info(f"数据布局: {target_datalayout}")
if hotspot_sites_file:
_vlog().info(f"调用点热点统计: {hotspot_sites_file}(按真实调用次数降序落盘,含 main 的模块启动时清空一次);每函数计时: {os.path.join(os.path.dirname(hotspot_sites_file), 'hotspot_time.txt')}")
# 每次编译自动清理子进程性能剖析输出(PROF 目录 *.prof,硬编码路径)
from lib.Projectrans.Phase2Translator import _CleanProfFiles, PROF_DIR
_CleanProfFiles()
_vlog().info(f"PROF 目录已清理: {PROF_DIR}")
if args.clean:
if output_dir and os.path.exists(output_dir):
for f in os.listdir(output_dir):
fpath: str = os.path.join(output_dir, f)
try:
if os.path.isfile(fpath) or os.path.islink(fpath):
os.remove(fpath)
elif os.path.isdir(fpath):
shutil.rmtree(fpath, ignore_errors=True)
except:
pass
_vlog().info(f"output: 已清空 {output_dir}")
if temp_dir and os.path.exists(temp_dir):
for f in os.listdir(temp_dir):
fpath: str = os.path.join(temp_dir, f)
try:
if os.path.isfile(fpath) or os.path.islink(fpath):
os.remove(fpath)
elif os.path.isdir(fpath):
shutil.rmtree(fpath, ignore_errors=True)
except:
pass
_vlog().info(f"temp: 已清空 {temp_dir}")
_vlog().info("清理完成")
if (not phase or phase == 'all') and not args.project:
return
if args.clear_cache:
# 清除 .transpyc_cache 全局缓存
cache_dir: str = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), '.transpyc_cache')
if os.path.isdir(cache_dir):
shutil.rmtree(cache_dir, ignore_errors=True)
os.makedirs(cache_dir, exist_ok=True)
_vlog().info(f"已清除全局缓存: {cache_dir}")
else:
_vlog().info(f"全局缓存目录不存在: {cache_dir}")
if phase in ('1', 'all'):
if isinstance(src, list):
_vlog().error("阶段一需要指定源目录,不支持文件列表")
sys.exit(1)
all_include_dirs: list[str] = list(project_include_dirs) if project_include_dirs else []
gen: Phase1Generator = Phase1Generator(src, temp_dir, include_dirs=all_include_dirs, target_triple=target_triple, target_datalayout=target_datalayout)
gen.run()
save_sha1_map(temp_dir, gen.sha1_map)
if phase in ('2', 'all'):
if not output_dir:
_vlog().error("阶段二需要指定 --output 目录")
sys.exit(1)
if not os.path.exists(temp_dir):
_vlog().error(f"声明目录不存在,请先运行阶段一: {temp_dir}")
sys.exit(1)
if isinstance(src, list):
_vlog().error("阶段二需要指定源目录,不支持文件列表")
sys.exit(1)
linker_cmd: str | None = resolved.get('linker', {}).get('cmd')
linker_flags: list[str] = resolved.get('linker', {}).get('flags', [])
linker_output: str | None = resolved.get('linker', {}).get('output')
# 显式静态链接文件列表(.a/.obj 等),相对 project.vpj 目录解析为绝对路径
static_libs: list[str] = []
for lib in resolved.get('linker', {}).get('static_libs', []) or []:
static_libs.append(lib if os.path.isabs(lib) else os.path.normpath(os.path.join(project_dir, lib)))
# 显式动态链接文件列表(.dll/.so 等),相对 project.vpj 目录解析为绝对路径;
# 链接时作为输入文件(GNU ld 从 DLL/SO 提取导入符号),运行时需 DLL/SO 在搜索路径
dynamic_libs: list[str] = []
for lib in resolved.get('linker', {}).get('dynamic_libs', []) or []:
dynamic_libs.append(lib if os.path.isabs(lib) else os.path.normpath(os.path.join(project_dir, lib)))
target_triple = resolved.get('target', {}).get('triple')
target_datalayout = resolved.get('target', {}).get('datalayout')
force_recompile_includes: bool = bool(resolved.get('options', {}).get('force_recompile_includes', False))
if args.rebuild_includes:
force_recompile_includes = True
all_inc_dirs: list[str] = list(project_include_dirs)
seen_dirs: set[str] = set()
for inc_dir in all_inc_dirs:
abs_inc: str = os.path.abspath(inc_dir)
if abs_inc in seen_dirs:
continue
seen_dirs.add(abs_inc)
if not os.path.isdir(abs_inc):
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')
if os.path.isdir(precompiled_dir):
cleared: int = 0
for fname in os.listdir(precompiled_dir):
fpath: str = os.path.join(precompiled_dir, fname)
try:
if os.path.isfile(fpath) or os.path.islink(fpath):
os.remove(fpath)
cleared += 1
except:
pass
_vlog().info(f"includes.binary: 已清空 {precompiled_dir} ({cleared} 个文件)")
trans: Phase2Translator = Phase2Translator(src, temp_dir, output_dir, compile_cmd=cc_cmd, compile_flags=cc_flags, linker_cmd=linker_cmd, linker_flags=linker_flags, linker_output=linker_output, static_libs=static_libs, dynamic_libs=dynamic_libs, include_dirs=project_include_dirs, target_triple=target_triple, target_datalayout=target_datalayout, force_recompile_includes=force_recompile_includes, asan_mode=args.asan, dwarf_enabled=dwarf_enabled)
trans.sha1_map = Load_sha1_map(temp_dir)
trans.slice_level = slice_level
# 调用点热点统计与每函数计时(--hotspot-sites):None=未启用(零影响)
trans.hotspot_sites_file = hotspot_sites_file
trans.hotspot_sites_all = bool(args.hotspot_sites_all)
trans.hotspot_callees = hotspot_callees
trans.run()
if args.run and linker_output and not linker_output.endswith('.bin'):
exe_path: str = os.path.join(output_dir, linker_output)
if os.path.isfile(exe_path):
_vlog().info(f"\n[运行] 执行: {exe_path}")
try:
result: subprocess.CompletedProcess = subprocess.run([exe_path], cwd=output_dir)
_vlog().info(f"[运行] 退出码: {result.returncode}")
except Exception as e:
_vlog().error(f"[运行] 执行失败: {e}")
sys.exit(1)
else:
_vlog().error(f"[运行] 找不到可执行文件: {exe_path}")
sys.exit(1)