豫言 / 技术规范

本篇描述 4cdeca1 的实现。版本与适用边界见基线说明

原生二进制接口

本篇描述本基线 LLVM 原生后端与 C 运行时的接口。整数数值位宽为 64,值容器为 128;WasmGC 不使用本篇布局。规则依据为值编解码、代码生成准备、直接代码生成、运行时栈及垃圾回收实现;具体路径见基线与实现依据

原一:目标与基本类型

语言函数的 LLVM 类型必须为 void (i128, i128),调用规范 ccc。两参依次是豫言栈帧地址和当前堆分配地址。C 对应 void 函数(__uint128_t 栈, __uint128_t 堆)。不能改为两指针 C 签名,也不能把 i128 改成 {i64,i64} 后假定调用分配相同。生成 LLVM 并由目标 clang 降级,是遵守宿主寄存器和栈参数规则的直接方法。

原生当前路径面向小端、64 位指针目标;值槽步长 16 字节。对象文件采用目标工具链的对齐与重定位,不硬编码宿主寄存器。C __uint128_t、LLVM i128 和 Mach-O/ELF 的实际调用降级须在目标机验证。不同架构对象不能直接互链;Mach-O 展示的符号前导下划线不属于下述 LLVM 名称字符串。

原二:128 位值

常规打包函数定义为:V(p,t,s,n) = uint128(uint64(p)) | (t<<64) | (s<<72) | (n<<80)。移位前提升至 128 位。拆包为 p = low64(v)t=(v>>64)&15s=(v>>72)&15n=(v>>80)&0xffffffff

字节偏移(小端) 用途
0–63 0–7 整数低位、IEEE binary64 位或地址
64–71 8 类型区;当前读取只取低四位
72–79 9 子类型区;运行时当前读取只取低四位
80–111 10–13 原始长度或构造器编号
112–127 14–15 保留;常规新值清零,见负整数例外
类型号 解释 子类型/长度 指向内容
0 空值/单位 常规全零
1 元组、闭包、引用、引用数组 长度为槽数 连续 16 字节值槽,无独立对象头
2 整数 解码只读低 64 位为有符号数
3 小数 解码低 64 位为 double
4 静态字符串/字节串 长度含隐藏末尾零 直接指向字节,不指向对象头
5 低位通常 0/1
6 语言函数代码指针 通常其余区零 void(i128,i128) 函数
7 静态对象地址 通常其余区零 例如全局 i128 的地址
8 豫言栈地址 通常其余区零 当前或祖先帧;栈不移动
9 GC 转发地址 GC 内部 新堆对象首槽地址
10 堆字符串/字节串 长度含隐藏末尾零 对象头槽地址
11 字节串头 长度含隐藏末尾零 本值位于堆中,后随原始字节
12 运行时通用指针 分配游标等内部临时值 不得当作 GC 根扫描值
13 构造器实例 子类型=载荷槽数,长度区=类型内构造器编号 载荷槽首地址;零载荷指针为零
14 C 栈/局部游标标记 代码生成内部使用 不得泄入持久语言数据
15 未分配

GC 只移动类型 1、10、13;类型 11 使扫描器跳过字节数据。类型 9 只能用于转发;普通对象首槽不能伪造它。运行时在堆指针分类中断言不出现类型 12。

原二甲:非规范化值是兼容事实

  1. 编译器直接生成内建 / 时产生原始 i128 1/0,未附类型 5;比较原语及 C 爻转豫言值 则生成类型 5。条件判断只取低一位。消费者必须容纳这两种来源;不要对任意语言爻都调用带类型断言的 C 解码函数后宣称兼容。
  2. C 整数转豫言值(int64_t n) 先把有符号数转换为无符号 128 位,再清除类型字节并写 2。负数因此保留高位的 1。其精确公式为 (uint128(n) & ~(255<<64)) | (2<<64)。LLVM 内建减法则 sub i64 后零扩展并附 2。同一个负整数可有不同高位。比较整数必须取低 64 位;不能全宽比较或按高位判长度。
  3. 构造器载荷长度:前端允许至 250,后端写入完整八位,但 C 运行时子类型读取只取四位且 setter 断言不大于 15。故载荷超过 15 的构造器不能视为已安全兼容的原生 GC 对象。独立实现不得悄改读掩码而声称与旧运行时所有对象互通。修复须升级基线或采用显式桥接。

