Skip to content

种子编译器实现笔记 ​

本文档是玄铁种子编译器(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.goAST 节点定义
compiler/llvm.go核心:遍历 AST 生成 LLVM IR 文本
runtime/xt_runtime.c核心:C 运行时,提供 ARC、内存分配、集合类型、系统调用
runtime/xt_runtime.h运行时头文件,类型定义与宏

LLVMCompiler 结构体关键字段 ​

go
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) | 142 → 85
布尔真固定常量 4XT_TRUE
布尔假固定常量 2XT_FALSE
空值固定常量 0XT_NULL
堆对象指针的 ptrtoint(LSB=0)%XTString* → i64

关键辅助函数:

  • ensureI64(reg, typ): 把任何类型的值转为标记后的 i64
  • ensureRawI64(reg, typ): 把任何类型的值转为原始 i64(去标记)
  • convertToObj(reg, typ): 把值转回指针类型(i8*)
  • isScalarType(typ): 判断是否是标量(raw_i64、i1、double),标量不需要 ARC

compileStatement 语句编译分支 ​

位于 llvm.go compileStatement 方法,约 L494-L684。以下按 switch case 顺序列出。

*ast.PrintStatement(示 语句) ​

go
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,而是写入 funcOutput
  • allocaOutput 用于分离 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 ​

go
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_*

通用函数调用流程:

  1. 解析函数名(符号表查找 → 全局符号用注册名,否则用 @\"函数名\")
  2. 判断是否内置函数(xt_ 前缀的视为 i8* 返回,参数也转为 i8*)
  3. 编译参数 → 每个参数 ensureI64 → 调用后必须 release 参数
  4. 处理间接调用(%reg 前缀的函数指针)vs 直接调用
  5. 调用后释放所有参数寄存器(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 + retain
  • 3(String)→ xt_string_get_char
  • 其他(Array)→ 手动访问 XTArray.elements[idx] + retain

坑: 这是少数需要运行时类型分派的代码路径。通过 null check + type_id 的 LLVM 分支实现。左侧对象在求值后被 release。

*ast.MemberCallExpression(x.方法()) ​

这是最长的分支(约 L1370-L1846),涉及多个处理路径:

  1. 类静态调用: 类名.方法(参数) → 查 c.classes → 直接 call
  2. 文件操作: 文件.读/写/存在? → 直接映射到 xt_file_*
  3. 时间操作: 时.现/毫秒/微秒/睡 → 直接映射到 xt_time_*
  4. 数学操作: 数学.随机 → xt_math_random
  5. 模块别名: 库.方法() → 查 moduleAliases → 直接 call @\"方法名\"
  6. 内置方法(动态分派):
    • 解包(Result 解包)→ 读 XTResult.is_success → select value/error 指针
    • 成功、值、错误 → 直接读 XTResult 结构体字段
    • 长度/大小 → 根据 type_id 分派读 Array/Dict/String 的长度字段
    • 取字节/字节、字节数、字符数 → 字符串相关
    • 替换、分割、截取、字符 → 字符串操作
    • 包含?/含? → 字符串用 xt_string_contains、字典用 xt_dict_contains
    • 追加、弹出、连接 → 数组操作
  7. 类方法调用(按 ClassName 查找): 用 objXt 作为 this 参数调用方法
  8. 字典动态调用(兜底): 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 data
  • XT_TYPE_ARRAY: 遍历所有元素 xt_release,free elements 数组
  • XT_TYPE_DICT: 遍历所有桶的链节点,release key/value,free entry,free buckets
  • XT_TYPE_INSTANCE: 同字典
  • XT_TYPE_RESULT: release value 和 error
  • XT_TYPE_CHANNEL: release 残留消息,free buffer
  • XT_TYPE_BYTES: 若非 arena,free data
  • XT_TYPE_ARENA: 释放扩容链节点及其 buffer,释放首节点 buffer
  • XT_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)XTValueXT_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_deserializeJSON 序列化/反序列化
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:

  1. PrintStatement:输出后 release 非标量值
  2. ExpressionStatement:表达式求值后 release 非标量值
  3. CallExpression:release 所有参数(调用后)
  4. If/While 条件:release 条件表达式(若为非标量)
  5. InfixExpression:release 左右操作数(若为非标量)
  6. IndexExpression:release 左侧对象和索引
  7. MemberCallExpression:release 对象
  8. 作用域退出:release 所有 tracked 局部变量

以下情况下编译器必须生成 xt_retain:

  1. Identifier 读取:非标量变量读取后 retain
  2. VarStatement:非标量初始值 retain
  3. AssignStatement:非标量新值 retain
  4. ReturnStatement:返回值 retain(在 exitScope 之前)
  5. IndexExpression:结果 retain(xt_dict_get / 数组元素的 array_load 之后)
  6. MemberCallExpression:结果 retain
  7. 函数参数:entry 块中 retain

标量类型的豁免 ​

raw_i64、i1、double 被定义为标量类型(isScalarType),它们不需要 ARC 管理:

  • 整数(raw_i64)直接是 i64 寄存器值
  • 布尔(i1)是 LLVM 的 i1
  • 浮点(double)是 LLVM 的 double

常见 ARC 陷阱 ​

  1. xt_dict_get / xt_array_get 不 retain:必须由编译器在 IndexExpression / MemberCallExpression 路径中显式 retain

曾在 v0.17.x 修过的 bug:MemberCallExpression 无参字段访问路径有过多余的 release,导致每次循环掉一个 retain count

  1. CallExpression 参数在调用后被 release:如果你需要把参数传给多个调用,需要在第一次调用前 multi-retain
  2. Return 必须先 retain 再 exitScope:否则 exitScope 会 release 掉你要返回的值
  3. 条件表达式求值后必须 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。它只做以下两件事:

  1. 解绑全局上下文:g_current_arena = NULL
  2. 释放扩容链节点(arena->next 的 follow-up 块)
  3. 将 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 安全跳过;池 被正常回收

已知未解决问题 ​

  1. ? 后缀操作符未完整实现: 目前是透传,未对 Result 做提前 return 控制流改写
  2. JSON 序列化返回"空": 化() 函数在某些嵌套结构上返回空字符串,疑似 xt_json_serialize 实现不完整
  3. 父类方法调用: 注释中写明"暂时简化以避开自举编译器的父类调用 Bug"
  4. Arena 扩容链的 buffer 在 destroy 时立即释放: 扩容链节点不被玄铁变量引用,安全。但若未来有需求让扩容链中的对象也被 ARC 保护,需要重新设计
  5. MinGW printf ABI: 需要 xt_ffi_printf 包装器中转,直接通过 IR 调用 printf 极度不稳定

基于 VitePress 静态构建 · 文档内容与仓库 GUIDE/ 同源

玄铁玄铁 XUANTIE · 中文系统级编程语言MARK 玄·折 120×120 · STROKE 8 · NO CURVES