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"] 上;函数体发射期间把 builder 的 默认调试位置设为 "当前函数 SP + 当前语句行/列" 的 DILocation,于是每条新建 指令(含所有 call)都自动带本函数 scope 的 !dbg —— 既有语句级粒度,又保证 llc 的 "inlinable function call ... must have a !dbg location" 校验通过、 不丢弃 DI。没有语句上下文时回退到函数入口位置(line=def 行、column=def 列)。 仅在 --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, }) # ---------- 语句/指令级位置:按 Builder 上下文管理当前 DILocation ---------- # # llvmlite 的 IRBuilder 支持"预设调试位置自动挂载":IRBuilder._insert 里 # `if self.debug_metadata is not None and 'dbg' not in instr.metadata: # instr.metadata['dbg'] = self.debug_metadata` # 即设置 builder.debug_metadata 之后,该 builder 新建的每条指令都会自动带 # `!dbg !N`。函数带 DISubprogram 时 llc 要求函数体内每个 call(对同模块有 # 定义的被调函数)都有 !dbg,否则报 # inlinable function call in a function with debug info must have a !dbg # warning: ignoring invalid debug info in # 并丢弃整份 DI(llvm-dwarfdump 拿不到行表)。 # # 当前 SP/入口位置直接存放在 IRBuilder 对象上(_TpcDbgSp / _TpcDbgEntry), # 而不是发射器自身:builder 是"每函数一个"的(见 LlvmCodeGenerator. # _NewIrBuilder 的唯一调用点 _EmitFunctionLlvm,且已有 body 的函数被 # existing_blocks 守卫提前返回),因此 # * 嵌套/特化函数发射会临时换 builder,恢复后自动回到外层函数的 SP, # 不会把 scope 指向别的函数; # * 新函数 = 新 builder = debug_metadata 默认 None,天然不跨函数/模块残留, # 也无需在函数结束时强制清理(PopFunction 只是显式停用,防误用)。 def PushFunction(self, Builder: ir.IRBuilder, func: ir.Function, Line: int, Column: int) -> ir.DIValue: """进入函数体:建 DISubprogram(func.metadata["dbg"])并把 Builder 的 默认调试位置设为入口 DILocation(line=def 行、column=def 列)。 返回入口 DILocation。此后该 Builder 新建的每条指令都自动带该位置,直到 SetStatement 覆盖 —— 保证函数体内所有 call 至少有合法 !dbg(llc 不再 因缺 !dbg 丢弃 DI)。 """ Entry: ir.DIValue = self.AttachFunction(func, Line, Column) Builder._TpcDbgSp = func.metadata["dbg"] Builder._TpcDbgEntry = Entry Builder.debug_metadata = Entry return Entry def SetStatement(self, Builder: ir.IRBuilder, Line: int, Column: int) -> None: """把 Builder 的默认调试位置切到(Line, Column)。 该语句之后新建的指令都带这个位置(scope 恒为当前函数 SP)。未 PushFunction 过的 Builder(无 DI 的函数、模块级代码、lambda/async 自建 builder)不做 任何事 —— 这些函数没有 DISubprogram,llc 不要求 !dbg,也绝不改动 builder.debug_metadata 的默认值。Line<=0(AST 无位置)回退到 CurrentOrEntry()。 """ Sp: ir.DIValue | None = getattr(Builder, '_TpcDbgSp', None) if Sp is None: return if Line <= 0: Builder.debug_metadata = self.CurrentOrEntry(Builder) return Builder.debug_metadata = self.Module.add_debug_info("DILocation", { "line": Line, "column": Column if Column > 0 else 1, "scope": Sp, }) def CurrentOrEntry(self, Builder: ir.IRBuilder) -> ir.DIValue | None: """当前默认位置;尚未设置过语句位置时回退到函数入口 DILocation。 (add_debug_info 自带 (kind, operands) 缓存,同 (SP, 行, 列) 复用同一 DILocation 节点,不会因逐语句调用撑大元数据。) """ Current: ir.DIValue | None = getattr(Builder, 'debug_metadata', None) if Current is not None: return Current return getattr(Builder, '_TpcDbgEntry', None) def Clear(self, Builder: ir.IRBuilder) -> None: """停用该 Builder 的默认调试位置(debug_metadata 置回 None)。""" Builder.debug_metadata = None def PopFunction(self, Builder: ir.IRBuilder) -> None: """离开函数体:停用默认位置并作废该 Builder 的 SP 绑定。 Builder 随后被丢弃(或由调用方恢复为外层函数的 builder),SP 上下文不会 跨函数残留。 """ self.Clear(Builder) Builder._TpcDbgSp = None Builder._TpcDbgEntry = None 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