种子编译器实现笔记
本文档是玄铁种子编译器(xt_go.exe)的实现笔记,面向未来的维护者和协作者。记录编译器架构、运行时语义、已知陷阱和隐形契约。 编译器代码由KimiK3负责实现,所有代码已经过作者审核。 本文档由DeepSeekV4Pro生成。
编译器总览与架构
编译流程
玄铁源码 (.xt) → Lexer → Parser → AST → LLVMCompiler → LLVM IR (.ll) → clang → 原生二进制种子编译器是一个用 Go 编写的单遍编译器(对主体代码而言),核心文件:
| 文件 | 职责 |
|---|---|
main.go | 入口,解析命令行(铁 / build / 造),驱动编译流程 |
lexer/lexer.go | 词法分析,将 UTF-8 中文源码转为 Token 流 |
parser/parser.go | 递归下降语法分析,生成 AST |
ast/ast.go | AST 节点定义 |
compiler/llvm.go | 核心:遍历 AST 生成 LLVM IR 文本 |
runtime/xt_runtime.c | 核心:C 运行时,提供 ARC、内存分配、集合类型、系统调用 |
runtime/xt_runtime.h | 运行时头文件,类型定义与宏 |
LLVMCompiler 结构体关键字段
type LLVMCompiler struct {
symbolTable map[string]SymbolInfo // 变量名 → alloca寄存器/函数名
scopeStack [][]string // 作用域栈,每层存需要ARC释放的寄存器列表
arenaPoolRegs map[string]bool // 【隐形契约】Arena池变量的寄存器标记
classes map[string]*ClassInfo // 类定义元数据
strings map[string]string // 字符串常量去重 → 全局别名
currentFunc string // 当前编译中的函数名,"": main
funcOutput bytes.Buffer // 所有函数定义的IR
globalOutput bytes.Buffer // 全局变量定义的IR
}IR 中的值表示(Tagged Pointer)
玄铁的所有值在 IR 中统一为 i64,采用标记指针编码:
| 类型 | 编码规则 | 示例 |
|---|---|---|
整数 raw_i64 | 原始 i64(未编码) | 42 → i64 42 |
| 整数(盒装) | (raw << 1) | 1 | 42 → 85 |
| 布尔真 | 固定常量 4 | XT_TRUE |
| 布尔假 | 固定常量 2 | XT_FALSE |
| 空值 | 固定常量 0 | XT_NULL |
| 堆对象 | 指针的 ptrtoint(LSB=0) | %XTString* → i64 |
关键辅助函数:
ensureI64(reg, typ): 把任何类型的值转为标记后的 i64ensureRawI64(reg, typ): 把任何类型的值转为原始 i64(去标记)convertToObj(reg, typ): 把值转回指针类型(i8*)isScalarType(typ): 判断是否是标量(raw_i64、i1、double),标量不需要 ARC
compileStatement 语句编译分支
位于 llvm.go compileStatement 方法,约 L494-L684。以下按 switch case 顺序列出。
*ast.PrintStatement(示 语句)
case *ast.PrintStatement:
valReg, valType, _ := c.compileExpression(s.Value)
xtValReg := c.ensureI64(valReg, valType)
c.emit(" call void @xt_print_value(i64 %s)", xtValReg)
if !c.isScalarType(valType) { c.emit(" call void @xt_release(i64 %s)", xtValReg) }语义: 将表达式编译为 i64,调用 xt_print_value 输出。非标量类型在输出后立即 release(消费所有权)。
坑: xt_print_value 内部会调用 xt_obj_to_string 生成临时字符串然后 release。传入的非标量值必须保证引用计数至少为 1。
*ast.VarStatement(设 语句——变量声明)
语义: 编译右侧表达式,若为非标量则 xt_retain(因为需要保持一份所有权给变量),分配 alloca 或全局变量,存储值。
关键逻辑: 顶层作用域(currentFunc == "")→ 全局变量 @\"变量名\" = global i64 0。函数内 → 栈上 alloca %\"变量名\" = alloca i64。
坑:
- 局部变量若为非标量,会调用
c.trackObject(addrReg)将其加入作用域栈,作用域退出时自动 release - 变量重定义(同一作用域内再次
设)会先 release 旧值再覆盖 isArenaCreate检测:若右侧是xt_arena_new(...)调用,该变量的寄存器被标记到arenaPoolRegs,确保在作用域退出时最后释放
*ast.AssignStatement(= 赋值语句)
语义: 与 VarStatement 类似但针对已存在变量。右侧 retain + 左侧旧值 release,然后 store 新值。
坑:
- 赋值给
_是特殊路径:只编译右侧表达式并立即 release(丢弃) - 也需检测
isArenaCreate以更新arenaPoolRegs
*ast.IfStatement / *ast.WhileStatement / *ast.LoopStatement / *ast.ForStatement
If 语句: 编译条件 → 转换为 i1 → 分支到 then/else/merge。每个分支进入新作用域,退出时 release 该作用域的变量。
While 语句: 循环条件 → br 到 body 或 end。维护 breakLabels/continueLabels/loopDepths 栈。
Loop 语句(无限循环): 无条件 br,break/continue 走标签。
For 语句: 委托到 compileForStatement。迭代器编译为对 xt_obj_to_string + xt_string_concat 的调用链,或者在 IR 中直接展开为 while 循环。
坑:
- 条件的 bool 判断:i64 的"真"是
== 4,非标量类型需要先ensureI64再比较 - 条件表达式求值后立即 release(因为只用于分支判断,不需要持有)
- 循环体每次迭代都创建新作用域(用
enterScope/exitScope)
*ast.FunctionStatement(函 语句)
语义: 保存当前编译输出,创建独立函数作用域。遍历函数体编译。函数被注册到 funcOutput 和全局符号表。
关键机制:
- 函数参数:
i64 %\"参数名_arg\",在 entry 块中 allocate,retain,store,trackObject - 返回类型默认为 i64(隐式
ret i64 0在末尾) - return 语句通过
xt_retain持有返回值后exitScope(true)退出所有作用域
坑:
- 函数编译时
currentFunc被设为函数名,全局变量 vs 局部变量的判断依赖此字段 compileFunctionStatement不会emit到主 output,而是写入funcOutputallocaOutput用于分离 alloca 指令到 entry 块(通过__ALLOCAS_MARKER__替换拼接)
*ast.TypeDefinitionStatement(型 语句)
语义: 解析类定义(字段、方法、继承关系),存入 c.classes 元数据表。不生成 IR 代码,类实例化时(造)才会用到这些元数据。
*ast.InterfaceStatement(接口 语句)
语义: 纯元数据,不生成任何 IR 代码。return 跳过。
*ast.ExternalFunctionStatement(外 函 语句)
语义: 调用 compileExternalFunctionStatement,生成 declare IR 声明。返回类型映射:整→i64、小数→double、字→i8*、空→void。所有参数均为 i64。
*ast.ReturnStatement(返 语句)
语义: 编译返回值表达式。main 函数中返回 i32(调用 exitScope(true) + ret i32)。其他函数返回 i64(xt_retain + exitScope(true) + ret i64)。
坑: isReturn=true 会让 exitScope 展开所有嵌套作用域而非只退出最内层。retain 必须在 exitScope 之前执行(否则变量已被 release 而无法持有)。
*ast.MatchStatement / *ast.TerminateStatement
Match: 模式匹配,委托到 compileMatchStatement,编译为条件分支链。
Terminate(终): 调用 exit(i32) 然后 unreachable。可选状态码参数。
*ast.ExpressionStatement
语义: 独立表达式语句(如调用函数但不赋值)。编译表达式后立即 release(除非是 raw_i64)。
坑: 这就是函数调用结果被消费的地方——如果忘记把函数调用的返回值赋给变量,它在这一行就被丢掉了。
*ast.BreakStatement / *ast.ContinueStatement
语义: exitScopesUntil(depth) 展开到循环开始时的作用域深度,然后 br 到 end/cond 标签。
坑: 跳转后生成 deadcode 标签防止 LLVM 报基本块后继未定义的错误。
compileExpression 表达式编译分支
位于 llvm.go compileExpression 方法,约 L894-L2249。每个分支返回 (reg, type, className)。
*ast.IntegerLiteral
直接返回 fmt.Sprintf("%d", e.Value), "raw_i64", ""。不生成任何 IR 指令——整数字面量是纯值。
*ast.FloatLiteral
返回 fmt.Sprintf("%f", e.Value), "double", ""。double 类型的特殊性:它不能直接用作 i64 函数参数,需要先通过 ensureI64 → xt_float_new 装箱。
*ast.BooleanLiteral
真 → "4", "i64",假 → "2", "i64"。注意这里的 type 是 "i64" 而非 "i1"——因为 XT_TRUE/XT_FALSE 是 i64 常量。
*ast.ImportExpression(引 语句)
语义: 递归读取被引文件,调用 lexer/parser 生成 AST,再递归调用 compileStatement。用 visitedImports map 防止循环引用。
坑:
- 别名(
予 库)通过moduleAliases[alias] = true记录,后续库.方法()解析时用到 - 文件路径拼接:相对路径以
c.filePath所在目录为基准 - 引用的所有顶层语句(变量、函数、类型定义)直接输出到当前编译上下文中
*ast.StringLiteral
alias := c.addString(e.Value) // 去重,生成 @str.N 全局常量
rawReg → getelementptr @str.N // 取字符串字节首地址
objReg → xt_string_new(rawReg) // 创建 XTString 堆对象
resReg → ptrtoint XTString* → i64 // 转 i64
return resReg, "i64", ""语义: 每次求值都调用 xt_string_new 创建新对象。返回的 ref_count = 1(由 xt_malloc 初始化),调用方负责 release。
坑: 同一个字符串字面量 "hello" 在代码中出现两次 → 共用 @str.N 全局常量,但生成两个独立的 xt_string_new 调用。
*ast.Identifier
语义:
- 特殊值:
空→0、真→4、假→2、_→0 - 类型关键字:
字→7、整→3、小数→5、逻→9、数组→11、字典→13、结果→17、字节→19、任务→21、道→23(值 = type_id * 2 + 1) - 变量名:
load其 alloca 寄存器。若为非标量则xt_retain并返回 i64——这意味着每次读取变量都会增加引用计数
坑:
- 类型关键字返回值是标记后的 tagged integer,不需要 release
- 函数名作为值使用时,转为
ptrtoint bitcast (...)* @\"函数名\" to i8* to i64
*ast.PostfixExpression(? 后缀)
语义: x? 目前是透传:直接编译 x 并返回。原始设计意图是:若 x 是 Result 且失败则提前 return,但 LLVM 层尚未实现该控制流改写。
*ast.PrefixExpression(非/!、-)
非/!: 转布尔条件 → xor true → select 4 或 2 → 返回 i64。非标量右值要 release。
负号(-): raw_i64 直接 sub 0, reg。标记整数用公式 (2 - tagged) | 1(因为 tag bit 需要保持),然后 release 原值。
*ast.CallExpression(函数调用)
这是最复杂的分支,约 L1037-1180。
特殊快捷路径:
整(x):转换为 raw_i64(脱壳 tagged int)成功(x)/失败(x):调用xt_result_new(is_success, value_obj, error_obj)整/小数/字类型转换:委托到xt_convert_to_*
通用函数调用流程:
- 解析函数名(符号表查找 → 全局符号用注册名,否则用
@\"函数名\") - 判断是否内置函数(
xt_前缀的视为i8*返回,参数也转为 i8*) - 编译参数 → 每个参数
ensureI64→ 调用后必须 release 参数 - 处理间接调用(
%reg前缀的函数指针)vs 直接调用 - 调用后释放所有参数寄存器(
for _, argReg := range argRegs { xt_release })
printf 特殊处理: MinGW 的 ABI 兼容性问题——通过 xt_ffi_printf 包装器中转。
坑:
- 参数在调用后总是被 release——这意味着如果你需要保持参数值,调用方必须提前 retain
- 内置函数(
xt_*)的参数用convertToObj转为i8*而非i64
*ast.NewExpression(造 表达式)
语义: 创建类实例(字典)。xt_dict_new(16) → 注入 __类型__ 字段 → 注入类字段(初始值 0)→ 注入方法(用 xt_func_new 包装)→ 若有 造 构造函数则调用。
坑:
- 这是 OOP 机制的基石——实例本质是带了类型标签和方法绑定的字典
- 方法绑定:
bitcast方法函数指针 →xt_func_new→xt_dict_set
*ast.ArrayLiteral([] 数组字面量)
xt_array_new(len) → 每个元素 ensureI64 → xt_array_append → 立即 xt_release 元素(append 内部会 retain)。
*ast.DictLiteral({} 字典字面量)
xt_dict_new(size*2) → 每对 k,v ensureI64 → xt_dict_set → 立即 release k 和 v(set 内部 retain)。
*ast.IndexExpression(x[y] 下标访问)
语义: 根据运行时类型分发——检查 type_id:
6(Dict)→xt_dict_get+ retain3(String)→xt_string_get_char- 其他(Array)→ 手动访问
XTArray.elements[idx]+ retain
坑: 这是少数需要运行时类型分派的代码路径。通过 null check + type_id 的 LLVM 分支实现。左侧对象在求值后被 release。
*ast.MemberCallExpression(x.方法())
这是最长的分支(约 L1370-L1846),涉及多个处理路径:
- 类静态调用:
类名.方法(参数)→ 查c.classes→ 直接 call - 文件操作:
文件.读/写/存在?→ 直接映射到xt_file_* - 时间操作:
时.现/毫秒/微秒/睡→ 直接映射到xt_time_* - 数学操作:
数学.随机→xt_math_random - 模块别名:
库.方法()→ 查moduleAliases→ 直接 call@\"方法名\" - 内置方法(动态分派):
解包(Result 解包)→ 读XTResult.is_success→ select value/error 指针成功、值、错误→ 直接读 XTResult 结构体字段长度/大小→ 根据 type_id 分派读 Array/Dict/String 的长度字段取字节/字节、字节数、字符数→ 字符串相关替换、分割、截取、字符→ 字符串操作包含?/含?→ 字符串用xt_string_contains、字典用xt_dict_contains追加、弹出、连接→ 数组操作
- 类方法调用(按 ClassName 查找): 用 objXt 作为 this 参数调用方法
- 字典动态调用(兜底):
xt_dict_get查方法 → 若存在且是 XTFunction → 解包调用
坑:
- 返回值通过 alloca 间接返回(
resAddr),避免在分支中无法统一返回寄存器 - 最终的
final = load resAddr; xt_release(objXt); return final - NaN-safety:对象可能为 0 / null,需要 null check + phi 合并
*ast.ExecuteExpression / *ast.InputExpression
Execute(执): 调用 xt_execute(命令)Input(输): 调用 xt_input(提示语)
*ast.ListenExpression / *ast.ConnectExpression / *ast.AwaitExpression
听(端口, 回调) → xt_listen、连(地址) → xt_connect、等待(任务) → xt_wait。参数 release 调用方负责。
*ast.InfixExpression(中缀表达式)
这是最长的分支之一,包含多层优化路径:
P0 路径:逻辑运算符
且/&&→compileLogicalAnd(短路求值)或/||→compileLogicalOr(短路求值)
P1 路径:是 类型判断
- 编译左侧 → 读取运行时 type_id → 与右侧(必须是 raw_i64 类型 ID)比较
- 需要处理标记整数、特殊常量、真实指针三种情况,用 phi 节点合并
P2 路径:算术/比较(标量快速路径)
- 双方都是标量 → 直接用 LLVM 的
add/sub/mul/sdiv/icmp指令 - 比较结果:
select i1, i64 4, i64 2
P3 路径:算术/比较(运行时比较)
- 用
xt_compare(a, b)拿 -1/0/1 → 再 icmp 比较 - 结果 select 4 或 2 返回
P4 路径:运行时算术(xt_add/xt_sub/xt_mul/...)
- 非标量运算委托给 C 运行时
P5 路径:魔法方法重载
- 检查
_加_、_减_、_乘_、_等_等方法是否在字典中注册 - 若有注册 → 函数调用;无 → fallback 到内置路径
- 这是一个初步的运算符重载机制
P6 路径:字符串连接(&)
xt_obj_to_string两遍 →xt_string_concat→ 返回新字符串- 中间临时字符串需要 release
P7 路径:范围(..)
xt_array_range(start, end)
运行时 xt_* 函数语义与陷阱
C 运行时位于 xt_runtime.c 和 xt_runtime.h。所有 IR 中 i64 的值最终映射到 C 的 uintptr_t XTValue。以下按功能分组列出关键函数。
内存管理
| 函数 | 语义 | 关键行为 |
|---|---|---|
xt_malloc(size, type_id) | 通用堆分配入口 | 当前有 active arena 则走 arena,否则走 malloc。初始化 ref_count=1、magic=XT_MAGIC、type_id |
xt_arena_new(size) | 创建 Arena | 分配 buffer(calloc),ref_count 初始化为 IMMORTAL(0x7FFFFFFF),type_id=XT_TYPE_ARENA(13) |
xt_arena_use(arena) | 激活 Arena | 设置 g_current_arena = arena,此后所有 xt_malloc 走 arena |
xt_arena_alloc_raw(size) | Arena 内分配原始内存 | 8 字节对齐,空间不足时自动扩容(链表挂载新块) |
xt_arena_alloc(size, type_id) | Arena 内分配对象 | 分配后设 ref_count=IMMORTAL,防止 ARC 误杀 |
xt_arena_destroy(arena) | 【隐形契约】延迟销毁 | 见第 6 章 |
引用计数(ARC)
| 函数 | 语义 | 关键行为 |
|---|---|---|
xt_retain(val) | +1 引用计数 | 跳过标记整数(XT_IS_INT)、跳过 null、跳过 IMMORTAL 对象 |
xt_release(val) | -1 引用计数 | 先 xt_check_obj 校验 magic → 跳过 IMMORTAL → 原子减 ref_count → 若归零则 xt_free_obj |
xt_check_obj(val) | 堆损坏检测 | 检查 obj->magic != XT_MAGIC(0x58544F42),失败则 fprintf + exit(-1) |
关键: xt_release 在检查 IMMORTAL 之前先检查 magic。这意味着即使是 IMMORTAL 对象,若其内存已被释放,也会触发堆损坏告警。这是 Arena UAF 问题的根源。
对象释放(xt_free_obj)
遍历 type_id 并执行每个类型的析构:
XT_TYPE_STRING: 若 data 不在 arena 中(!data_in_arena),free dataXT_TYPE_ARRAY: 遍历所有元素xt_release,free elements 数组XT_TYPE_DICT: 遍历所有桶的链节点,release key/value,free entry,free bucketsXT_TYPE_INSTANCE: 同字典XT_TYPE_RESULT: release value 和 errorXT_TYPE_CHANNEL: release 残留消息,free bufferXT_TYPE_BYTES: 若非 arena,free dataXT_TYPE_ARENA: 释放扩容链节点及其 buffer,释放首节点 bufferXT_TYPE_FUNCTION: 仅 free 结构体本身(函数指针不需要释放)
最后 free(obj) 释放结构体自身。
值创建
| 函数 | 返回类型 | 说明 |
|---|---|---|
xt_int_new(val) | XTValue | 标记整数:(val << 1) | 1 |
xt_float_new(val) | void* (XTValue) | 堆分配 XTFloat 结构体,ref_count=1 |
xt_bool_new(val) | XTValue | XT_TRUE(4) 或 XT_FALSE(2) |
xt_string_new(data) | XTString* (XTValue) | 堆分配 XTString,调用 xt_malloc,data 单独分配(arena 模式下用 arena) |
xt_string_new_len(data, len) | XTString* | 同上,指定长度 |
xt_string_from_char(c) | XTString* | 单字符字符串 |
xt_result_new(is_success, value, error) | void* (XTValue) | 堆分配 XTResult,retain value 和 error |
xt_func_new(func_ptr) | XTValue | 堆分配 XTFunction 包装函数指针 |
xt_array_new(capacity) | XTValue | 堆分配 XTArray |
xt_dict_new(capacity) | XTValue | 堆分配 XTDict |
集合操作
| 函数 | 语义 | 坑 |
|---|---|---|
xt_array_append(arr, elem) | 数组追加 | 内部 retain elem;不够时 2x 扩容 |
xt_array_get(arr, idx) | 数组索引 | 不返回 retain 过的值——调用方需自行 retain |
xt_array_set(arr, idx, val) | 修改元素 | release 旧值 + retain 新值 |
xt_array_pop(arr) | 弹出末尾 | 返回值 ref_count 不变(转移所有权) |
xt_array_remove(arr, idx) | 删除元素 | release + 前移后续元素 |
xt_array_insert(arr, idx, val) | 插入元素 | 先 append 再移位 |
xt_array_slice(arr, start) | 子数组 | 返回新数组,内部元素被 retain |
xt_array_range(start, end) | 范围数组 | [start, end) 区间的标记整数数组 |
xt_array_join(arr, sep) | 数组连接 | 返回新字符串 |
xt_dict_set(dict, key, val) | 字典设值 | 内部 retain key 和 val;key 冲突时释放旧值 |
xt_dict_get(dict, key) | 字典取值 | 不返回 retain 过的值——调用方需自行 retain |
xt_dict_contains(dict, key) | 是否含键 | 返回 0 或 1 |
重要规则: xt_array_get、xt_dict_get 不主动 retain。编译器在 IndexExpression 和 MemberCallExpression 中需要显式调用 xt_retain。
字符串操作
| 函数 | 语义 |
|---|---|
xt_string_get_char(str, idx) | 返回第 idx 个 UTF-8 字符(新字符串) |
xt_string_get_byte(str, idx) | 返回第 idx 个原始字节(tagged int) |
xt_string_byte_length(str) | 字节长度(tagged int) |
xt_string_char_count(str) | 字符数(tagged int) |
xt_string_concat(a, b) | 拼接,返回新字符串 |
xt_string_substring(str, start, end) | UTF-8 子串 |
xt_string_contains(str, sub) | 子串查找,返回 0/1 |
xt_string_replace(str, from, to) | 替换,返回新字符串 |
xt_string_split(str, sep) | 分割,返回数组 |
输出与类型转换
| 函数 | 语义 |
|---|---|
xt_print_value(val) | 通用输出:xt_obj_to_string(val) → printf → release 临时字符串 |
xt_print_int/string/bool/float | 各类型的直接输出 |
xt_obj_to_string(val) | 将任意值转为字符串(根据 type_id 或标记位分派) |
xt_to_int(val) | 将任意值转为 int64_t |
xt_convert_to_int/float/string(val) | 类型转换(创建新对象) |
xt_compare(a, b) | 比较两个值,返回 -1/0/1 |
xt_ffi_printf(fmt, arg) | MinGW ABI 兼容的 printf 包装器 |
系统接口
| 函数 | 语义 |
|---|---|
xt_file_read/xt_file_write/xt_file_exists | 文件 I/O |
xt_time_now/xt_time_ms/xt_time_micro | 获取当前时间 |
xt_time_sleep(ms) | 睡眠(毫秒) |
xt_execute(cmd) | 执行系统命令 |
xt_input(prompt) | 读取用户输入 |
xt_listen(port, cb) / xt_connect(addr) | 网络(占位) |
xt_json_serialize / xt_json_deserialize | JSON 序列化/反序列化 |
xt_channel_new/send/receive | 通道(并发原语) |
xt_wait(task) | 等待异步任务 |
xt_http_request(url) | HTTP 请求(占位) |
ARC 边界规则
所有权模型
玄铁采用 +1 返回 / +1 消费 的 ARC 约定:
| 操作 | 规则 |
|---|---|
创建对象(xt_*_new) | 返回 ref_count=1,调用方拥有所有权 |
| 变量读取(Identifier) | 编译器生成 xt_retain,调用方获得 +1 引用 |
| 成员/索引访问 | 编译器生成 xt_retain 返回结果 |
| 变量赋值(Var/Assign) | retain 新值,release 旧值 |
| 函数返回(Return) | retain 返回值后退出作用域 |
| 函数参数传入 | entry 块中 retain 参数 |
| 临时表达式消费 | 使用后在语句中 release |
| 作用域退出 | exitScope 遍历 release 所有被 track 的局部变量 |
编译器中的 retain/release 配对规则
以下情况下编译器必须生成 xt_release:
- PrintStatement:输出后 release 非标量值
- ExpressionStatement:表达式求值后 release 非标量值
- CallExpression:release 所有参数(调用后)
- If/While 条件:release 条件表达式(若为非标量)
- InfixExpression:release 左右操作数(若为非标量)
- IndexExpression:release 左侧对象和索引
- MemberCallExpression:release 对象
- 作用域退出:release 所有 tracked 局部变量
以下情况下编译器必须生成 xt_retain:
- Identifier 读取:非标量变量读取后 retain
- VarStatement:非标量初始值 retain
- AssignStatement:非标量新值 retain
- ReturnStatement:返回值 retain(在 exitScope 之前)
- IndexExpression:结果 retain(
xt_dict_get/ 数组元素的 array_load 之后) - MemberCallExpression:结果 retain
- 函数参数:entry 块中 retain
标量类型的豁免
raw_i64、i1、double 被定义为标量类型(isScalarType),它们不需要 ARC 管理:
- 整数(raw_i64)直接是 i64 寄存器值
- 布尔(i1)是 LLVM 的 i1
- 浮点(double)是 LLVM 的 double
常见 ARC 陷阱
xt_dict_get/xt_array_get不 retain:必须由编译器在 IndexExpression / MemberCallExpression 路径中显式 retain
曾在 v0.17.x 修过的 bug:MemberCallExpression 无参字段访问路径有过多余的 release,导致每次循环掉一个 retain count
- CallExpression 参数在调用后被 release:如果你需要把参数传给多个调用,需要在第一次调用前 multi-retain
- Return 必须先 retain 再 exitScope:否则 exitScope 会 release 掉你要返回的值
- 条件表达式求值后必须 release:因为它只用于生成 i1 分支条件,不再被引用
隐形契约与新增机制
隐形契约 1:arenaPoolRegs 必须被正确维护
定义位置: llvm.go LLVMCompiler.arenaPoolRegs map[string]bool
契约内容: 凡是由 xt_arena_new 创建的 Arena 池变量,其 alloca 寄存器必须在 arenaPoolRegs 中注册。exitScope / exitScopesUntil 会先释放普通变量,再释放 arenaPoolRegs 中的变量,确保 Arena 池在最后才被 ARC 回收。
当前注册点:
VarStatement:新变量声明时,通过isArenaCreate(s.Value)检测并注册AssignStatement:重赋值时,通过isArenaCreate(s.Value)检测并注册
任何新加的 Arena 创建路径必须同步注册 arenaPoolRegs。 如果遗漏,Arena 池将在普通变量之前被释放 → 同作用域的 Arena 内对象会被 ARC 扫描已释放内存 → 堆损坏崩溃。
隐形契约 2:xt_arena_destroy 现在是"延迟销毁"
定义位置: xt_runtime.c xt_arena_destroy
契约内容: xt_arena_destroy 不再立即释放 Arena buffer。它只做以下两件事:
- 解绑全局上下文:
g_current_arena = NULL - 释放扩容链节点(arena->next 的 follow-up 块)
- 将 Arena 首节点的 ref_count 从 IMMORTAL 降级为 2
为什么是 2 而不是 1: 编译器在 xt_arena_destroy(池) 调用后会自动插入 xt_release(池)(CallExpression 的参数释放)。若设为 1,这次 release 会立即触发 xt_free_obj 并释放 buffer → 此时作用域尚未退出,后续 ARC 扫描其他 Arena 内变量 → UAF 崩溃。设为 2 确保:
- 调用后的 release: 2→1,不触发 free
- 作用域退出时的 release: 1→0,触发
xt_free_obj→XT_TYPE_ARENA分支 → 释放 buffer 和壳子
真正的 buffer 释放时机: 当拥有 Arena 池变量的作用域退出时,ARC 最后一次 release 触发 xt_free_obj,在 XT_TYPE_ARENA 分支中释放 buffer 和结构体。
对维护者的警告: 如果你修改了 xt_arena_destroy 让它"恢复"立即释放 buffer,所有使用 xt_arena_destroy 的代码都会出现 Use-After-Free 崩溃。除非你同时也修改编译器让它在 Arena 池所处的作用域内不再对任何 Arena 内变量做 ARC 扫描。
Arena + ARC 共存的时间线
以 test_same_scope.xt(同作用域内分配+销毁)为例:
1. 设 池 = xt_arena_new(1024) → ref=IMMORTAL, magic=XT_MAGIC
2. xt_arena_use(池) → g_current_arena = 池
3. 设 arr = [1, 2, 3] → arr 在 arena->buffer 中, ref=IMMORTAL, magic=XT_MAGIC
4. xt_arena_destroy(池) → g_current_arena=NULL, ref=2
→ 编译器自动 xt_release(池): 2→1,不触发
5. 示("结束前") → xt_print_value,用 malloc(arena 已解绑)
6. } // 作用域退出 →
→ xt_release(arr) → magic✓ → IMMORTAL✓ → 跳过
→ xt_release(池) → magic✓ → 1→0 → xt_free_obj → XT_TYPE_ARENA → free(buffer)+free(壳)关键保证:
- 步骤 5 中,arr 的 magic 仍然完好(buffer 尚未释放)
- 步骤 6 中,arr 先于 池 释放(arenaPoolRegs 机制)
- arr 被 IMMORTAL 安全跳过;池 被正常回收
已知未解决问题
?后缀操作符未完整实现: 目前是透传,未对 Result 做提前 return 控制流改写- JSON 序列化返回"空":
化()函数在某些嵌套结构上返回空字符串,疑似xt_json_serialize实现不完整 - 父类方法调用: 注释中写明"暂时简化以避开自举编译器的父类调用 Bug"
- Arena 扩容链的 buffer 在 destroy 时立即释放: 扩容链节点不被玄铁变量引用,安全。但若未来有需求让扩容链中的对象也被 ARC 保护,需要重新设计
- MinGW printf ABI: 需要
xt_ffi_printf包装器中转,直接通过 IR 调用 printf 极度不稳定