# llvmlite 设计方案 用 TransPyC 自身实现的轻量级 LLVM IR 文本生成库,替代 Python `llvmlite` 依赖,为自举(见 `wiki/13-bootstrapping.md` 路径 B)铺路。 ## 1. 目标与定位 - **输入**:类型/值/指令的语义对象 - **输出**:合法的 LLVM IR 文本(`.ll`),可直接喂给 `llc` - **不做什么**:不解析 IR、不做优化、不绑定 LLVM C API(纯文本生成) - **自举约束**:本库必须能用当前 TransPyC 编译器完整编译,不得依赖任何 Python 运行时 ## 2. 核心设计决策 ### 2.1 类型系统用 `t.REnum`(tagged union) LLVM 类型变体少、无通用字段、字段都是指针/小整数、操作主要通过 `match` 分派 —— 完全契合 REnum 的 tag+union 模型。 ### 2.2 指令 opcode 用 `t.CEnum` 指令类别(Binary/Unary/Cast/Memory/Terminator/Compare)与具体 opcode(Add/Sub/Load/Store/Br/Ret/...)用 `t.CEnum` 组织,类型安全且零开销。 ### 2.3 IR 文本生成用 `viperlib.snprintf` 所有 IR 文本通过 `viperlib.snprintf(buf, size, fmt, *args)` 格式化写入缓冲区,避免手写逐字符拼接。`snprintf` 已支持宽度/精度/符号/`%s`/`%d`/`%x` 等,足够覆盖 IR 语法。 ### 2.4 对象分配用普通类 + `mpool` 像 `mbuddy` 一样用普通类(无 `__new__`),由调用方注入 `mpool.MPool` 做批量分配。库内部不持有全局 `mbuddy`,遵循项目约定(`_mbuddy` 指针由 `os.py` 或调用方设置)。 --- ## 3. 为什么 AST 不用 REnum,而 llvmlite 用? 用户质疑:REnum 是 tag+union(Rust enum),配合 `match` 模式匹配,按理很适合 AST。下面解释两者的适配性差异。 ### 3.1 REnum 的硬限制(来自 `HandlesClassDef._EmitREnumLlvm`) 1. **无通用字段共享**:每个 variant 独立声明字段,通用字段(如 `lineno`/`child`/`next`)必须在每 variant 重复声明。 2. **共享最大变体尺寸**:布局为 `{ i32 __tag, }`,小变体浪费内存。 3. **payload 仅位置绑定**:`match` 通过位置绑定 payload,不能直接 `node.child` 属性访问 payload 字段。 4. **无方法**:variant 不能挂方法。 ### 3.2 AST 不适合 REnum 的四个原因 | 维度 | AST 需求 | REnum 限制 | 冲突 | |------|----------|------------|------| | 变体数量 | 70+ 节点类型 | 共享最大变体尺寸 | 内存膨胀(每节点都占最大变体空间) | | 通用字段 | lineno/child/next/parent 等 ~8 个字段全节点共享 | 无通用字段共享 | 要么每 variant 重复声明(冗长),要么放弃统一访问 | | 访问模式 | 频繁直接 `node.child`/`node.next`/`node.lineno` | payload 仅位置绑定,需 match 分派 | 遍历代码爆炸(每次访问都要 match) | | 字段复用 | `int_val`/`str_val`/`op` 等字段按 vtype 复用语义 | 每 variant 独立 payload | 无法复用 | AST 采用"胖节点"设计(所有节点共用一个 `AST` struct,用 `vtype` 区分):一次 `malloc`、统一链表遍历(`child`/`next`)、字段复用。这与 REnum"每变体独立 payload"的哲学根本冲突。 ### 3.3 llvmlite 的 LLVMType 适合 REnum 的原因 | 维度 | LLVMType 需求 | REnum 特性 | 契合度 | |------|---------------|------------|--------| | 变体数量 | ~10 个(Int/Ptr/Func/Array/Struct/Void/Float/Label/...) | 共享最大变体尺寸 | 浪费可接受(最大变体 ~24 字节) | | 通用字段 | 无(每变体字段不同) | 无通用字段共享 | 完美匹配 | | 访问模式 | 类型操作天然通过 match 分派("是 Int 取 bits,是 Ptr 取 pointee") | payload 位置绑定 + match | 天然契合 | | 字段尺寸 | 都是指针(8B)或小整数(4B) | 共享最大变体 | 浪费小 | **结论**:REnum 是"少变体、无共性、match 分派"场景的最佳工具 —— llvmlite 类型系统正好如此,AST 正好相反。 --- ## 4. 文件结构 ``` includes/llvmlite/ ├── __init__.py # 公共导出 + 便捷工厂函数 ├── __types.py # LLVMType (REnum) + 类型构造/打印 ├── __values.py # Value/Constant/SSA 值表示 ├── __module.py # Module 容器(函数列表 + 目标三元组 + 输出 .ll) ├── __function.py # Function + BasicBlock + 参数管理 ├── __builder.py # IRBuilder(指令发射 + SSA 命名 + 块跳转) └── README.md # 本文件 ``` ### 职责划分 - `__types.py`:`LLVMType` REnum 定义 + `TypePrint(buf, ty)` 将类型序列化为 IR 文本(如 `i32`/`i32*`/`{i32, i8*}`) - `__values.py`:`Value` 表示一个 SSA 值(`%0`/`%result`/常量),含类型指针 + 名字 + 是否常量 - `__module.py`:`Module` 持有函数链表 + 目标三元组 + 数据布局,`ModulePrint` 输出完整 `.ll` - `__function.py`:`Function` 持有基本块链表 + 参数 + 返回类型;`BasicBlock` 持有指令文本缓冲 - `__builder.py`:`IRBuilder` 游标式 API,`build_add`/`build_load`/`build_br`/... 发射指令到当前块 --- ## 5. 类型系统设计(REnum) ```python import t from stdint import * class LLVMType(t.REnum): # 变体 Int:整数类型 IntBits: t.CInt # 位宽 1/8/16/32/64/128 # 变体 Ptr:指针类型 PtrPointee: LLVMType | t.CPtr # 指向的类型(自引用用指针) # 变体 Func:函数类型 FuncRet: LLVMType | t.CPtr # 返回类型 FuncParams: LLVMType | t.CPtr # 参数类型链表头 FuncPCount: t.CInt # 参数数量 # 变体 Array:数组类型 ArrayElem: LLVMType | t.CPtr # 元素类型 ArrayCount: t.CInt # 元素数量 # 变体 Struct:结构体类型 StructHead: LLVMType | t.CPtr # 字段类型链表头 StructCount: t.CInt # 字段数量 # 变体 Float:浮点类型 FloatBits: t.CInt # 16/32/64/128 # 变体 Void / Label / Metadata:无 payload # (REnum 允许空变体,仅靠 __tag 区分) ``` ### 类型工厂(`__init__.py` 导出) ```python def Int1() -> LLVMType | t.CPtr: ... # i1 def Int8() -> LLVMType | t.CPtr: ... # i8 def Int32() -> LLVMType | t.CPtr: ... # i32 def Int64() -> LLVMType | t.CPtr: ... # i64 def Ptr(pointee: LLVMType | t.CPtr) -> LLVMType | t.CPtr: ... def Func(ret: LLVMType | t.CPtr, params: LLVMType | t.CPtr, count: t.CInt) -> LLVMType | t.CPtr: ... def Void() -> LLVMType | t.CPtr: ... ``` ### 类型打印(match 分派) ```python def TypePrint(buf: t.CChar | t.CPtr, size: t.CSizeT, ty: LLVMType | t.CPtr): match ty: case LLVMType.Int: # 位置绑定 IntBits viperlib.snprintf(buf, size, "i%d", ty.IntBits) case LLVMType.Ptr: # 位置绑定 PtrPointee TypePrint(inner, size2, ty.PtrPointee) # 拼接 "*" case LLVMType.Func: # 打印 ret (params) case LLVMType.Void: string.strcpy(buf, "void") ... ``` --- ## 6. Value / SSA 设计 ```python class Value: Ty: LLVMType | t.CPtr # 值的类型 Name: t.CChar | t.CPtr # SSA 名("%0"/"%result")或常量文本 IsConst: t.CInt # 1=常量字面量,0=SSA 临时值 Next: Value | t.CPtr # 链表(函数内的值池) ``` `IRBuilder` 维护一个递增的 `%N` 计数器,每发射一条产生结果的指令就分配一个新名。 --- ## 7. IRBuilder 设计 游标式 API,跟踪当前 `BasicBlock`,发射指令文本到块的缓冲区。 ```python class IRBuilder: Func: Function | t.CPtr # 所属函数 CurBlock: BasicBlock | t.CPtr # 当前插入点 Counter: t.CInt # SSA 名计数器 def build_add(self, lhs: Value | t.CPtr, rhs: Value | t.CPtr) -> Value | t.CPtr: # 1. 分配 SSA 名 %N # 2. snprintf(buf, size, " %%%d = add %s %s, %s\n", N, ty, lhs, rhs) # 3. 追加到 CurBlock 缓冲 # 4. 返回新 Value def build_load(self, ty: LLVMType | t.CPtr, ptr: Value | t.CPtr) -> Value | t.CPtr: ... def build_store(self, val: Value | t.CPtr, ptr: Value | t.CPtr): ... def build_br(self, target: BasicBlock | t.CPtr): ... def build_cond_br(self, cond: Value | t.CPtr, then: BasicBlock | t.CPtr, else_: BasicBlock | t.CPtr): ... def build_ret(self, val: Value | t.CPtr): ... def build_alloca(self, ty: LLVMType | t.CPtr) -> Value | t.CPtr: ... def build_gep(self, ptr: Value | t.CPtr, idx: Value | t.CPtr) -> Value | t.CPtr: ... def build_call(self, callee: t.CChar | t.CPtr, args: Value | t.CPtr, ret_ty: LLVMType | t.CPtr) -> Value | t.CPtr: ... ``` ### 指令 opcode(CEnum) ```python class IROp(t.CEnum): # Terminator Ret: t.CEnum = 1 Br: t.CEnum = 2 CondBr: t.CEnum = 3 Switch: t.CEnum = 4 # Binary Add: t.CEnum = 10 Sub: t.CEnum = 11 Mul: t.CEnum = 12 SDiv: t.CEnum = 13 # Memory Load: t.CEnum = 20 Store: t.CEnum = 21 Alloca: t.CEnum = 22 Gep: t.CEnum = 23 # Cast BitCast: t.CEnum = 30 SExt: t.CEnum = 31 Trunc: t.CEnum = 32 # Compare ICmp: t.CEnum = 40 # Call Call: t.CEnum = 50 ``` --- ## 8. IR 文本生成(snprintf 用法) 每条指令发射用 `viperlib.snprintf` 格式化到块缓冲区。示例: ```python import viperlib def _emit_add(block: BasicBlock | t.CPtr, name: t.CChar | t.CPtr, ty_str: t.CChar | t.CPtr, lhs: t.CChar | t.CPtr, rhs: t.CChar | t.CPtr): line: t.CChar | t.CPtr = mpool.alloc(128) # 行缓冲 viperlib.snprintf(line, 128, " %s = add %s %s, %s\n", name, ty_str, lhs, rhs) _block_append(block, line) ``` ### Module 输出 ```python def ModulePrint(mod: Module | t.CPtr, out_path: t.CChar | t.CPtr): # 1. 写头部:target triple / datalayout # 2. 遍历函数,写 declare/define # 3. 每函数:签名 + 基本块 + 指令 # 用 snprintf 拼接,写到文件(viperio 或 w32/win32file) ``` --- ## 9. 内存管理 - 库内所有对象从 `mpool.MPool` 分配(像 `ast` 库一样) - `IRBuilder`/`Module`/`Function` 不持有 `mbuddy`,由调用方传入 `mpool` - 文本缓冲区按行分配(每指令一行,128 字节起步),避免频繁 realloc --- ## 10. 基础构建范围(第一阶段) 最小可用集,能生成一个完整的小程序 IR: 1. **类型**:`IntType`(1/8/16/32/64) + `PointerType` + `FunctionType` + `VoidType` + `StructType` 2. **值**:`Value` + 常量整数字面量 3. **Module/Function/BasicBlock**:容器与链表 4. **IRBuilder** 指令子集: - `alloca` / `load` / `store` - `add` / `sub` / `mul` / `sdiv` - `icmp` (eq/ne/sgt/sge/slt/sle) - `br` / `cond_br` / `ret` - `call`(外部函数) - `bitcast` / `sext` / `trunc` - `getelementptr` 5. **ModulePrint**:输出完整 `.ll` 文件 ### 后续扩展路线 - **第二阶段**:浮点指令、`phi` 节点、`switch`、字符串常量全局、属性(`nounwind`/`nocapture`) - **第三阶段**:内联汇编、`atomic` 指令、`va_arg`、精确的 SSA 验证(支配边界、phi 完整性) - **第四阶段**:对接 TransPyC 的 `LLVMCG`,替换 Python `llvmlite`,完成路径 B 自举 --- ## 11. 测试计划 独立测试项目(`Test/LLvmLiteTest/`): 1. **类型打印测试**:构造各 `LLVMType`,`TypePrint` 输出,对比预期字符串 2. **小函数生成**:用 `IRBuilder` 生成 `i32 add(i32 a, i32 b) { ret i32 %r }`,`ModulePrint` 写 `.ll`,用 `llc` 编译验证语法合法 3. **match 分派测试**:构造各变体 `LLVMType`,`match` 正确绑定 payload 4. **snprintf 格式测试**:验证 `%d`/`%s`/`%x` 在 IR 文本中的正确性 --- ## 12. 与现有架构的对接点 - **`lib/core/Codegen/LLVMCG.py`**:当前用 Python `llvmlite` 生成 IR,自举时改用本库 - **`includes/viperlib.py`**:提供 `snprintf` / `sprintf` - **`includes/mpool.py`**:提供 `MPool` 内存池 - **`includes/string.py`**:提供 `strcpy`/`strlen`/`memcpy` - **`includes/ast/`**:参考其"胖节点 + mpool + CEnum 常量"的组织方式(但类型系统用 REnum 而非胖节点)