diff --git a/lib/Projectrans/Config.py b/lib/Projectrans/Config.py index 6288df2..f3d7af0 100644 --- a/lib/Projectrans/Config.py +++ b/lib/Projectrans/Config.py @@ -148,6 +148,7 @@ def main() -> None: 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 各模块按真实次数降序写入 (默认为 hotspot_sites.txt);同时为每个函数定义加计时器,按自身耗时(self)降序写入同目录 hotspot_time.txt,行为 `<函数名> <进入次数>`(单位 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') @@ -161,6 +162,16 @@ def main() -> None: 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'] @@ -328,7 +339,7 @@ def main() -> None: 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) + 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=未启用(零影响) diff --git a/lib/Projectrans/Phase2Translator.py b/lib/Projectrans/Phase2Translator.py index bcf3913..c91344d 100644 --- a/lib/Projectrans/Phase2Translator.py +++ b/lib/Projectrans/Phase2Translator.py @@ -21,14 +21,15 @@ from lib.Projectrans.Utils import compute_sha1, _check_annotation_for_export, fi from lib.Projectrans.Phase1Generator import _TryGlobalCache, _SaveToGlobalCache, _GLOBAL_CACHE_DIR import TransPyC from StubGen import PythonToStubConverter +from lib.core.LLVMCG.IRMetadataFix import SanitizeIRMetadata # PROF 热点:nt._path_isfile 2.6 万次 16.4s(Windows 磁盘 stat ~0.6ms/次)。 # 编译进程内文件状态稳定(obj/ll 一旦生成不删除、不被移动),用模块级缓存 # 避免重复 stat。仅用于"存在性检查"类调用(不用于内容变化的判断)。 -_FILE_STAT_CACHE: dict[str, bool] = {} - - +_FILE_STAT_CACHE: dict[str, bool] = {} + + def _CachedIsFile(path: str) -> bool: r: bool | None = _FILE_STAT_CACHE.get(path) if r is None: @@ -233,7 +234,8 @@ def _parallel_translate_batch(batch: list, src_root: str, temp_dir: str, output_ shared_sym_pickle_path: str, struct_sha1_map: dict[str, str], hotspot_sites_file: str | None = None, hotspot_sites_all: bool = False, - hotspot_callees: list[str] | None = None) -> tuple[list, list]: + hotspot_callees: list[str] | None = None, + dwarf_enabled: bool = False) -> tuple[list, list]: """批次子任务:worker 内创建一个 Phase2Translator + pickle.load 一次,顺序翻译批内全部文件。 batch 每项: (i, src_path, out_path, sha1, prebuilt_ModuleSha1Map) @@ -292,7 +294,7 @@ def _parallel_translate_batch(batch: list, src_root: str, temp_dir: str, output_ if symbol_table_bytes is not None: trans._shared_symbol_table = pickle.loads(symbol_table_bytes) trans._shared_symbol_table.translator = None - trans._translate_file(src_path, out_path, prebuilt_ModuleSha1Map=msm) + trans._translate_file(src_path, out_path, prebuilt_ModuleSha1Map=msm, dwarf_enabled=dwarf_enabled) per_file_times.append((src_path, time.time() - _f0, os.path.relpath(src_path, src_root))) except Exception as e: # fail-fast(2026-08-18):任意文件编译出错立即终止整个编译进程, @@ -555,7 +557,7 @@ def _AddWeakComdat(ll_content: str) -> str: COFF 下无 COMDAT 的 weak/linkonce 定义会被 llc 展开成强符号 `.weak..default`(weak external 的实现副本)。同一弱定义出现在 - 多个模块时(泛型特化方法如 `e94ef9e139461915.list[rowptr[AST]]. + 多个模块时(泛型特化方法如 `e94ef9e139461915.list[rawptr[AST]]. __before_init__`、跨模块共享常量如 PYI_BUF_SIZE),副本同名 → ld.lld 报 duplicate symbol。补 COMDAT(any) 后 llc 输出单一 COMDAT 成员,链接器自动去重(llc/ld.lld 端到端已实验验证)。 @@ -569,7 +571,16 @@ def _AddWeakComdat(ll_content: str) -> str: # 1) 函数定义:define [dso_local] ... @name(...) { → @name(...) comdat { def _fn_repl(m: re.Match) -> str: comdat_names.append(m.group(3)) - return m.group(1) + ' comdat ' + m.group(4) + head: str = m.group(1) + # comdat 是函数头关键字,必须写在 !dbg 等元数据附着**之前**:LLVM 解析器 + # 读完 `!...` 附着后就只接受 `{`,附着之后写 comdat 会报 + # "expected '{' in function body"(`[^{]*` 会跨行吞掉换行,--dwarf 给 + # 每个 define 行尾加了 !dbg 后插入点就落到了附着之后)。故插入点前移到 + # 第一个元数据附着之前。 + attach = re.search(r'\s![\w.]', head) + if attach is None: + return head + ' comdat ' + m.group(4) + return head[:attach.start()] + ' comdat' + head[attach.start():] + m.group(4) ll_content = re.sub( r'^(define\s+(?:dso_local\s+)?(?:%s)\s+[^@]*@("?)([^"\s\(]+)\2[^{]*)(\{)' % weak_links, _fn_repl, ll_content, flags=re.MULTILINE) @@ -625,7 +636,7 @@ class Phase2Translator: _include_sha1_cache: dict[str, str] = {} _include_exist_cache: dict[str, bool] = {} - def __init__(self, src_root: str, temp_dir: str, output_dir: str, compile_cmd: str = 'llc', compile_flags: list[str] | None = None, linker_cmd: str | None = None, linker_flags: list[str] | None = None, linker_output: str | None = None, static_libs: list[str] | None = None, dynamic_libs: list[str] | None = None, include_dirs: list[str] | None = None, entry_files: list[str] | None = None, target_triple: str | None = None, target_datalayout: str | None = None, force_recompile_includes: bool = False, build_pyi_cache: bool = True, asan_mode: bool = False) -> None: + def __init__(self, src_root: str, temp_dir: str, output_dir: str, compile_cmd: str = 'llc', compile_flags: list[str] | None = None, linker_cmd: str | None = None, linker_flags: list[str] | None = None, linker_output: str | None = None, static_libs: list[str] | None = None, dynamic_libs: list[str] | None = None, include_dirs: list[str] | None = None, entry_files: list[str] | None = None, target_triple: str | None = None, target_datalayout: str | None = None, force_recompile_includes: bool = False, build_pyi_cache: bool = True, asan_mode: bool = False, dwarf_enabled: bool = False) -> 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) @@ -656,13 +667,18 @@ class Phase2Translator: os.environ['PATH'] = _clang64_bin + os.pathsep + os.environ.get('PATH', '') self.asan_rt_dir = _FindAsanRuntimeDir() self.slice_level = 3 - # 调用点热点统计(--hotspot-sites):None=未启用(零影响);str=调用点计数 - # 落盘文件路径(每函数计时落同目录 hotspot_time.txt)。各模块运行时自带 - # Ctrl+C 处理器、统一以追加方式写该路径;含 main 的模块在 main 入口清空一次 - # → 每次运行一份结果。 - self.hotspot_sites_file: str | None = None - # 是否对全部直接 call 点插桩(忽略白名单);白名单被调函数基名列表(None=默认) - self.hotspot_sites_all: bool = False + # 函数级 DWARF 调试元数据(--dwarf;--asan 时自动开启):False=未启用 + # (零影响,不构造任何 DI 元数据);True=App 源模块的 .ll 携带 + # !llvm.dbg.cu + 每函数 DISubprogram + 入口 DILocation,供 ASan / + # llvm-symbolizer 反查 ".vp:函数起始行"。 + self.dwarf_enabled: bool = dwarf_enabled + # 调用点热点统计(--hotspot-sites):None=未启用(零影响);str=调用点计数 + # 落盘文件路径(每函数计时落同目录 hotspot_time.txt)。各模块运行时自带 + # Ctrl+C 处理器、统一以追加方式写该路径;含 main 的模块在 main 入口清空一次 + # → 每次运行一份结果。 + self.hotspot_sites_file: str | None = None + # 是否对全部直接 call 点插桩(忽略白名单);白名单被调函数基名列表(None=默认) + self.hotspot_sites_all: bool = False self.hotspot_callees: list[str] | None = None self.sha1_map: dict[str, str] = {} self.sig_files: dict[str, str] = {} @@ -871,7 +887,9 @@ class Phase2Translator: # 启用时一律重译,保证每个模块都嵌入 internal 计数器、计时器与 # @llvm.global_ctors 自注册。未启用时 hotspot_on=False,缓存与重组分支 # 行为与改动前完全一致(零影响)。 - hotspot_on: bool = bool(self.hotspot_sites_file) + # --dwarf(函数级 DWARF)同理:缓存 .ll/.obj 不含 !llvm.dbg.cu,必须一并 + # 强制重译,否则开启开关后仍复用旧产物、看不到任何 .vp 行号。 + hotspot_on: bool = bool(self.hotspot_sites_file) or bool(self.dwarf_enabled) for i, src_path in enumerate(py_files, 1): rel = os.path.relpath(src_path, self.src_root) # 复用 parse_python_file 的 AST 缓存(get_file_dependencies 已 parse 过) @@ -1049,6 +1067,7 @@ class Phase2Translator: hotspot_sites_file=self.hotspot_sites_file, hotspot_sites_all=self.hotspot_sites_all, hotspot_callees=self.hotspot_callees, + dwarf_enabled=self.dwarf_enabled, ) futures[future] = (bidx, rel_first, len(batch)) @@ -1095,7 +1114,7 @@ class Phase2Translator: rel = os.path.relpath(src_path, self.src_root) t0 = time.time() try: - self._translate_file(src_path, out_path, prebuilt_ModuleSha1Map=msm) + self._translate_file(src_path, out_path, prebuilt_ModuleSha1Map=msm, dwarf_enabled=self.dwarf_enabled) t1 = time.time() _vlog().info(f" [{idx+1}/{len(need_translate)}] {rel} ({t1-t0:.1f}s)") except Exception as e: @@ -1539,8 +1558,13 @@ class Phase2Translator: 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 声明""" + def _translate_file(self, src_path: str, out_path: str, prebuilt_ModuleSha1Map: dict[str, str] | None = None, dwarf_enabled: bool = False) -> None: + """翻译单个源文件,嵌入相关 .stub.ll 声明 + + dwarf_enabled: 函数级 DWARF 调试元数据(--dwarf),默认 False(零影响)。 + 仅 App 源模块传 True;include 文件的 .obj 会落进跨项目共享的 + includes.binary 预编译缓存,注入 DI 会污染该缓存,故一律保持 False。 + """ _t0: float = time.time() with open(src_path, 'r', encoding='utf-8') as f: code = f.read() @@ -1567,6 +1591,10 @@ class Phase2Translator: trans = TransPyC.TransPyC(code=code, triple=self.triple, datalayout=self.datalayout) trans.translator.CurrentFile = src_path + # 函数级 DWARF(--dwarf):透传给 Translator,由其传给每个新 LlvmCodeGenerator; + # 未启用时不设置 → 与改动前逐字节一致(零影响) + if dwarf_enabled: + trans.translator.DwarfEnabled = True trans.SliceLevel = self.slice_level trans.translator.SliceLevel = self.slice_level trans.translator.SliceCount = 0 @@ -1831,6 +1859,24 @@ class Phase2Translator: except Exception as e: _vlog().warning(f" [警告] 解析包重导出失败 {init_pyi_path}: {e}") + def _SanitizeIRFile(self, ll_path: str) -> str: + """送入 opt/llc 之前,对该 .ll 做一次元数据规整(见 IRMetadataFix)。 + + App 模块的 IR 是「本模块 IR + 各依赖 .stub.ll 片段」直接拼接而成,而 + --dwarf(--asan 自动开启)给每份片段都加了从 !0 开始编号的 DI 元数据, + 合并文件里同一个 !N 被定义多次 → opt/llc 报 "Metadata id is already + used"、命名元数据 "defined more than once"。规整后编号全局唯一、 + 命名元数据只留一份。无元数据 / 已规整的文件逐字节不变(no-op), + 原地覆盖没有额外产物。 + """ + with open(ll_path, 'r', encoding='utf-8', newline='') as f: + text = f.read() + fixed = SanitizeIRMetadata(text) + if fixed != text: + with open(ll_path, 'w', encoding='utf-8', newline='') as f: + f.write(fixed) + return ll_path + def _llc_compile(self, ll_path: str, obj_path: str, cwd: str | None = None) -> subprocess.CompletedProcess: """编译单个 .ll 为 obj。 @@ -1843,6 +1889,7 @@ class Phase2Translator: 函数插桩(clang -fsanitize=address 自动加;手写 IR 没有)—— 先 forceattrs 批量加属性再 asan,否则只链接 runtime 不插桩。 """ + ll_path = self._SanitizeIRFile(ll_path) compile_input = ll_path if self.asan_mode: asan_ll = ll_path[:-3] + '_asan.ll' diff --git a/lib/core/Handles/HandlesFunctions.py b/lib/core/Handles/HandlesFunctions.py index bb6cf0f..8869590 100644 --- a/lib/core/Handles/HandlesFunctions.py +++ b/lib/core/Handles/HandlesFunctions.py @@ -1782,22 +1782,22 @@ class FunctionHandle(BaseHandle): # 导致方法调用查找 ndarray.sum 而非 ndarray[double].sum。 if param_name in Gen.var_struct_class: _existing_ps: str = Gen.var_struct_class[param_name] - # 指针占位名(rowptr / {sha1}.rowptr / rowptr[...])是模块限定 + # 指针占位名(rawptr / {sha1}.rawptr / rawptr[...])是模块限定 # struct 联合(llvmlite.Function | t.CPtr)被 resolver 退化 # 的产物,必须允许 FindStructNameInAnnotation 短名修正 - # (否则 func.IsVarArg → "属性在类 'rowptr' 中找不到")。 - if not (_existing_ps == 'rowptr' or '.rowptr' in _existing_ps - or _existing_ps.startswith('rowptr[')): + # (否则 func.IsVarArg → "属性在类 'rawptr' 中找不到")。 + if not (_existing_ps == 'rawptr' or '.rawptr' in _existing_ps + or _existing_ps.startswith('rawptr[')): continue found_struct: str | None = FindStructNameInAnnotation(arg.annotation, Gen.structs) if found_struct: Gen.var_struct_class[param_name] = found_struct - # 普通类注解形参(非 BinOp 联合):rowptr[X] / t.CPtr[X] / X / t.X + # 普通类注解形参(非 BinOp 联合):rawptr[X] / t.CPtr[X] / X / t.X # 一律按"声明类"登记 var_struct_class。避免按 param.type.pointee 匹配 # 到泛型父特化(如 class Function(GSListNode[Function]) 的形参 func 被 # 登记成 GSListNode[Function],其 struct 仅含 Next → func.Blocks 访问 # fast-fail "属性 'Blocks' 在类 'GSListNode[Function]' 中找不到")。 - # 泛型占位(rowptr[T])的 T 不在 Gen.structs,自动跳过、保留 pointee 匹配。 + # 泛型占位(rawptr[T])的 T 不在 Gen.structs,自动跳过、保留 pointee 匹配。 for i, arg in enumerate(Node.args.args): if arg.annotation is None: continue @@ -1815,7 +1815,7 @@ class FunctionHandle(BaseHandle): elif isinstance(ann, ast.Subscript): subv = ann.value subname = getattr(subv, 'id', None) or getattr(subv, 'attr', '') - if subname in ('rowptr', 'CPtr', 'Ptr'): + if subname in ('rawptr', 'CPtr', 'Ptr'): slc = ann.slice if isinstance(slc, ast.Name): target = slc.id @@ -1943,6 +1943,22 @@ class FunctionHandle(BaseHandle): except Exception: pass raise + # 函数级 DWARF 调试元数据(--dwarf / --asan 自动开启):为每个 define 建 + # DISubprogram 挂 func.metadata["dbg"],并在入口块第一条指令挂 DILocation + # (line/column = 该 def 语句的位置),使 DWARF 行表至少有一条覆盖函数地址 + # 区间的行。declare/extern 在此之前已 return,不会被挂 !dbg;未启用时 + # _GetDebugInfo() 返回 None,不产生任何 IR 差异(零影响)。 + _DbgInfo = Gen._GetDebugInfo() + if _DbgInfo is not None: + try: + _DbgLoc = _DbgInfo.AttachFunction( + func, getattr(Node, 'lineno', 0) or 0, getattr(Node, 'col_offset', 0) + 1) + if EntryBlock.instructions: + EntryBlock.instructions[0].metadata['dbg'] = _DbgLoc + except Exception as _e: + if _config_mode == "strict": + raise + _vlog().warning(f"发射调试元数据失败: {_e}", "Exception") if not Gen.builder.block.is_terminated: if 'naked' in fn_attrs: Gen.builder.unreachable() diff --git a/lib/core/LLVMCG/BaseGen.py b/lib/core/LLVMCG/BaseGen.py index 8cc6cbf..8405a20 100644 --- a/lib/core/LLVMCG/BaseGen.py +++ b/lib/core/LLVMCG/BaseGen.py @@ -212,6 +212,11 @@ class BaseGenMixin: # 注册策略:False=种子 CLI 单模块(在 main 入口注册);True=Projectrans # 多模块(每模块用 @llvm.global_ctors 自注册,不依赖 main 所在模块) self.hotspot_sites_module_local: bool = False + # 函数级 DWARF 调试元数据(--dwarf / --asan 自动开启): + # False=未启用(零影响,不构造任何 DI 元数据、不挂 !dbg); + # True=每个 define 发 DISubprogram + 入口首条指令发 DILocation。 + self.dwarf_enabled: bool = False + self.DebugInfo: Any = None pi: dict[str, bool | int] = self._platform_info # 根据目标平台设置宏(声明式,从三元组推导) if pi['is_windows']: @@ -584,6 +589,22 @@ class BaseGenMixin: except Exception: # 文件读取失败时回退为空行列表 self._current_source_lines = [] + def _GetDebugInfo(self) -> Any: + """返回本模块的 DebugInfoEmitter;未启用 --dwarf 或无源文件时返回 None。 + + 未启用(dwarf_enabled=False)时不构造任何对象、不产生任何 IR 差异, + 行为与改动前逐字节一致。源文件取 _current_source_file(由 GenerateCCode + 在生成 IR 前设定,模块内恒定),每个模块一份 DICompileUnit + DIFile。 + """ + if not self.dwarf_enabled: + return None + if not self._current_source_file: + return None + if self.DebugInfo is None: + from lib.core.LLVMCG.DebugInfo import DebugInfoEmitter + self.DebugInfo = DebugInfoEmitter(self.module, self._current_source_file) + return self.DebugInfo + def _skip_mangle(self, name: str) -> bool: skip_prefixes: tuple[str, ...] = ('llvm.', 'str_const_', 'printf', 'memset_pattern') if any(name.startswith(p) for p in skip_prefixes): diff --git a/lib/core/LLVMCG/DebugInfo.py b/lib/core/LLVMCG/DebugInfo.py new file mode 100644 index 0000000..bf14710 --- /dev/null +++ b/lib/core/LLVMCG/DebugInfo.py @@ -0,0 +1,97 @@ +from __future__ import annotations + +import llvmlite.ir as ir + + +class DebugInfoEmitter: + """函数级 DWARF 调试元数据发射器(llvmlite 文本 IR)。 + + 每个 Module 一份 DICompileUnit + DIFile;每个 define 一份 DISubprogram + (is_distinct=True)挂在 func.metadata["dbg"] 上;函数入口第一条指令挂 + DILocation(line=该函数 def 行,column=该行 col),使 LLVM 行表至少有一条 + 覆盖函数地址区间的行 —— ASan / llvm-symbolizer 反查函数内任意地址即可得到 + ".vp:函数起始行"。 + + 仅在 --dwarf(或 --asan 自动开启)时被实例化;把 Module 落成 .ll 文本之前 + 必须调用 Finalize() 补模块级 !llvm.dbg.cu 与 !llvm.module.flags + (Debug Info Version = 3),否则 llc 会丢弃调试信息或报无效 DI 警告。 + """ + + def __init__(self, module: ir.Module, source_path: str) -> None: + self.Module: ir.Module = module + normalized: str = (source_path or '').replace('\\', '/') + head: str + sep: int = normalized.rfind('/') + if sep < 0: + head, self.FileName = '', normalized + else: + head, self.FileName = normalized[:sep], normalized[sep + 1:] + self.Directory: str = head + self._File: ir.DIValue | None = None + self._CompileUnit: ir.DIValue | None = None + self._SubroutineType: ir.DIValue | None = None + self.Finalized: bool = False + + def _GetFile(self) -> ir.DIValue: + if self._File is None: + self._File = self.Module.add_debug_info("DIFile", { + "filename": self.FileName, + "directory": self.Directory, + }) + return self._File + + def _GetCompileUnit(self) -> ir.DIValue: + if self._CompileUnit is None: + # emissionKind 必须是 DIToken("FullDebug")(写成 "Full" llc 报无效 DI) + self._CompileUnit = self.Module.add_debug_info("DICompileUnit", { + "language": ir.DIToken("DW_LANG_C99"), + "file": self._GetFile(), + "producer": "TPC", + "isOptimized": False, + "runtimeVersion": 0, + "emissionKind": ir.DIToken("FullDebug"), + }, is_distinct=True) + return self._CompileUnit + + def _GetSubroutineType(self) -> ir.DIValue: + if self._SubroutineType is None: + # DISubprogram 必须带 type,缺失会让 llc 崩溃(0xC0000005) + self._SubroutineType = self.Module.add_debug_info("DISubroutineType", { + "types": self.Module.add_metadata([None]), + }) + return self._SubroutineType + + def AttachFunction(self, func: ir.Function, Line: int, Column: int) -> ir.DIValue: + """为 define(有函数体)建 DISubprogram 并返回其入口 DILocation。 + + 仅对 define 调用;declare/extern 没有函数体,不得挂 !dbg。 + """ + sp: ir.DIValue = self.Module.add_debug_info("DISubprogram", { + "name": func.name, + "scope": self._GetFile(), + "file": self._GetFile(), + "type": self._GetSubroutineType(), + "line": Line, + "scopeLine": Line, + "unit": self._GetCompileUnit(), + "spFlags": ir.DIToken("DISPFlagDefinition"), + }, is_distinct=True) + func.metadata["dbg"] = sp + return self.Module.add_debug_info("DILocation", { + "line": Line, + "column": Column, + "scope": sp, + }) + + def Finalize(self) -> None: + """落 .ll 文本之前补模块级 named metadata;无任何函数时不做(no-op)。""" + if self.Finalized or self._CompileUnit is None: + return + self.Module.add_named_metadata("llvm.dbg.cu", self._CompileUnit) + flags: ir.MDValue = self.Module.add_metadata([ + ir.Constant(ir.IntType(32), 2), + ir.MetaDataString(self.Module, "Debug Info Version"), + ir.Constant(ir.IntType(32), 3), + ]) + self.Module.add_named_metadata("llvm.module.flags", flags) + self.Finalized = True diff --git a/lib/core/LLVMCG/IRMetadataFix.py b/lib/core/LLVMCG/IRMetadataFix.py new file mode 100644 index 0000000..68d5cb7 --- /dev/null +++ b/lib/core/LLVMCG/IRMetadataFix.py @@ -0,0 +1,248 @@ +"""合并 IR 的元数据规整(sanitize)。 + +场景:多个模块的 .ll 文本被直接拼接成一个文件(每个模块都自带一小段 +DWARF 元数据:!llvm.dbg.cu / !llvm.module.flags / !0 = !DIFile / ... +以及内联在 `define ... !dbg !N`、指令行上的 !N 引用)时,各模块的编号 +元数据都从 !0 重新开始 —— 合并文件里同一个 !N 被定义多次、命名元数据 +也被定义多次,LLVM 解析即报: + + error: Metadata id is already used + error: ... !llvm.dbg.cu ... defined more than once + +本模块提供 SanitizeIRMetadata(text) -> text:把全文的 `!N = ` 定义按 +「元数据块」为单位重新编号成全局唯一 id,并同步改写全文引用;命名元数据 +按名字聚合/去重成一份。无元数据的文本逐字节返回原样(严格 no-op),对 +已规整的文本再跑一次结果一致(幂等)。 +""" + +from __future__ import annotations + +import re + +# `!N = ` 形式的编号元数据定义行 +_Definition = re.compile(r'^!(\d+) = ') +# 编号元数据引用 +_Reference = re.compile(r'!(\d+)') +# `!name = ...` 形式的命名元数据(首字符为字母,故不会命中 `!N = `) +_NamedDefinition = re.compile(r'^!([A-Za-z][\w.]*)\s*=\s*(.*)$') +# `!{ ... }` 列表形式 +_ListForm = re.compile(r'^!\{(.*)\}$', re.DOTALL) +# 纯 id 引用 `!N` +_PureId = re.compile(r'^!(\d+)$') + + +def _SplitTopLevel(Content: str) -> list[str]: + """按顶层逗号切分 !{ ... } 的条目(跳过字符串字面量与嵌套括号内的逗号)。""" + Items: list[str] = [] + Depth: int = 0 + Buf: list[str] = [] + i: int = 0 + n: int = len(Content) + while i < n: + Ch: str = Content[i] + if Ch == '"': + j: int = i + 1 + while j < n: + if Content[j] == '\\': + j += 2 + continue + if Content[j] == '"': + j += 1 + break + j += 1 + Buf.append(Content[i:j]) + i = j + continue + if Ch in '{[(': + Depth += 1 + elif Ch in '}])': + Depth -= 1 + elif Ch == ',' and Depth == 0: + Items.append(''.join(Buf).strip()) + Buf = [] + i += 1 + continue + Buf.append(Ch) + i += 1 + Tail: str = ''.join(Buf).strip() + if Tail: + Items.append(Tail) + return Items + + +def _ListItems(Rhs: str) -> list[str] | None: + """`!{ a, b }` -> ['a', 'b'];非列表形式返回 None。""" + m = _ListForm.match(Rhs.strip()) + if not m: + return None + return _SplitTopLevel(m.group(1)) + + +def _RewriteReferences(Line: str, Mapping: dict[int, int]) -> str: + """改写一行里对编号元数据的引用,跳过字符串字面量与 `;` 注释。""" + if '!' not in Line: + return Line + if '"' not in Line and ';' not in Line: + # 元数据行绝大多数不含字符串/注释:整体正则替换,避免逐字符扫描 + return _Reference.sub( + lambda m: '!' + str(Mapping.get(int(m.group(1)), int(m.group(1)))), Line) + Out: list[str] = [] + i: int = 0 + n: int = len(Line) + while i < n: + Ch: str = Line[i] + if Ch == '"': + j: int = i + 1 + while j < n: + if Line[j] == '\\': + j += 2 + continue + if Line[j] == '"': + j += 1 + break + j += 1 + Out.append(Line[i:j]) + i = j + continue + if Ch == ';': + Out.append(Line[i:]) + break + if Ch == '!': + m = _Reference.match(Line, i) + if m: + New: int | None = Mapping.get(int(m.group(1))) + Out.append('!' + str(New) if New is not None else m.group(0)) + i = m.end() + continue + Out.append(Ch) + i += 1 + continue + Out.append(Ch) + i += 1 + return ''.join(Out) + + +def SanitizeIRMetadata(text: str) -> str: + """让拼接后的 IR 元数据合法:编号全局唯一 + 命名元数据只留一份。 + + 无元数据(没有任何以 `!` 开头的行)时原样返回 —— 严格 no-op。 + """ + if '!' not in text: + return text + Segments: list[str] = text.split('\n') + LineCount: int = len(Segments) + + # 1) 元数据块 = 连续的以 '!' 开头的行。 + # 每个模块的元数据(命名行 + 编号定义)是连续的一段;IR 行把它断开, + # 所以块边界即模块边界,块内共享同一套局部编号空间。 + Blocks: list[tuple[int, int]] = [] # [Start, End) + i: int = 0 + while i < LineCount: + if Segments[i].startswith('!'): + j: int = i + while j < LineCount and Segments[j].startswith('!'): + j += 1 + Blocks.append((i, j)) + i = j + else: + i += 1 + if not Blocks: + return text + + # 2) 每行归属的块:块内行归本块;块之间的行(IR、命名行)归「其后第一个 + # 块」—— 因为每个模块的 IR 之后紧跟它自己的元数据块(含被引用的 !N)。 + Owner: list[int] = [-1] * LineCount + PrevEnd: int = 0 + for Index, (Start, End) in enumerate(Blocks): + for k in range(PrevEnd, End): + Owner[k] = Index + PrevEnd = End + + # 3) 块内按定义出现顺序重新编号(局部 id -> 全局 id) + Lines: list[str] = list(Segments) + BlockMaps: list[dict[int, int]] = [] + BlockIsIdentity: list[bool] = [] + Counter: int = 0 + for Start, End in Blocks: + Mapping: dict[int, int] = {} + Identity: bool = True + for k in range(Start, End): + m = _Definition.match(Lines[k]) + if not m: + continue + Local: int = int(m.group(1)) + Mapping[Local] = Counter + if Local != Counter: + Identity = False + Lines[k] = '!' + str(Counter) + ' = ' + Lines[k][m.end():] + Counter += 1 + BlockMaps.append(Mapping) + BlockIsIdentity.append(Identity) + + # 4) 改写全文引用(定义体右值、define/指令行的 !dbg、命名元数据列表) + for k in range(LineCount): + Index: int = Owner[k] + if Index < 0 or BlockIsIdentity[Index] or not BlockMaps[Index]: + continue + Fixed: str = _RewriteReferences(Lines[k], BlockMaps[Index]) + if Fixed != Lines[k]: + Lines[k] = Fixed + + # 5) 定义体索引(新 id -> 右值文本),供命名元数据判重 + BodyOf: dict[int, str] = {} + for k in range(LineCount): + m = _Definition.match(Lines[k]) + if m: + BodyOf[int(m.group(1))] = Lines[k][m.end():].strip() + + # 6) 命名元数据:同名多份合并成一份。 + # - !llvm.dbg.cu:全部条目聚合(每个 DICompileUnit 都必须被列出,不能去重) + # - 其它(!llvm.module.flags、!llvm.ident ...):条目内容(id 引用解析成 + # 节点正文)完全相同的多份只留一份,不同则聚合。 + NamedLines: dict[str, list[int]] = {} + for k in range(LineCount): + m = _NamedDefinition.match(Lines[k]) + if m: + NamedLines.setdefault(m.group(1), []).append(k) + + Dropped: set[int] = set() + for Name, Indices in NamedLines.items(): + if len(Indices) == 1 and Name != 'llvm.dbg.cu': + continue + First: int = Indices[0] + Cr: str = Segments[First][len(Segments[First].rstrip('\r')):] + Items: list[str] = [] + SeenItems: set[str] = set() + SeenBodies: set[tuple[str, ...]] = set() + Single: bool = True + for k in Indices: + Rhs: str = _NamedDefinition.match(Lines[k]).group(2).strip() + ListItems: list[str] | None = _ListItems(Rhs) + if ListItems is None: + Single = False + break + Bodies: tuple[str, ...] = tuple( + BodyOf.get(int(_PureId.match(Item).group(1)), Item) if _PureId.match(Item) else Item + for Item in ListItems + ) + if Name != 'llvm.dbg.cu': + if Bodies in SeenBodies: + continue + SeenBodies.add(Bodies) + for Item in ListItems: + if Item not in SeenItems: + SeenItems.add(Item) + Items.append(Item) + if not Single: + # 非 `!{ ... }` 列表形式(实际 IR 里不出现):保留首份,其余丢弃, + # 避免出现同名命名元数据被定义多次。 + Dropped.update(Indices[1:]) + continue + Rebuilt: str = '!%s = !{ %s }%s' % (Name, ', '.join(Items), Cr) + if Rebuilt != Segments[First]: + Lines[First] = Rebuilt + Dropped.update(Indices[1:]) + + if not Dropped and Lines == Segments: + return text + return '\n'.join(Line for k, Line in enumerate(Lines) if k not in Dropped) diff --git a/lib/core/LLVMCG/StructGen.py b/lib/core/LLVMCG/StructGen.py index a705710..56f6a38 100644 --- a/lib/core/LLVMCG/StructGen.py +++ b/lib/core/LLVMCG/StructGen.py @@ -134,6 +134,12 @@ class StructGenMixin: def finalize(self) -> str: self._set_Vtable_initializers() self._fix_global_var_init() + # 函数级 DWARF(--dwarf / --asan):把 Module 落成 .ll 文本之前补模块级 + # !llvm.dbg.cu 与 !llvm.module.flags(Debug Info Version=3);未启用时 + # _GetDebugInfo() 返回 None,不做任何事(零影响)。 + _DbgInfo = self._GetDebugInfo() + if _DbgInfo is not None: + _DbgInfo.Finalize() ir_text: str try: ir_text = str(self.module) diff --git a/lib/core/Translator/BaseTranslator.py b/lib/core/Translator/BaseTranslator.py index 6d16f5d..e08c987 100644 --- a/lib/core/Translator/BaseTranslator.py +++ b/lib/core/Translator/BaseTranslator.py @@ -85,6 +85,9 @@ class BaseTranslatorMixin: # 注册策略:False=种子 CLI 单模块(在 main 入口注册);True=Projectrans # 多模块(每模块 @llvm.global_ctors 自注册,见 BaseGen._InstallHotspotSitesModuleLocal) self.HotspotSitesModuleLocal: bool = False + # 函数级 DWARF 调试元数据(--dwarf / --asan 自动开启):False=未启用 + # (零影响),True=生成的 .ll 携带每函数 !dbg(见 LlvmGenerator.GenerateCCode) + self.DwarfEnabled: bool = False # 导出表 self.Exportable = Exportable(filename='') diff --git a/lib/core/Translator/LlvmGenerator.py b/lib/core/Translator/LlvmGenerator.py index bf82ffd..b0986fa 100644 --- a/lib/core/Translator/LlvmGenerator.py +++ b/lib/core/Translator/LlvmGenerator.py @@ -665,6 +665,10 @@ class LlvmGeneratorMixin: self.LlvmGen.setup_from_symbol_table(self.SymbolTable) if hasattr(self, 'CurrentFile') and self.CurrentFile: self.LlvmGen._set_source_info(self.CurrentFile) + # 函数级 DWARF 调试元数据(--dwarf / --asan):透传开关给本模块的 + # LlvmCodeGenerator;未启用时 dwarf_enabled=False → 不构造 DI、不挂 !dbg, + # 生成的 .ll 与改动前逐字节一致(零影响) + self.LlvmGen.dwarf_enabled = bool(getattr(self, 'DwarfEnabled', False)) # --- 修复:共享 vtable 状态跨模块 --- # 根因:LlvmGen 每模块重新创建,class_vtable/class_methods 等是实例级属性,