185 lines
16 KiB
Markdown
185 lines
16 KiB
Markdown
# 小型字节码 VM
|
||
|
||
当前提供指令与代码对象、内部执行帧、取指执行循环,语言函数已接入执行器。参考 CPython 3.14 的职责划分和栈式执行设计;源码 lexer/parser 由独立 [frontend](frontend.md) 提供,基础源码已由独立 [compiler](compiler.md) 接入字节码生成;执行器不读取 Python `.pyc`,不承诺 CPython 操作码编号、ABI 或完整语言兼容性。
|
||
|
||
Endor 是解释型语言,字节码是内部执行表示;从 AST 生成字节码属于自动执行准备,不要求使用者发布编译产物。当前没有字节码持久化缓存或跨版本装载协议;现有不可变代码对象仍可在 VM 内共享复用。平台升级与源码修订的区别见 [执行与版本模型](../architecture/execution-and-versions.md)。
|
||
|
||
指令、CodeLayout、EndorCode 与 EndorCodeObject 的定义、编解码和静态校验位于 objects 包;core 只拥有 VM、执行帧与求值循环。执行器通过公开的只读代码接口读取指令、槽名和共享代码,通过 ObjectStore 委托维护对象根;现有 endor_core 类型导入继续可用。
|
||
|
||
## 与 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` 也可以接收原始字节序列,先解码并验证再使用。
|
||
|
||
所有执行位置和跳转距离以**两字节代码单元**计数,包含扩展参数单元;不是字节偏移,也不一定等于逻辑指令下标。前跳从当前逻辑指令之后的位置加距离;后跳从该位置减距离。目标必须是完整逻辑指令的开头,不能跳进扩展参数中间。
|
||
|
||
当前有 30 个可执行操作码和一个由编解码器处理的 EXTENDED_ARG:
|
||
|
||
| 指令组 | 内容 |
|
||
| --- | --- |
|
||
| 常量与局部变量 | `LoadConst`、`LoadFast`、`StoreFast`、`DeleteFast` |
|
||
| 名称与属性 | `LoadGlobal`、`StoreGlobal`、`DeleteGlobal`、`LoadAttr`、`StoreAttr` |
|
||
| 栈操作 | `Nop`、`PopTop`、`Copy`、`Swap`、`PushNull` |
|
||
| 函数定义、调用与返回 | `MakeFunction`、`Call`、`ReturnValue` |
|
||
| 控制流 | `JumpForward`、`JumpBackward`、`PopJumpIfFalse`、`PopJumpIfTrue` |
|
||
| 真值与比较 | `ToBool`、`UnaryNot`、`CompareOp` |
|
||
| 闭包 | `LoadDeref`、`StoreDeref`、`DeleteDeref`、`LoadClosure` |
|
||
| 容器与类型 | `BuildMap`、`BuildClass` |
|
||
|
||
没有把 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 启动指令。
|
||
|
||
## 执行循环与函数调用
|
||
|
||
```text
|
||
Vm::execute(code, globals)
|
||
创建临时函数
|
||
→ Vm::call
|
||
→ 准备第一个 EndorFrame
|
||
→ 读取当前指令
|
||
→ match opcode 执行操作
|
||
→ 推进或跳转到下一位置
|
||
→ CALL 切入新帧 / RETURN_VALUE 恢复调用者
|
||
```
|
||
|
||
语言函数互调使用一个显式帧向量,不递归调用 Rust 求值器。调用者已推进的指令位置就是返回后继续的位置,`previous` 用向量索引连接调用者。NativeFunction 则通过已有的 Rust 回调入口执行。
|
||
|
||
普通函数调用采用 `[callable, NULL, arg0, ...]` 栈布局,CALL 参数是显式位置参数个数。NULL 是内部标记,与 `Value::Nil` 不同;如果第二个位置是实际值,就把它作为隐式接收者加在参数前。尚未实现属性描述符或自动方法绑定。
|
||
|
||
## 在字节码中创建函数
|
||
|
||
`MakeFunction`(操作码编号 28)将代码对象、已经求值的默认参数和共享 Cell 组装为一个新的语言函数。使用 `Instruction::make_function(default_count, closure_count)` 构造指令,两个计数均为 u16(0~65535):参数低 16 位为默认参数数,高 16 位为闭包 Cell 数;大参数继续由 EXTENDED_ARG 编解码。不含默认参数和闭包时也可使用 `Instruction::simple(Opcode::MakeFunction)`。
|
||
|
||
执行前后栈形状如下,右边为栈顶:
|
||
|
||
```text
|
||
[..., default0, ..., defaultN, cell0, ..., cellM, code]
|
||
→ [..., function]
|
||
```
|
||
|
||
默认值按尾部位置参数的声明顺序排列,Cell 按目标代码的 free_variables 顺序排列。没有默认值或闭包时省略对应区域;每个默认值、每个 Cell 和代码对象都占一个栈项。函数名取自目标代码的 name;globals 使用当前帧的 globals,builtins 在创建时从该 globals 的可选 __builtins__ 字典捕获,与宿主 new_function 工厂一致。
|
||
|
||
例如,无参数的函数定义与调用可生成如下逻辑指令。函数体代码对象预先放入当前代码的常量表中:
|
||
|
||
```text
|
||
LoadConst body_code_index
|
||
MakeFunction 0
|
||
StoreGlobal function_name_index
|
||
LoadGlobal function_name_index
|
||
PushNull
|
||
Call 0
|
||
ReturnValue
|
||
```
|
||
|
||
嵌套函数使用 `LoadClosure` 取出当前帧的共享 Cell,再压入内层代码对象并执行 `MakeFunction`。创建函数不运行函数体;默认值表达式应先由外围代码求值,创建时保存其结果,因此同一函数的后续调用共享这些默认值。重复执行创建指令会产生不同函数对象,它们可以共享同一代码对象。
|
||
|
||
静态校验根据两个计数检查所需栈高度、合流高度和最大栈深。执行时复用 new_function 校验代码对象种类、默认参数数量、闭包数量与 Cell 种类;NULL 调用标记不能用作任何输入值。类型和数量不匹配通过现有 VmError 返回。所有输入在完整校验及分配成功前都保持在栈中,成功后由函数持有其托管引用,再消费输入并压入函数的一份根;失败时沿现有帧清理路径释放输入。
|
||
|
||
函数创建后不提供修改其默认值或闭包的指令。当前一条 MakeFunction 直接构造完整函数,不要求 tuple 对象或 CPython 3.13 起的 SET_FUNCTION_ATTRIBUTE;操作数格式和操作码编号均为 Endor 自有协议。
|
||
|
||
## 对象引用与返回值
|
||
|
||
每个帧持有函数的一份执行根,函数继续保活代码、全局字典、内建字典、默认值及闭包。局部槽和求值栈中的每个对象值也各自持有执行根,重复出现分别计数。执行根复用现有外部强引用计数,所以循环回收能保留它们。
|
||
|
||
装载借用值时先 retain,弹栈或清空槽时 release;槽之间移动直接转移持有。调用先准备并保活被调函数的参数,再清理调用者的输入。返回值从当前帧移交给调用者栈或宿主,帧清理不会释放这一份结果。
|
||
|
||
**API 变化:**`Vm::call` 现在接收 `&mut self`,语言函数和原生函数统一返回持有的一份结果。返回 `Value::Object` 后,宿主完成使用时调用 `vm.release_value(result)`,或取出句柄后 `release`。原生回调本身仍接收借用参数、返回借用值;VM 会验证并 retain 回调的结果,之后才弹出输入参数。即使回调返回的是刚在字节码里创建的临时参数,也不会在清理输入时失效。
|
||
|
||
`get_attr`、`dict_get`、`dict_items`、`cell_get` 仍返回借用内容,不属于上述调用返回值变化。
|
||
|
||
执行出错时,当前尚未消费的栈值仍由帧持有;所有活动帧按从内到外顺序清理。已完成的全局字典/属性写入不会回滚,但未完成操作和临时执行根会清理。字典构造失败时也会清理尚未返回的临时字典。
|
||
|
||
## 最小示例
|
||
|
||
```rust
|
||
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 的 frontend/compiler 已能从源码生成 AST 和字节码,也不解析 Python 源码。宿主仍负责准备代码对象和执行环境;函数既可由宿主 new_function 工厂创建,也可由上述 MakeFunction 在字节码运行期间创建。
|
||
|
||
## 限额、诊断与边界
|
||
|
||
`ExecutionLimits` 默认限制 1000 个活动帧,指令配额为 `usize::MAX`。`execute_with_limits`/`call_with_limits` 可显式指定限额,嵌套语言调用和原生分派共享计步器;每次原生分派额外计一步,扩展参数不单独计步。原生 Invoke 委托通过同一帧循环推进,不重置预算;详见 [原生调用](native-calls.md)。指令预算不会抢占正在执行的宿主回调。
|
||
|
||
运行错误仍返回 `VmError`。`last_traceback()` 另提供从外到内的函数名、文件、代码首行和出错指令偏移;它保存独立元数据,不保活已经清理的帧。首行是代码声明的 first_line,不是由行号表推导出的实际错误源码行。成功的新一次调用会清空旧记录。尚无语言异常对象传播、raise/try/except 或完整源码行号表。
|
||
|
||
暂未实现:源码闭包编译、关键字/可变参数、生成器/协程、迭代器专用指令、数值算术、自动方法绑定、指令特化和 JIT。
|
||
|
||
## 验证
|
||
|
||
[execution.rs](../../crates/core/tests/execution.rs) 覆盖 wordcode、控制流校验、变量、循环、嵌套调用与默认参数、闭包、字节码函数定义/默认值/环境捕获、容器和返回值所有权、函数创建失败与后续错误清理及帧/指令限额;已有 builtins 测试已迁移共享代码对象和持有式调用返回值。lib.rs 另有可供远端 rustdoc 执行的例子。
|
||
|
||
验证命令与结果边界统一见 [验证](../operations/validation.md)。限额与环境隔离见 [配置](../operations/configuration.md),数据与校验见 [objects 源码导航](../../crates/objects/src/README.md),执行职责见 [core 源码导航](../../crates/core/src/README.md)。
|
||
|
||
参考:[CPython 解释器设计](https://github.com/python/cpython/blob/v3.14.0/InternalDocs/interpreter.md)、[执行帧设计](https://github.com/python/cpython/blob/v3.14.0/InternalDocs/frames.md)、[帧结构](https://github.com/python/cpython/blob/v3.14.0/Include/internal/pycore_interpframe_structs.h)、[指令定义](https://github.com/python/cpython/blob/v3.14.0/Python/bytecodes.c)、[求值入口与执行器](https://github.com/python/cpython/blob/v3.14.0/Python/ceval.c)。
|
||
|
||
## 宿主共享引用的赋值协议
|
||
|
||
`ObjectPayload::assign_from(source)` 是可选协议,默认返回 false,普通值与对象仍替换变量绑定。StoreGlobal、StoreFast、StoreDeref 在既有值和新值均为对象时调用此协议;返回 true 表示宿主对象已替换自己的目标,绑定保留旧对象身份,其他别名随共享引用观察更新。相同句柄赋值为无操作。此协议同时适用于这些存储指令实现的声明初始化/赋值;字典 API 与 StoreAttr 不隐式调用它。
|
||
|
||
协议不传递变量名字或 UI 元数据,也不引入渲染器。实现必须先验证再修改,拒绝替换时保留旧目标;不得通过可变 payload 悄悄增加未跟踪的 VM 对象边。宿主原生关系如持有受管理对象,必须通过 VM retain/release 登记根。原生资源的内部引用可以由宿主维护,但其管理对象身份和存活根仍由 Aster 校验和计数。StoreFast 会先持有选择后的值,再释放栈值与旧槽,失败执行仍走统一帧清理。
|
||
|
||
|
||
## 类创建与方法读取
|
||
|
||
BuildClass(编号 29,无参数)消费四个栈值 `[name, baseType, fieldsDict, methodsDict]`,产生一个受管类对象;验证器按 4 入 1 出检查栈。字段字典按声明顺序保存字段名与注解名,方法字典保存 MakeFunction 创建的函数。VM 先验证基类、字段与方法关系,再创建完整类型;输入根在成功前保持在执行栈上,失败由帧统一清理。
|
||
|
||
LoadAttr 对实例自有属性返回原值;缺失时沿类型父链找方法并创建绑定方法。绑定方法持有 receiver 和选定的函数,调用时经已有非递归委托路径补入 self。类对象上的方法读取提供未绑定函数;type/name/base 元数据优先。宿主读取继承方法应使用返回拥有式结果的 bind_attr;get_attr 保持借用式读取,类型方法结果为未绑定函数。所有委托消耗同一指令预算。
|