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)
- 无通用字段共享:每个 variant 独立声明字段,通用字段(如
lineno/child/next)必须在每 variant 重复声明。 - 共享最大变体尺寸:布局为
{ i32 __tag, <max_variant_payload> },小变体浪费内存。 - payload 仅位置绑定:
match通过位置绑定 payload,不能直接node.child属性访问 payload 字段。 - 无方法: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:LLVMTypeREnum 定义 +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)
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 导出)
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 分派)
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 设计
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,发射指令文本到块的缓冲区。
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)
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 格式化到块缓冲区。示例:
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 输出
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:
- 类型:
IntType(1/8/16/32/64) +PointerType+FunctionType+VoidType+StructType - 值:
Value+ 常量整数字面量 - Module/Function/BasicBlock:容器与链表
- IRBuilder 指令子集:
alloca/load/storeadd/sub/mul/sdivicmp(eq/ne/sgt/sge/slt/sle)br/cond_br/retcall(外部函数)bitcast/sext/truncgetelementptr
- ModulePrint:输出完整
.ll文件
后续扩展路线
- 第二阶段:浮点指令、
phi节点、switch、字符串常量全局、属性(nounwind/nocapture) - 第三阶段:内联汇编、
atomic指令、va_arg、精确的 SSA 验证(支配边界、phi 完整性) - 第四阶段:对接 TransPyC 的
LLVMCG,替换 Pythonllvmlite,完成路径 B 自举
11. 测试计划
独立测试项目(Test/LLvmLiteTest/):
- 类型打印测试:构造各
LLVMType,TypePrint输出,对比预期字符串 - 小函数生成:用
IRBuilder生成i32 add(i32 a, i32 b) { ret i32 %r },ModulePrint写.ll,用llc编译验证语法合法 - match 分派测试:构造各变体
LLVMType,match正确绑定 payload - snprintf 格式测试:验证
%d/%s/%x在 IR 文本中的正确性
12. 与现有架构的对接点
lib/core/Codegen/LLVMCG.py:当前用 Pythonllvmlite生成 IR,自举时改用本库includes/viperlib.py:提供snprintf/sprintfincludes/mpool.py:提供MPool内存池includes/string.py:提供strcpy/strlen/memcpyincludes/ast/:参考其"胖节点 + mpool + CEnum 常量"的组织方式(但类型系统用 REnum 而非胖节点)