Files
endorphin-aster-vm/docs/runtime/bytecode.md
T
codex-bot 18cd24a203
Verify databases and deploy Aster VM / verify-and-deploy (push) Successful in 15m7s
Align package and executable names with workspace directories
2026-10-18 21:15:10 +08:00

10 KiB
Raw Blame History

小型字节码 VM

当前提供指令与代码对象、内部执行帧、取指执行循环,语言函数已接入执行器。参考 CPython 3.14 的职责划分和栈式执行设计;不实现 lexer/parser,不读取 Python .pyc,不承诺 CPython 操作码编号、ABI 或完整语言兼容性。

与 CPython 的对应

职责 CPython 3.14 Endor
指令定义与解码 opcode/oparg、_Py_CODEUNIT、EXTENDED_ARG Opcode、Instruction、DecodedInstruction
保存可执行代码 PyCodeObject 堆中的 EndorCodeObject,持有不可变 EndorCode
一次调用的状态 _PyInterpreterFrame EndorFrame
执行入口 PyEval_EvalCode、函数调用入口 Vm::execute、Vm::call
帧求值 _PyEval_EvalFrameDefault eval_frames 的取指/分派循环

Vm::new() 仍从空堆启动,按宿主需求创建代码、全局字典和函数。没有自动加载 Python type、语言根类或默认 print。

指令:要做什么

Instruction 是一个操作码与 u32 参数。实际 wordcode 每个代码单元为 [opcode: u8, argument: u8],先操作码、后参数,与机器端序无关。大于 255 的参数在前面使用最多三个 EXTENDED_ARG 单元补足高位。单个逻辑指令因此可以占多个代码单元。

Instruction::encode_all 接受逻辑指令并自动生成扩展参数。EndorCode::with_layout 也可以接收原始字节序列,先解码并验证再使用。

所有执行位置和跳转距离以两字节代码单元计数,包含扩展参数单元;不是字节偏移,也不一定等于逻辑指令下标。前跳从当前逻辑指令之后的位置加距离;后跳从该位置减距离。目标必须是完整逻辑指令的开头,不能跳进扩展参数中间。

首版有 28 个可执行操作码和一个由编解码器处理的 EXTENDED_ARG:

指令组 内容
常量与局部变量 LoadConst、LoadFast、StoreFast、DeleteFast
名称与属性 LoadGlobal、StoreGlobal、DeleteGlobal、LoadAttr、StoreAttr
栈操作 Nop、PopTop、Copy、Swap、PushNull
调用与返回 Call、ReturnValue
控制流 JumpForward、JumpBackward、PopJumpIfFalse、PopJumpIfTrue
真值与比较 ToBool、UnaryNot、CompareOp
闭包 LoadDeref、StoreDeref、DeleteDeref、LoadClosure
容器 BuildMap

没有把 Number 转为 f64 来补运算。数字仍以无损表示为主;真假可以根据是否为零判断,数值比较和算术尚未定义。文本可作内容比较,普通对象使用现有比较接口。

代码构造时检查:未知操作码、奇数字节长度、悬空/过长扩展参数、非法索引、错误 Cell 槽、无效跳转、栈下溢、控制流汇合的栈高度不一致,以及可达路径跌出代码末尾。返回时栈必须只剩一个返回值。验证同时计算最大求值栈深度。NULL 调用标记的实际用法在执行时继续检查,它不能作为语言值返回。

代码对象:保存静态信息

EndorCode 保存 wordcode、解码结果、常量、名称表、局部变量布局、参数与自由变量信息、最大栈深度、代码名和源码位置元信息。它是不可变 Rust 数据。

vm.new_code(code) 把数据放进 EndorCodeObject,分配一个受管理的堆对象。代码对象持有常量中的对象引用,并参与引用计数和循环回收;函数持有代码对象句柄,所以多个函数可共享同一代码对象,代码常量也只由这一对象统一持有。

CodeLayout 规定:

  1. locals 的开头必须与 parameters 一致,目前只支持位置参数。
  2. cell_variables 必须来自本地变量名;捕获的局部变量仍占原来的本地槽位,但槽内保存 Cell。
  3. free_variables 的 Cell 槽紧接所有本地槽位;不得与本地名称重复。
  4. names 提供全局名称和属性名称的索引表。

函数通过 vm.new_function(name, code_ref, globals, defaults, closure) 创建。工厂检查代码与全局字典种类、默认参数数量和闭包 Cell 数量,并从 globals 的可选 __builtins__ 字典捕获内建环境。全局查找先查 globals,缺失再查捕获的 builtins。

执行帧:这一次运行到哪里

EndorFrame 不是堆中的语言对象,没有 EndorObject 头。它类似 CPython 的内部 _PyInterpreterFrame;以后可另加可见的帧包装,不把反射对象强加给每次调用。

帧保存函数及代码引用、共享可执行数据、globals/builtins、调用者帧索引、当前与下一条指令位置、本地/Cell/自由变量槽,以及求值栈。Rust 使用安全容器表达这些逻辑区域,没有复制 CPython 的 C 指针和柔性数组物理布局。