这些差异属于明确的兼容限制,不以“保留位任意”一语掩盖。

原三:具体对象

元组、闭包、引用

长度为 m>0 的元组占 m 个槽:分配 16*m 字节,把字段按源顺序写入,值为 V(p,1,0,m)。读取第 k 项为 load i128 (p+16*k),合法索引从零起。生成器零元素元组降为单位 0;C 元组转豫言值(0,...) 可产生类型 1、长度零、空指针,两者不宜混作普通语言可观察元组。

闭包使用普通元组,字段[0]=V(code,6,0,0),余项依闭包编译器自己的捕获顺序保存。调用者只读取第零项并把整个闭包作为隐含运行时实参;不得读取他方闭包的捕获字段。无捕获闭包仍为单槽元组。闭包的静态多态参数已擦除。

引用是长度一的元组。C 的写引用形参顺序是 豫言_写入引用(新值, 地址);与表层库函数的形参顺序不可混淆。引用数组为普通元组,数组长度在值头;写数组的 C 顺序是 (新值, 序数, 地址)。没有原生写屏障,当前 GC 不是分代增量收集器。

构造器实例

载荷为 [v0,...,v(m-1)],实例值为 V(p,13,m,c)c 从 1 起,在同一源文件中按同一返回类型头的构造声明次序计数。不同数据类型各自从 1 起,不能用全文件构造器总序号。零参构造器为 V(0,13,0,c),不分配堆。实例不携带编译期类型名;类型检查保证使用正确的判别方式。

/ 不是 C 同构数组表示。数组转同构列 产生的是 [元素元组, 整数长度],供特定原语使用。标准库链表必须按其类型的构造器编号及载荷表示。

字符串与字节串

逻辑字节数 L,存储长度 N=L+1;末尾一字节必须为零,不计入逻辑内容。堆串占 1+ceil(N/16) 槽;第一槽为 V(0,11,0,N),字节数据从下一槽开始;外部值为 V(header,10,0,N)。GC 拷贝整块而不扫描其中字节。静态串是 V(data,4,0,N),没有头槽,存活期覆盖程序运行。

逻辑内容可以含零字节,按长度的原语保留它;printf("%s")strlen 等 C 文本原语仍按零终止。不要因此推导所有字符串操作都支持内嵌零。复制字节为豫言值 接收不含末零的长度;复制字符串为豫言值 接收含末零的长度并检查末字节。逻辑长度须小于 UINT32_MAX

原四:豫言栈帧

函数硬件参数不是源语言参数。帧 S 指向以下值槽:

字节偏移 内容
0 0 祖先/调用者帧地址,类型 8
1 16 返回到的语言函数代码地址,类型 6
2 32 返回续延编号,带类型 2 的整数
3 48 此帧接收的调用结果;入调用时置单位
4 64 当前函数应执行的块编号;首次进入为整数 1
5… 80… 有序运行时参数,随后是续延存活局部值

经闭包调用时,槽 5 是闭包自身,槽 6 才是该次调用的显式参数。已知直接函数调用不传闭包,首个参数在槽 5。是否传闭包由目标入口契约决定,不能只看机器函数类型。

入口编号 1;其他续延编号由该函数生成器分配。本基线内部续延表包含编号 0 的分派入口,但正常初调从 1 开始。外部调用者只需把初调编号设为 1;旧函数保存给自己的返回编号必须原样传回,不得重新解释。

原四甲:源码饱和与机器逐参调用

类型检查要求显式参数给足,但隐式参数擦除把调用脊逐项降成一参数应用,并把显式 λ 降为一参数函数。普通跨模块函数值因此采用逐参闭包协议:f 于 a 于 b 在机器层先以 [f,a] 调 f 的代码,得到闭包 g,再以 [g,b] 调 g 的代码。不能一次把 [f,a,b] 填入旧入口。中间闭包是运行实现细节,不表示源码允许部分应用。

后端 IR 可表达多参数直接函数/闭包,但其实际参数个数必须由目标 IR/接口给出,不能从表层连续箭头数量猜测。独立实现最稳妥的外部值 ABI 是保留逐参闭包包装,内部优化可用自定多参数代码。例如二参数减法的普通闭包入口,先接收被减数并返回中间闭包,再由中间闭包接收减数。

