Rils 的执行后端分为两条路径:树遍历解释器负责快速验证完整语言语义;字节码后端把已经稳定的 语义逐步固化为更紧凑、可验证、可重复执行的表示。两条路径在迁移期间并存,并用相同源码的结果 对照测试防止语义漂移。
第一阶段已经贯通以下流水线:
source -> lexer/parser -> static analysis -> HIR -> MIR -> bytecode -> verifier -> VM
当前 crate 边界中,rils_frontend 负责解析和静态分析,rils_compiler 负责 HIR/MIR lowering;主
crate 消费 MIR 并完成字节码编码、验证和 VM 执行。encoder 迁移前需先让字节码类型表摆脱运行时
StructType/EnumType,改为可独立验证和链接的静态描述。
- HIR 完成词法作用域名称解析,把局部变量转换为稳定槽位,并保留源码范围。
- MIR 使用寄存器和基本块显式表示值流与控制流。
- 编码器把基本块展平为指令流,并解析跳转目标。
- 验证器在执行前检查常量、局部槽位、寄存器和跳转目标。
- VM 每次执行创建独立的寄存器、局部槽位和显式调用帧,因此同一模块可以安全地重复执行。
execute_with_limit保留只配置指令步数的便捷入口;execute_with_limits和execute_with_host_and_limits使用共享ExecutionLimits同时配置指令步数与调用深度。- 调用栈默认限制为 1024 帧。字节码 VM 使用显式帧;AST 解释器按需增长独立栈段。两者都会在 超限时返回带源码位置的错误,而不是继续递归直到宿主线程栈溢出。
- 模块导入按稳定名称、签名、宿主 ABI 版本和 capability 链接;校验在 VM 启动前完成。
当前支持常量、局部 let/let mut、局部赋值、基础一元/二元运算、短路逻辑、块表达式、
if、while、loop、break value、continue、函数、参数、直接命名调用、递归、函数值、
间接调用、嵌套函数、词法闭包和 return。迭代控制流支持 Range、拥有型数组、Vec、HashMap、
HashSet 及脚本自定义 Iterator / IntoIterator 的 for,包括 break value 与 continue。
复合值已覆盖 tuple、数组、重复数组、Range、Option 和 Result,以及局部 tuple/数组的索引读取
与写入、tuple 数字字段读写。函数内的 ? 会在 VM 调用帧上直接传播 Err。局部和参数读取沿用
Rils 的显式所有权语义:Copy 值复制,其他值 move;索引读取也会对非 Copy 元素执行部分 move。
match 已支持字面量、通配、变量绑定、Some/None、Ok/Err,以及 struct/enum 模式编码。
类型别名在静态阶段展开,泛型函数当前采用静态检查后的类型擦除字节码。
VM 局部槽位已经改为带可变性、移动状态和活动引用计数的共享存储。字节码支持局部变量与数组
元素的 &T/&mut T、引用参数、Copy 值解引用读取和 *reference = value,并在词法块退出时
发出显式局部清理指令,避免不可见引用继续占用所有者。
模块内的 struct/enum 定义会进入独立类型表。当前支持 struct record、enum unit/tuple/record variant 构造,局部 struct 字段的 move/Copy 读取、写入和借用,以及 struct/enum 的 record、tuple 和 unit 模式解构。泛型字段暂以运行时实际值补全字段槽位类型,完整类型参数表留待方法链接阶段。
impl 方法会登记为带 receiver 元数据的模块函数。关联函数、struct/enum 成员方法、trait impl 和
<Type as Trait>::method(...) UFCS 均进入普通调用帧;self/mut self 执行 move,&self/
&mut self 自动创建相应引用。泛型方法与泛型函数一样采用静态检查后的类型擦除执行。
内联模块中的函数和用户类型会扁平化为稳定的 module::symbol 链接名,多段路径调用、嵌套模块、
模块限定的 struct/enum 构造和 use path [as alias] 已进入字节码后端。模块函数解析未限定名称时
优先查找自身命名空间,因此不同模块的同名私有辅助函数不会错误互链。模块声明和导入本身不生成
运行时指令。模块内 impl、关联函数、成员调用和模块内 UFCS 使用同一限定符号表。项目模式下
compile_file 按 rils.toml 的 script_paths 建立完整模块目录,归一化 crate/self/super 路径,
并调用所选文件的零参数 fn main();无项目文件时保留 name.rils / name/mod.rils 规则。
函数值由函数表索引和捕获槽位组成。HIR 提升嵌套函数并形成显式捕获列表,MIR/字节码分别使用 创建闭包和按值调用指令;捕获槽位与外层 frame 共享,因此可变捕获在多次调用间保持状态,返回 闭包后仍然有效。验证器检查函数索引、捕获布局和寄存器,间接调用在执行时检查 arity。引用不能 进入闭包环境。无须函数值的具名调用仍保留直接调用指令。任意返回函数值的表达式都可直接作为 callee;UFCS 方法可作为未绑定函数值,Copy 的按值 receiver 可形成绑定方法值。非 Copy 的按值 receiver 和引用 receiver 只允许立即调用,不能进入可复制的绑定方法值。
方法 receiver 不再要求是单独的局部变量:临时值、嵌套字段和索引 place 都可调用方法。字段/索引
投影也可从引用根开始,&*reference / &mut *reference 通过显式 reborrow 指令保留原目标及
可变性约束。
确定性的 core/prelude 函数和 Vec 基础操作通过导入表调用。默认 BytecodeHost 提供 core;
std::io 与 std::fs 的实现可分别启用,并受同名 capability 控制。编译期 HostContract 可声明
自定义宿主函数的稳定 ID、完整名称、固定签名和 capability;compile_with_host /
compile_file_with_host 会让这些声明参与静态分析并生成普通 import,运行前由
BytecodeModule::validate_host 或正常执行路径链接到 BytecodeHost。自定义
Iterator/IntoIterator 的脚本方法登记在模块迭代表中,for 会通过普通 VM 调用帧驱动它们。
跨工具交换使用严格、确定性排序的 Host Manifest v2,不直接序列化 Rust 结构。
当前覆盖边界汇总如下:
| 能力 | 状态 | 说明 |
|---|---|---|
| 表达式与控制流 | 已支持 | 运算、块、if、while、loop、for、break/continue、return、match |
| 函数 | 已支持 | 直接/任意表达式间接调用、递归、函数值、绑定方法值、嵌套闭包、可变捕获、泛型类型擦除 |
| 复合类型 | 已支持 | tuple、数组、Range、Option/Result、struct/enum、类型别名 |
| 所有权与引用 | 已支持基础层 | move/Copy、解引用、reborrow,以及引用根和 struct/tuple/数组/Vec 混合投影链的读取、赋值和局部借用 |
| Trait 与方法 | 已支持 | 关联函数、四种 self、任意 receiver、trait impl、UFCS/UFCS 函数值、模块内 impl |
| 模块 | 已支持 | 内联模块、use/as、多段路径及 compile_file 外部模块链接 |
| 迭代器 | 部分支持 | Range、数组、Vec 和自定义 Iterator/IntoIterator;借用迭代器待实现 |
| 标准库/宿主 | 部分支持 | core/Vec、内置宏、显式授权的 std::io/std::fs,以及编译期自定义 HostContract 已链接;解释器 Engine 与同一契约的整合待完成 |
| 磁盘预编译 | 实验可用 | .rilbc v5、bytes/file API、CLI compile/verify/run;尚未承诺跨版本稳定 |
Rust 宿主入口如下:
let module = rils::compile("let mut n = 1; while n < 5 { n = n + 1; } n")?;
println!("instructions: {}", module.instruction_count());
let value = module.execute()?;
let game_scripts = rils::compile_file("scripts/main.rils")?;
let image = game_scripts.to_bytes()?;
let loaded = rils::BytecodeModule::from_bytes(&image)?;
game_scripts.write_file("scripts/main.rilbc")?;
let loaded_file = rils::BytecodeModule::read_file("scripts/main.rilbc")?;
let io_script = rils::compile("std::fs::try_exists(\"save.dat\")")?;
let mut host = rils::BytecodeHost::standard();
host.enable_standard_fs()?;
let result = io_script.execute_with_host(&host)?;
let mut contract = rils::HostContract::new();
contract.register_function(
100,
"unity_engine::time::frame_count",
rils::FunctionSignature::fixed(Vec::new(), rils::Type::Integer(rils::IntegerType::U64)),
"unity.time",
)?;
let game = rils::compile_with_host("unity_engine::time::frame_count()", &contract)?;
let mut game_host = rils::BytecodeHost::new(rils::BYTECODE_HOST_ABI_VERSION);
game_host.allow_capability("unity.time");
game_host.register_function(
"unity_engine::time::frame_count",
rils::FunctionSignature::fixed(Vec::new(), rils::Type::Integer(rils::IntegerType::U64)),
"unity.time",
|_| Ok(rils::Value::U64(42)),
)?;
game.validate_host(&game_host)?;不在当前子集中的合法语义会返回带源码范围的 CompileError,不会静默退回解释执行。当前 AST
中的表达式、控制流和模块级声明语法均已有字节码路径;剩余边界是借用迭代器、尚未实现的
借用迭代器,以及解释器 Engine 注册内容与 HostContract 的统一描述。运行时错误使用
BytecodeError,同样可以通过 render
生成带源码位置的诊断。
当前已实现实验性 .rilbc v5。它采用带版本的显式小端容器,不直接序列化任何 Rust enum、地址或
内存布局:
magic | format version | language version | host ABI | pointer width | flags | section directory | CRC32
v5 包含 module、imports、types、iterators、functions、sources 和 trait implementations 七个必需
section。trait implementations 表以受 verifier 校验的类型名、trait 名、声明 SourceId、方法名和函数索引保留实现身份,
宿主无需扫描源码或猜测函数名即可发现入口并精确分发 trait 方法。sources 表只
保存确定性 SourceId -> 来源名称 映射,不嵌入源码正文;常量、指令和源码 Span 使用各自的显式
tag/字段编码,每个 Span 都携带 SourceId。加载器限制文件为 64 MiB、单个字符串为 1 MiB、通用集合为一百万项、
函数/类型/导入表各 65,536 项、总指令两百万条、单函数寄存器和局部槽位各 262,144 个、类型/模式
嵌套为 128 层,并验证目录边界、重叠、UTF-8、标量范围和 CRC32。集合还必须与 section 剩余字节数
相符,避免小文件用伪造 count 触发不成比例的预分配。解码完成后仍必须通过
现有 verifier,函数、常量、类型、导入、寄存器、局部槽位和跳转索引都不会被信任。未知必需
section 拒绝加载,未知可选 section 在完成边界验证后跳过。
由于语言当前存在 usize/isize,v5 记录目标指针宽度,32 位和 64 位产物不允许交叉加载,避免
发生静默截断。format version、language version 和 host ABI 分别检查。当前格式仍处于 0.3.0
实验期,后续不兼容调整会提升格式版本;尚未承诺长期跨版本兼容。
格式 v5 在 v4 的显式整数 cast、稳定 ID intrinsic 和来源文件表之外增加 trait implementation 表。
旧 loader 不会误读这些内容;
升级后的 loader 也会明确拒绝旧文件,项目需要从源码重新生成 .rilbc/.bytes。
CLI 入口为:
rils compile scripts/main.rils -o scripts/main.rilbc
rils verify scripts/main.rilbc
rils run scripts/main.rilbccompile 仍不访问文件;CLI 和 compile_file 采用统一的项目/兼容模块加载规则,将入口和模块
链接为一个 .rilbc。C# 的 RilsRuntime.LoadBytecode(byte[]) 可直接消费 AssetBundle 或
Addressables 中的 bytes,不要求发布包保留 .rils 文件布局。
Unity Editor 也可通过 RilsModule.GetBytecode() 或 WriteBytecodeFile(path) 将 C API 编译得到的内存
module 导出为构建产物。
格式版本和语言版本分开维护:前者描述二进制编码兼容性,后者描述脚本语义。首个磁盘格式只有在 函数调用、复合值、模块依赖和宿主 ABI 进入字节码后端后才冻结,避免早期实现细节成为长期包袱。
游戏嵌入场景还需要独立配置最大指令数、调用深度、堆内存、容器长度和宿主 IO 能力。预编译文件 只消除前端解析与降低成本,并不天然意味着可信;加载器验证和运行时资源预算都必须保留。
未来的格式、性能和运行时增强统一记录在仓库根目录的 TODO.md,不作为当前字节码 格式承诺。