Files
2026-09-17 14:52:37 +08:00

181 lines
8.5 KiB
Python
Raw Permalink 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 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 <file>
# 并丢弃整份 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