普通调用

设调用者当前占用 q 槽,新帧 T=S+16*q。按以下次序生成逻辑操作:

  1. 将返回后仍活的值存入调用者帧,确保此前保存的参数槽和新增槽都是有效值;记录下一块编号 k
  2. T[0]=V(S,8,0,0)T[1]=V(当前代码,6,0,0)T[2]=整数(k)T[3]=0T[4]=整数(1)
  3. 把运行时实参写到 T[5+i]。写之前保证栈空间,不能等写坏内存才检查。
  4. 如堆游标超过阈值,按原六执行安全点。GC 后从帧重读移动过的参数/闭包;代码地址不移动。
  5. musttail call void 目标(i128 编码T, i128 当前分配值);紧跟 ret void。不依赖 C 调用栈保存续延。

返回

返回值为 r:先取本帧 P=S[0]F=S[1]K=S[2],写 P[3]=r,写 P[4]=K,以 P 和当前分配值尾调用 F,随后 ret void。旧运行时注释中“写第零项”不符合当前生成器;写错会毁坏祖先帧链。恢复的函数根据槽 4 分派,从槽 3 读取返回值,从保存槽重载局部量。

尾调用

复用当前帧,保留槽 0、1、2;重置槽 3、4,写新参数,再走安全点和尾跳。必须先将可能被覆盖的新参数求值并暂存,再覆盖帧。尾调用实参数更多时也须检查空间。不可把旧帧指针替换为自身返回参数槽。

原五:直接调用与公开入口

每个数据定义通常有可寻址全局 i128,初值零。后端 IR 还可承接公开直接函数入口,其参数不含闭包;函数作为值使用时仍须以包装闭包适配。下列名称算法作用于原始源文件身份字符串,通常为包上下文解析得到的绝对路径。

