# 种子编译器实现笔记
> 本文档是玄铁种子编译器(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 结构体关键字段
```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) \| 1` | `42` → `85` |
| 布尔真 | 固定常量 `4` | `XT_TRUE` |
| 布尔假 | 固定常量 `2` | `XT_FALSE` |
| 空值 | 固定常量 `0` | `XT_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](https://github.com/MARKJY-China/XuanTie-Lang/blob/master/compiler/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](https://github.com/MARKJY-China/XuanTie-Lang/blob/master/compiler/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](https://github.com/MARKJY-China/XuanTie-Lang/blob/master/runtime/xt_runtime.c) 和 [xt_runtime.h](https://github.com/MARKJY-China/XuanTie-Lang/blob/master/runtime/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)` | 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`:
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
2. **CallExpression 参数在调用后被 release**:如果你需要把参数传给多个调用,需要在第一次调用前 multi-retain
3. **Return 必须先 retain 再 exitScope**:否则 exitScope 会 release 掉你要返回的值
4. **条件表达式求值后必须 release**:因为它只用于生成 i1 分支条件,不再被引用
---
## 隐形契约与新增机制
### 隐形契约 1:arenaPoolRegs 必须被正确维护
**定义位置:** [llvm.go](https://github.com/MARKJY-China/XuanTie-Lang/blob/master/compiler/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](https://github.com/MARKJY-China/XuanTie-Lang/blob/master/runtime/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 极度不稳定