帧初始化时绑定位置参数及尾部默认参数,未绑定局部槽为空,捕获的局部槽转为 Cell,并放入闭包提供的自由变量 Cell。读取空槽或已删除变量会报错。Cell 初始化在建帧时完成,首版无需单独的 MAKE_CELL/COPY_FREE_VARS 启动指令。

执行循环与函数调用

Vm::execute(code, globals)
  创建临时函数
  → Vm::call
  → 准备第一个 EndorFrame
  → 读取当前指令
  → match opcode 执行操作
  → 推进或跳转到下一位置
  → CALL 切入新帧 / RETURN_VALUE 恢复调用者

语言函数互调使用一个显式帧向量,不递归调用 Rust 求值器。调用者已推进的指令位置就是返回后继续的位置,previous 用向量索引连接调用者。NativeFunction 则通过已有的 Rust 回调入口执行。

普通函数调用采用 [callable, NULL, arg0, ...] 栈布局,CALL 参数是显式位置参数个数。NULL 是内部标记,与 Value::Nil 不同;如果第二个位置是实际值,就把它作为隐式接收者加在参数前。尚未实现属性描述符或自动方法绑定。

对象引用与返回值

每个帧持有函数的一份执行根,函数继续保活代码、全局字典、内建字典、默认值及闭包。局部槽和求值栈中的每个对象值也各自持有执行根,重复出现分别计数。执行根复用现有外部强引用计数,所以循环回收能保留它们。

装载借用值时先 retain,弹栈或清空槽时 release;槽之间移动直接转移持有。调用先准备并保活被调函数的参数,再清理调用者的输入。返回值从当前帧移交给调用者栈或宿主,帧清理不会释放这一份结果。

API 变化:Vm::call 现在接收 &mut self,语言函数和原生函数统一返回持有的一份结果。返回 Value::Object 后,宿主完成使用时调用 vm.release_value(result),或取出句柄后 release。原生回调本身仍接收借用参数、返回借用值;VM 会验证并 retain 回调的结果,之后才弹出输入参数。即使回调返回的是刚在字节码里创建的临时参数,也不会在清理输入时失效。

get_attr、dict_get、dict_items、cell_get 仍返回借用内容,不属于上述调用返回值变化。

执行出错时,当前尚未消费的栈值仍由帧持有;所有活动帧按从内到外顺序清理。已完成的全局字典/属性写入不会回滚,但未完成操作和临时执行根会清理。字典构造失败时也会清理尚未返回的临时字典。

最小示例

use endor_core::{CodeLayout, EndorCode, Instruction, Opcode, Value, Vm};

fn main() -> Result<(), endor_core::VmError> {
    let mut vm = Vm::new()?;
    let globals = vm.new_dict()?;
    let code = EndorCode::from_instructions(
        &[
            Instruction::new(Opcode::LoadConst, 0),
            Instruction::simple(Opcode::ReturnValue),
        ],
        vec![Value::Text("Hello Endor".into())],
        CodeLayout::default(),
    )?;
    let code = vm.new_code(code)?;
    let result = vm.execute(code, globals)?;
    assert_eq!(result, Value::Text("Hello Endor".into()));
    vm.release_value(result)?;
    vm.release(code)?;
    vm.release(globals)?;
    Ok(())
}

该示例直接提供指令,不表示已经支持 Endor 或 Python 源码解析。函数和嵌套函数目前由宿主工厂创建,尚无 MAKE_FUNCTION 指令;后续编译器接入时再补函数定义所需的操作。

限额、诊断与边界

ExecutionLimits 默认限制 1000 个活动帧,指令配额为 usize::MAX,正常使用不设置有限的指令预算。execute_with_limits/call_with_limits 可显式指定限额,所有嵌套语言调用共享计步器;扩展参数不单独计步。可选指令预算是 Endor 的宿主运行边界,不是 CPython 默认行为,也不会抢占正在执行的宿主回调。

运行错误仍返回 VmError。last_traceback() 另提供从外到内的函数名、文件、代码首行和出错指令偏移;它保存独立元数据,不保活已经清理的帧。首行是代码声明的 first_line,不是由行号表推导出的实际错误源码行。成功的新一次调用会清空旧记录。尚无语言异常对象传播、raise/try/except 或完整源码行号表。

暂未实现:lexer/parser、源码编译器、关键字/可变参数、生成器/协程、迭代器专用指令、数值算术、MAKE_FUNCTION、自动方法绑定、指令特化和 JIT。

验证

execution.rs 覆盖 wordcode、控制流校验、变量、循环、嵌套调用与默认参数、闭包、容器和返回值所有权、错误清理及帧/指令限额;已有 builtins 测试已迁移共享代码对象和持有式调用返回值。lib.rs 另有可供远端 rustdoc 执行的例子。

验证命令与结果边界统一见 验证。限额与环境隔离见 配置,文件职责见 core 源码导航。

参考:CPython 解释器设计、执行帧设计、帧结构、指令定义、求值入口与执行器。