转义函数 E 逐字符把 -./*%+ 替成 _,其余原样保留。既不去文件后缀,也不做哈希或 Unicode 正规化。

  • 模块前缀:M(P) = 豫言模块_ + E(P)
  • 值全局:M(P) + _定义_ + E(名字) + _ + 十进制定序
  • 直接函数:M(P) + _函数_ + E(名字) + _ + 十进制定序
  • 模块计算入口:M(P) + _计算函数,零源参数、无闭包。
  • 程序入口:豫言_程序入口,同样为 void(i128,i128)

示例:源身份 /甲/乙。豫、名 减一、序号 4,值全局为 豫言模块__甲_乙_豫_定义_减一_4。替换并非单射;甲-乙甲_乙 可碰撞。不得自行改良旧符号算法。名称含 LLVM 引号等特殊字符的兼容范围须额外处理并验证。

定序是检查后/运行时并列定义表中的零基索引,不是源码行号,也不只是有运行时值的定义排名。类型声明与定义先合并;类型、命名空间占位及重导出可能影响表。类型内构造器序号与文件定序是两套数字。链接旧模块时优先从依赖接口/其生成 IR 取得完整定序;只有源文本则须重做相同的定义展开。

模块计算入口按定义次序注册全局根地址、求值并写全局;直接函数声明只提供代码。程序入口按依赖先于使用者的编译顺序各调用模块计算入口。单个模块入口没有自动的一次性初始化屏障;不允许随意再次调用。

原六:分配与 GC

必须提供的可见全局是 豫言_栈指针豫言_当前分配指针豫言_当前堆垃圾回收阈值,C 类型均为 豫言值*,LLVM 声明 external global i128*。生成函数持有局部分配游标;C 原语调用前同步游标和当前帧到全局,调用后重新读分配游标。

生成器元组以 bump allocation 分配:在游标处顺序写槽,然后游标前移槽数。没有每对象独立 GC 头,也不在每次分配时自动回收。C 分配器同样前移游标;在 C 调用中一般不能移动堆,因为 C 局部原始指针和 SSA 值未全部登记。不得把 malloc 地址包装成普通堆对象交给旧 GC。

安全点在语言函数调用之前,实参已写帧、存活调用者值已存栈时:比较游标与阈值,严格大于才调用 豫言_函数调用前垃圾回收(新帧, 整数(新帧槽数))。它把全局栈指针暂设到新帧末尾,回收后恢复至新帧首,返回单位。C 调用前后同步分配全局。该接口不返回新参数数组;帧中值与全局根被原地改写。

旧 GC 的协议:

  1. 根是 豫言_登记垃圾回收根 登记的值地址,以及 [豫言栈开始, 豫言_栈指针) 中每个值槽。不扫描普通 C 栈,不扫描任意机器寄存器。
  2. 类型 1/10/13 的非空旧堆指针需要搬迁;按值头计算大小。对象已搬迁时其旧首槽为类型 9,复用其中的新地址。
  3. 首次搬迁按完整槽数复制,把旧首槽覆盖为转发地址,并以原有类型、子类型、长度加新指针重建根值。
  4. 广度扫描新堆:普通值槽递归搬迁;遇类型 11,跳过 1+ceil(N/16) 槽;不得把原始字节当指针。
  5. 处理完释放旧堆,更新游标、堆界和阈值。存活比例超过 0.45 时下一轮尝试加倍,阈值为当前容量的 90%。容量策略可替换,但可见根更新协议不可改变。

堆不支持指向对象内部字段的任意移动指针。静态根登记的地址必须持续有效,不能登记即将消失的 C 栈局部。循环引用通过转发标记保持,共享引用必须仍共享;复制不能把同一引用变成两个独立引用。

原七:C 外部调用

通常生成 declare i128 @符号(i128,...) 和普通 call i128。所有源值保持豫言编码,不自动解箱为 C 整数、char* 或裸指针。C wrapper 自行解码和编码。外调名称为字符串,参数数目来自实参容器;无参 →0 参,单变量→1 参,元组变量序列→多参。表层任意表达式需先 成变量。

两个必须特别处理的旧接口:豫言_登记垃圾回收根 实际 C 签名是 void(豫言值*),但旧 LLVM 当 i128(i128) 调用并忽略结果;豫言_运行时报错并中止 实际返回 uint64_t,调用后终止。前者是平台相关的签名错配,不能要求所有目标都成立。新实现与旧 C 运行时互操作可用实际签名调用注册函数,把类型 7 地址解成裸指针;不要使用无意义的返回寄存器。若要逐条复现旧 LLVM,则必须保留错配并在精确目标验证。

引用 C 形参次序、字节长度是否含末零以及返回宽度都以运行时接口表为准,不能由中文函数名猜测。同一 C 符号不能在不同位置以不同参数数目声明而仍要求正常链接。

原八:异常、入口与运行环境

当前异常处理器是已注册全局根的闭包。默认处理器从帧槽 6 读取异常字符串,打印后 exit(1)豫言_获取当前异常处理器() 返回完整闭包;豫言_设置当前异常处理器(v) 替换它并返回单位。异常控制流的具体展开见执行篇。

续延异常表索引从 1 增长,记录类型 8 帧地址;豫言_设置续延异常处理器(编号,帧)豫言_获取续延异常处理器(编号) 保存/读取动态返回目标。提前返回用当前帧的返回代码与目标帧地址配合,不能实现为任意 C longjmp 并绕过豫言帧协议。帧生命周期之外的恢复不受稳定契约保护。

正常启动顺序:保存命令行;运行时参数处理;可选入口初始化(默认初始化堆);初始化全局异常处理器并登记根;随机种子初始化;分配豫言栈;设置根帧;进入 豫言_程序入口

根帧:[自身栈指针, 退出续延代码指针, 整数1, 单位, 整数1]。退出续延收到根帧,验证槽 4 为 1 后正常返回;运行时返回状态 0。显式退出和未捕获异常另行终止。保留 main 的原始 C 参数签名,不能将其视为豫言函数。

原生默认栈为 32*1024*1024 个槽,即 512 MiB。环境变量 YY_GC_INITIAL_STACK_SIZE_MB 在原生实现实际按“值×1024×1024 槽”处理,与其名称不符;WASI 则按 MiB 换算。堆变量 YY_GC_INITIAL_HEAP_SIZE_MBYY_GC_MAX_HEAP_SIZE_MB 按字节再除 16。重实现若修正这些外部配置行为,须明确标为不完全兼容。