Files
TransPyC/vdb.md
2026-07-18 19:25:40 +08:00

23 KiB
Raw Blame History

TransPyC 调试器vdb设计文档

状态:设计阶段,暂不实现
创建时间2026-06-26
目标:为 TransPyC 编译的程序提供源码级调试能力,支持断点、变量查看、单步、调用栈、异常栈跟踪


1. 背景与动机

TransPyC 将 Python 子集编译为 LLVM IR 再编译为原生机器码。当前编译产物完全不含调试信息(无 DWARF/PDB运行时崩溃只能看到错误码和消息无法定位源码行、查看变量、回溯调用栈。

本设计文档规划一个 TransPyC 专用调试器 vdb,目标是:

  • 源码级断点(break main.py:30
  • 变量查看(print x / info locals
  • 单步执行(step / next
  • 调用栈回溯(backtrace
  • 异常栈跟踪(raise 时记录栈帧)
  • 硬错误捕获segfault / 溢出)

2. 现状分析

2.1 已有可复用资产

资产 位置 复用方式
变量映射三件套 variables/_direct_values/_reg_values lib/core/LLVMCG/BaseGen.py:79-81 序列化到 .tpd 供变量查看
变量类型信息 var_type_info/var_signedness/var_struct_class lib/core/LLVMCG/BaseGen.py:82-112,124 序列化为 VarBinding.TypeDesc
源码行号跟踪 _current_lineno/_set_node_info/_get_source_line lib/core/LLVMCG/BaseGen.py:126-127,138,297-440 扩展为 SourceMap
类元信息表 class_members/class_member_defaults/class_parent/Vtables lib/core/LLVMCG/BaseGen.py:85-100 序列化为 TypeMeta
符号表序列化 SymbolTable.ToDict lib/core/SymbolTable.py:265-319 vdb 直接加载,无需重建
异常机制 __eh_msg_out__/__eh_code_out__ lib/core/Handles/HandlesRaise.py / HandlesTry.py / HandlesExprCall.py 扩展携带栈信息
VLogger compile_error(file, line, code) 三元组 lib/core/VLogger.py:286 复用错误位置格式
本地堆指针记录 _local_heap_ptrs/_var_to_heap_ptr lib/core/LLVMCG/MemoryOps.py:7-15 堆对象追踪起点

2.2 关键缺口

能力 现状 缺口说明
DWARF 调试信息 完全未生成 lib/ 下无任何 DIBuilder/!dbg/DICompileUnit 调用
PDB 调试信息 完全未生成 Windows 下 WinDbg/VS 无法源码级调试
源码行 → IR 指令映射 无数据结构 最核心缺口,需从零构建 SourceMap
变量名 → 运行时内存 ⚠️ 仅编译期 ephemeral Gen.variables 随 Gen 实例销毁,需持久化
类型元信息运行时查询 无 RTTI/反射 class_members 等均编译期状态,未发射到二进制
运行时调用栈 完全无 异常仅传 (code, msg),无栈帧链
硬错误处理 __builtin_trap 仅声明未实现segfault/SIGFPE 无捕获
断点/单步/变量查看 完全无 lib/ 下搜索 breakpoint/step/watch 零命中

3. 路线选择

3.1 方案 A纯 DWARF + GDB/LLDB

优点:生态成熟,无需自研前端
缺点

  • llvmlite 的 DIBuilder 支持度需验证
  • Python 语义与 C 不完全对应(self 是指针但 Python 视为对象)
  • SHA1 命名导致符号名难读
  • 泛型特化类型(Stack[int])在 DWARF 中表达困难
  • 跨模块符号(6d71a13705da41ad.main)可读性差

3.2 方案 B纯自研 vdb

优点:完全匹配 Python 语义,可显示原始源码,理解泛型/SHA1/跨模块
缺点:开发量大,需自研调试协议与前端

3.3 方案 C混合推荐

  • 生成轻量 DWARF仅行号 + 变量名)让 GDB 可用作后备
  • 同时生成 TransPyC 专用调试元数据 .tpd 文件
  • vdb 前端优先使用 .tpd,可降级到 GDB 后端

本设计采用方案 C,但优先实现 .tpd + vdbDWARF 作为可选增强。


4. 核心数据结构

4.1 .tpd 文件格式TransPyC Debug

每个 .ll 文件配套生成一个 .tpd 文件JSON 格式),包含以下结构:

from dataclasses import dataclass, field
from typing import Any

@dataclass
class IrLocation:
    """单条 IR 指令的源码位置映射"""
    FuncName: str          # mangled 函数名(含 SHA1 头,如 '6d71a13705da41ad.main'
    BlockName: str         # 基本块名(如 'entry'、'if.then'
    InstrIdx: int          # 指令在块中的索引
    SourceFile: str        # 源码相对路径
    LineNo: int            # 源码行号
    ColNo: int             # 列号

@dataclass
class VarBinding:
    """变量绑定信息(作用域内)"""
    VarName: str           # Python 变量名(如 'x'、'self'
    LlvmName: str          # alloca 名或 SSA如 '%x'、'%".1"'
    TypeDesc: str          # 类型描述('i32' / 'Stack[int]*' / 'i8*'
    IsSigned: bool         # 是否有符号整数
    StructClass: str | None  # 所属结构体类名(如 'Stack[int]'
    StartLine: int         # 作用域起始行
    EndLine: int           # 作用域结束行
    Storage: str           # 'stack' | 'register' | 'direct' | 'global'
    StackOffset: int | None  # 栈偏移(仅 'stack'

@dataclass
class TypeMeta:
    """类型元数据"""
    TypeName: str          # 'Stack[int]' / 'IntPair' / 'memory_block'
    Members: list[tuple[str, str, int]]  # (成员名, 类型, 字节偏移)
    Size: int              # 总大小(字节)
    Align: int             # 对齐
    Parent: str | None     # 父类名
    IsException: bool      # 是否异常类
    ExceptionCode: int     # 异常码(仅 IsException=True
    HasVTable: bool        # 是否有虚表
    IsPacked: bool         # 是否紧凑布局

@dataclass
class FuncSig:
    """函数签名"""
    FuncName: str          # mangled 名
    RawName: str           # 原始 Python 名(去 SHA1
    ReturnType: str        # 返回类型描述
    ParamNames: list[str]
    ParamTypes: list[str]
    IsExport: bool         # 是否导出(@t.CExport
    IsMethod: bool         # 是否实例方法
    ClassName: str | None  # 所属类名(方法)
    SourceFile: str
    StartLine: int
    EndLine: int
    HasException: bool     # 是否可能抛异常(有 __eh_msg_out__

@dataclass
class TpdFile:
    """完整的 .tpd 调试信息文件"""
    SourceMap: list[IrLocation]
    VarScopes: dict[str, list[VarBinding]]  # FuncName -> bindings
    TypeMeta: dict[str, TypeMeta]
    FuncSignatures: dict[str, FuncSig]
    ExceptionCodes: dict[int, str]          # code -> exception name
    SourceFiles: dict[str, list[str]]       # file -> 源码行列表(供 vdb 显示上下文)
    ModuleSha1: str | None
    Version: str = "1.0"

4.2 序列化格式

.tpd 文件使用 JSON字段名采用 PascalCase与代码一致。示例

{
  "Version": "1.0",
  "ModuleSha1": "6d71a13705da41ad",
  "SourceFiles": {
    "App/main.py": ["import t", "import stdio", ...]
  },
  "SourceMap": [
    {"FuncName": "main", "BlockName": "entry", "InstrIdx": 0, "SourceFile": "App/main.py", "LineNo": 255, "ColNo": 0},
    ...
  ],
  "VarScopes": {
    "6d71a13705da41ad.test_generic_stack": [
      {"VarName": "s", "LlvmName": "%s", "TypeDesc": "Stack[int]*", "IsSigned": false, "StructClass": "Stack[int]", "StartLine": 66, "EndLine": 73, "Storage": "stack", "StackOffset": 0}
    ]
  },
  "TypeMeta": {
    "Stack[int]": {
      "TypeName": "Stack[int]",
      "Members": [["data", "[16 x i32]", 0], ["top", "i32", 64]],
      "Size": 68,
      "Align": 4,
      "Parent": null,
      "IsException": false,
      "ExceptionCode": 0,
      "HasVTable": false,
      "IsPacked": false
    }
  },
  "ExceptionCodes": {
    "1": "ValueError",
    "2": "TypeError",
    "99": "Exception",
    "100": "MyCustomError"
  }
}

5. 实现阶段

5.1 Phase 1源码级断点最小可用

目标break main.py:30 + continue + run

5.1.1 编译器改动

位置lib/core/LLVMCG/BaseGen.py

  1. 扩展 _set_node_info:记录当前指令的源码位置到 SourceMap
def _set_node_info(self, Node: ast.AST) -> None:
    self._current_lineno = getattr(Node, 'lineno', 0)
    self._current_col = getattr(Node, 'col_offset', 0)
    self._current_node_info = f" [line {self._current_lineno}, col {self._current_col}]"
    # 新增:记录到 SourceMap
    if self.func and self.builder and self.builder.block:
        loc: IrLocation = IrLocation(
            FuncName=self.func.name,
            BlockName=self.builder.block.name,
            InstrIdx=len(self.builder.block.instructions),
            SourceFile=self._current_source_file or '',
            LineNo=self._current_lineno,
            ColNo=self._current_col
        )
        self._source_map.append(loc)
  1. 插入断点桩:在每行源码对应的首条 IR 指令前插入
; 运行时断点桩声明
declare void @__tpc_breakpoint(i32 %line, i8* %file)

; 在 main.py:255 的首条指令前插入
%src_file_ptr = getelementptr [12 x i8], [12 x i8]* @.src.main_py, i32 0, i32 0
call void @__tpc_breakpoint(i32 255, i8* %src_file_ptr)
  1. 条件性插入:仅当 --debug 编译选项启用时插入release 构建完全剥离

  2. 生成 .tpd 文件:函数编译结束时,序列化 SourceMap.tpd

5.1.2 运行时库

新建 includes/vdb_runtime.py

import t
import c

# 全局断点表(行号 -> 是否启用)
_Breakpoints: t.CArray[t.CInt, 65536] | t.CPtr = [0]
# 当前调试器状态0=运行, 1=暂停)
_DebugState: t.CInt = 0
# 当前源码文件名(用于断点匹配)
_CurrentFile: t.CArray[t.CInt8T, 256] | t.CPtr = [0]

@t.CExport
def __tpc_breakpoint(line: t.CInt, file: t.CPtr) -> None:
    """断点桩入口,由编译器在每个源码行前插入调用"""
    # 检查是否启用断点
    if _Breakpoints[line] == 0:
        return
    # 命中断点,陷入调试器循环
    _DebugState = 1
    _tpc_debug_loop(line, file)

def _tpc_debug_loop(line: t.CInt, file: t.CPtr) -> None:
    """调试器主循环,等待用户命令"""
    # 通过 IPC 或 stdio 与 vdb 前端通信
    # 解析命令continue / step / next / print / info / backtrace
    while _DebugState == 1:
        cmd: t.CArray[t.CInt8T, 256] | t.CPtr = [0]
        stdio.scanf("%s", cmd)
        # ... 命令分派

5.1.3 vdb 前端

新建 lib/vdb/ 目录:

lib/vdb/
├── __init__.py
├── TpdLoader.py          # .tpd 文件加载器
├── BreakpointManager.py  # 断点管理
├── DebugSession.py       # 调试会话主循环
├── CommandParser.py      # 命令解析
└── SourceView.py         # 源码显示

核心命令

  • break <file>:<line> / b <file>:<line> — 设置断点
  • delete <n> / d <n> — 删除断点
  • continue / c — 继续执行
  • run / r — 启动程序
  • list / l — 显示当前源码上下文
  • info breakpoints — 列出断点

5.2 Phase 2变量查看

目标print x / info locals / info args

5.2.1 编译器改动

位置lib/core/LLVMCG/FuncGen.pylib/core/Handles/HandlesFunctions.py

函数编译结束时,序列化变量绑定:

def _serialize_var_scope(self, FuncName: str) -> list[VarBinding]:
    bindings: list[VarBinding] = []
    for var_name, llvm_val in self.variables.items():
        if llvm_val is None:
            continue
        binding: VarBinding = VarBinding(
            VarName=var_name,
            LlvmName=llvm_val.name if hasattr(llvm_val, 'name') else '',
            TypeDesc=self._describe_llvm_type(llvm_val.type),
            IsSigned=self.var_signedness.get(var_name, True),
            StructClass=self.var_struct_class.get(var_name),
            StartLine=self._func_start_line,
            EndLine=self._func_end_line,
            Storage='stack',
            StackOffset=self._get_alloca_offset(llvm_val)  # 新增:记录栈偏移
        )
        bindings.append(binding)
    # 同样处理 _direct_values 和全局变量
    return bindings

5.2.2 运行时变量读取

调试器通过以下方式读取变量值:

  1. 栈变量:通过 StackOffset + 当前栈帧基址读取
  2. 全局变量:通过符号表查找全局符号地址
  3. 寄存器变量:需调试器理解 SSA 值较复杂Phase 2 可跳过,仅支持 stack

5.2.3 类型渲染

def RenderValue(llvm_val: bytes, type_meta: TypeMeta, type_desc: str) -> str:
    """根据类型渲染变量值"""
    if type_desc == 'i32':
        return str(int.from_bytes(llvm_val, 'little', signed=True))
    elif type_desc == 'i8*':
        # 尝试作为 C 字符串读取
        return f'"{read_cstring(llvm_val)}"'
    elif type_desc.endswith('*') and type_meta:
        # 结构体指针:展开成员
        return RenderStruct(llvm_val, type_meta)
    ...

5.3 Phase 3单步执行

目标step / next / finish

基于 SourceMap 实现:

  • step(步入):在下一行源码的首条 IR 指令前设临时断点,包括被调用函数内部
  • next(步过):在当前函数的下一行设临时断点,跳过函数调用
  • finish(步出):在当前函数的返回点设临时断点

实现

  1. 查 SourceMap 找到当前行的下一个源码行
  2. 设置临时断点(一次性)
  3. continue
  4. 命中后自动删除临时断点

5.4 Phase 4调用栈跟踪

目标backtrace / bt / frame <n>

5.4.1 编译器改动

每个函数入口/出口插入栈帧维护调用:

; 函数入口
call void @__tpc_push_frame(
    i8* getelementptr([N x i8], [N x i8]* @.func.name, i32 0, i32 0),  ; 函数名
    i8* getelementptr([M x i8], [M x i8]* @.src.file, i32 0, i32 0),   ; 源文件
    i32 255                                                              ; 起始行
)

; 函数出口(每个 ret 前)
call void @__tpc_pop_frame()

5.4.2 运行时栈结构

import t

@t.CStruct
class FrameInfo:
    FuncName: t.CPtr        # i8* 函数名
    SrcFile: t.CPtr         # i8* 源文件
    LineNo: t.CInt          # 当前行号(动态更新)
    PrevFrame: 'FrameInfo' | t.CPtr  # 上一帧(链表)

# 全局栈顶
_FrameTop: FrameInfo | t.CPtr = t.CPtr(0)

@t.CExport
def __tpc_push_frame(func_name: t.CPtr, src_file: t.CPtr, line: t.CInt) -> None:
    frame: FrameInfo | t.CPtr = c.cast(FrameInfo, mb.alloc(FrameInfo.__sizeof__()))
    frame.FuncName = func_name
    frame.SrcFile = src_file
    frame.LineNo = line
    frame.PrevFrame = _FrameTop
    _FrameTop = frame

@t.CExport
def __tpc_pop_frame() -> None:
    if _FrameTop != t.CPtr(0):
        old: FrameInfo | t.CPtr = _FrameTop
        _FrameTop = _FrameTop.PrevFrame
        mb.free(old)

5.4.3 断点桩更新当前行

__tpc_breakpoint 命中时,更新 _FrameTop.LineNo

@t.CExport
def __tpc_breakpoint(line: t.CInt, file: t.CPtr) -> None:
    if _Breakpoints[line] == 0:
        return
    if _FrameTop != t.CPtr(0):
        _FrameTop.LineNo = line
    _DebugState = 1
    _tpc_debug_loop(line, file)

5.5 Phase 5异常栈跟踪

目标raise ValueError("msg") 时输出完整调用栈

5.5.1 编译器改动

位置lib/core/Handles/HandlesRaise.py

_HandleRaiseLlvm 中,抛异常前捕获栈:

def _HandleRaiseLlvm(self, Node: ast.Raise) -> None:
    # ... 原有错误码计算 ...
    # 新增:捕获调用栈
    capture_call = self.Gen.builder.call(
        self._get_or_declare_function('__tpc_capture_trace', ir.FunctionType(ir.VoidType(), [])),
        []
    )
    # ... 原有异常传播逻辑 ...

5.5.2 异常结构扩展

__eh_msg_out__i8**)扩展为指向包含栈的结构:

import t

@t.CStruct
class ExceptionInfo:
    Code: t.CInt
    Message: t.CArray[t.CInt8T, 256] | t.CPtr
    TraceDepth: t.CInt
    TraceFuncs: t.CArray[t.CPtr, 64]    # 函数名指针数组
    TraceLines: t.CArray[t.CInt, 64]    # 对应行号
    TraceFiles: t.CArray[t.CPtr, 64]    # 对应源文件

5.5.3 未捕获异常处理

main 函数返回前检查异常,打印栈:

def main() -> t.CInt:
    # ... 用户代码 ...
    # 编译器自动插入:检查未捕获异常
    if _UnhandledException != t.CPtr(0):
        _tpc_print_trace(_UnhandledException)
        return 1
    return testcheck.end()

5.6 Phase 6硬错误捕获

目标segfault / 除零 / 栈溢出时进入调试器

5.6.1 信号处理器注册

import t
import c

@t.CExport
def __tpc_signal_handler(sig: t.CInt) -> None:
    """OS 信号处理入口"""
    __tpc_capture_trace()
    # 打印信号类型
    if sig == 11:  # SIGSEGV
        stdio.printf("Segmentation fault\n")
    elif sig == 8:  # SIGFPE
        stdio.printf("Arithmetic exception\n")
    # 陷入调试器
    _tpc_debug_loop(0, t.CPtr(0))

def __tpc_install_signal_handlers() -> None:
    """在 main 入口前自动调用"""
    c.signal(11, __tpc_signal_handler)  # SIGSEGV
    c.signal(8, __tpc_signal_handler)   # SIGFPE

5.6.2 编译器改动

main 函数入口自动插入 __tpc_install_signal_handlers() 调用(仅 --debug 模式)。


6. vdb 命令参考

6.1 启动与控制

命令 缩写 说明
run [args] r 启动被调试程序
continue c 继续执行至下一断点
step s 单步进入函数
next n 单步跳过函数
finish f 执行至当前函数返回
quit q 退出调试器

6.2 断点管理

命令 缩写 说明
break <file>:<line> b 设置源码行断点
break <func> b 设置函数入口断点
tbreak <file>:<line> tb 临时断点(命中后删除)
delete <n> d 删除断点 n
info breakpoints i b 列出所有断点

6.3 变量查看

命令 缩写 说明
print <expr> p 打印表达式值
info locals i lo 列出当前局部变量
info args i ar 列出函数参数
display <expr> - 每次暂停自动显示
set <var>=<val> - 修改变量值

6.4 栈操作

命令 缩写 说明
backtrace bt 显示调用栈
frame <n> fr 切换到第 n 帧
up - 向上切换一帧
down do 向下切换一帧

6.5 源码浏览

命令 缩写 说明
list l 显示当前行上下文
list <file>:<line> l 显示指定位置
info source i so 显示当前源文件信息

6.6 类型与结构

命令 缩写 说明
ptype <typename> pt 显示类型定义
info types i ty 列出所有类型
info exceptions i ex 列出异常码表

7. 实现优先级

优先级 阶段 价值 改动量
P0 Phase 1源码断点+ Phase 2变量查看 80% 调试价值
P1 Phase 4调用栈 崩溃定位关键
P2 Phase 5异常栈 Python 语义匹配
P3 Phase 3单步 体验提升 中(依赖 Phase 1
P4 Phase 6硬错误 兜底保护
P5 DWARF 生成 GDB 互操作 大(可选)

8. 风险与考量

8.1 性能开销

  • 断点桩:仅 --debug 构建启用release 完全剥离
  • IR 膨胀:每行插入断点桩约增加 10-20% IR 体积,可接受
  • 运行时开销:断点桩检查为单次数组访问 + 分支预测,开销极低
  • 栈帧维护:每函数调用增加 2 次 push/pop_frame,约 5-10% 调用开销

8.2 兼容性

  • SHA1 命名vdb 需理解 SHA1 前缀,通过 ModuleSha1 字段还原为 模块名.函数名 显示
  • 泛型特化Stack[int] 需作为完整类型名处理TypeMeta 中保留特化名
  • 跨模块.tpd 文件按模块生成vdb 启动时合并所有相关 .tpd
  • main 特化main 函数无 SHA1 头vdb 需特殊处理

8.3 安全性

  • 栈帧分配:使用 mb.alloc 而非 c.malloc,确保与 TransPyC 内存管理一致
  • 信号处理器:异步信号中避免调用非异步信号安全函数,仅设置标志位

8.4 限制

  • 优化构建:高优化级别(-O2+)下变量可能被优化掉,.tpd 中的 VarBinding 可能失效
  • 内联函数:内联后栈帧信息丢失,需在 .tpd 中标记 IsInline
  • 尾调用优化:破坏调用栈链,需禁用或特殊处理

9. 文件结构规划

lib/vdb/                          # vdb 调试器前端
├── __init__.py
├── TpdLoader.py                  # .tpd 文件加载与合并
├── BreakpointManager.py          # 断点管理
├── DebugSession.py               # 调试会话主循环
├── CommandParser.py              # 命令解析
├── SourceView.py                 # 源码显示
├── ValueRenderer.py              # 变量值渲染
├── StackWalker.py                # 调用栈遍历
└── IpcChannel.py                 # 与被调试进程的 IPC 通信

lib/core/LLVMCG/
└── DebugInfoGen.py               # 新增:.tpd 生成器(编译器侧)

includes/
└── vdb_runtime.py                # 运行时断点/栈帧/信号处理库

10. 与现有系统的集成点

10.1 编译器集成

  • Phase2Translator.py:在生成 .ll 后,调用 DebugInfoGen.GenerateTpd(module, gen, output_path)
  • BaseGen.py_set_node_info 扩展记录 SourceMap函数编译结束序列化 VarScope
  • HandlesFunctions.py:函数入口/出口插入 __tpc_push_frame/__tpc_pop_frame(仅 debug 模式)
  • HandlesRaise.py_HandleRaiseLlvm 插入 __tpc_capture_trace

10.2 project.json 配置

新增 debug 选项:

{
  "options": {
    "debug": true,
    "debug_info": "tpd"
  }
}
  • debug: true — 启用调试构建(插入断点桩、栈帧维护、信号处理)
  • debug_info: "tpd" — 生成 .tpd 文件(默认)
  • debug_info: "dwarf" — 生成 DWARF未来支持
  • debug_info: "both" — 同时生成(未来支持)

10.3 命令行集成

# 编译带调试信息
transpyc build --debug

# 直接启动调试器
transpyc debug ./output/MyApp.exe
# 等价于
vdb ./output/MyApp.exe

11. 开放问题

  1. llvmlite DIBuilder 支持度:需验证 llvmlite 是否暴露 DIBuilder 接口,决定 DWARF 生成可行性
  2. IPC 协议vdb 前端与被调试进程的通信方式(管道/Socket/共享内存)待定
  3. 远程调试:是否支持远程调试(如 ViperOS 裸机目标)需进一步设计
  4. 表达式求值print x.y.z 等复杂表达式是否需要完整 Python 解释器,还是简化为路径访问
  5. 多线程TransPyC 当前是否支持多线程,调试器如何处理线程切换

12. 参考文档

  • 现状分析:基于 2026-06-26 对 lib/core/lib/Projectrans/ 的完整搜索
  • 异常机制:wiki/09-exceptions.md
  • 导入机制:wiki/10-imports.md
  • 符号表:lib/core/SymbolTable.py
  • 类型系统:lib/core/TypeSpec.py / lib/core/Handles/HandlesBase.py
  • LLVM 生成:lib/core/LLVMCG/BaseGen.py / FuncGen.py / MemoryOps.py

本文档为设计阶段产物,暂不实现。待社区需求或开发优先级提升后,按 Phase 1 → 2 → 4 → 5 → 3 → 6 顺序推进。