Files
endorphin-aster-vm/docs/runtime/bytecode.md
T

207 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 小型字节码 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 数据。可选 SourceSignature 还保存形参/返回注解名称和声明类名;Compiler 保留这些数据,宿主可启用[源码函数注解检查](types.md#源码函数注解检查)。
`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 自有协议。
## 对象引用与返回值
每个帧持有函数的一份执行根,函数继续保活代码、全局字典、内建字典、默认值及闭包。启用源码注解时,帧还持有本次返回约束的 Type/Trait 根,成功或失败清理时释放。局部槽和求值栈中的每个对象值也各自持有执行根,重复出现分别计数。执行根复用现有外部强引用计数,所以循环回收能保留它们。
装载借用值时先 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)。指令预算不会抢占正在执行的宿主回调。
`start_read_only_call(function, arguments, limits, slice)` 和 `resume_read_only_call(slice)` 提供可恢复的只读字节码调用。`ExecutionProgress::Pending` 保留同一 VM 中的活动帧、局部槽、栈、原生委托返回契约、读取追踪及所有执行根;继续时不重放已执行指令。`Complete` 返回拥有式 value、借用式 reads 和累计 instructions。slice 必须大于0,暂停只能发生在字节码指令边界;原生回调和原生委托链原子完成,软分片不保证墙钟延迟,整个调用仍共用原始硬指令/帧限额。
暂停期间只读保护一直有效;已有对象不可写,也不能用普通 call 或第二个 start 在同一 VM 中交错执行。宿主不得更改只读作用域栈或手动释放并非自己拥有的引用。`cancel_read_only_call`、完成或执行错误会清理帧根并退出只读作用域,错误保留 traceback。取消只终止只读执行,不是通用事务回滚。跨线程任务调度、GUI 帧发布及响应新输入由宿主负责;本接口不实现调度器。参见 execution.rs 的分片/取消/累计限额/GC 根回归(未本地执行)。
运行错误仍返回 `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 保持借用式读取,类型方法结果为未绑定函数。所有委托消耗同一指令预算。
## 独立托管调用帧
`Vm::new_coroutine(function, arguments, limits)` 返回拥有一份外部根的通用 coroutine 对象;`resume_coroutine(handle, slice)` 返回 `CoroutineProgress::Pending` 或带拥有式结果的 `Complete`。源码指令首次在 resume 执行,原生函数/委托可能在 new 的准备期间执行,仍受硬指令限额约束。该接口目前只提供分片执行基础,不等于 Endor async/await 语法或等待协议。
与只读续算不同,每个 coroutine 把自己的帧保存在同一 VM 的托管对象载荷中,多个调用可独立暂停;Pending 之间可执行普通调用和堆写入,没有跨量子的只读作用域。resume 不重放先前指令,局部槽、栈、返回类型及原生委托契约保持,所有量子共用原始硬指令/帧预算。原生调用仍是原子的,slice 不保证墙钟延迟。读写共享堆,无快照、事务或自动回滚。不能在只读执行/只读暂停期间创建、恢复或取消 coroutine。
暂停时,帧内原本的执行根转为载荷 trace 的托管边(包括重复边);恢复前先保活为执行根,再清空暂停载荷。函数边继续拥有 code/globals/builtins。运行中的 handle 临时保活,不能恢复或取消 Running/已结束帧;完成和错误均消耗帧,错误保留 traceback。`cancel_coroutine(handle)` 对尚未结束帧返回 true 并释放托管边,已结束返回 false;已发生的写入不回滚。调用方最终 release handle 和完成结果。丢弃未完成且不可达的 coroutine 不要求先 cancel,局部/全局指回 coroutine 的环可由正常循环回收处理,不把暂停帧永久登记为 VM 根。调度、任务归属、等待条件及取消策略由宿主实现,网络/GUI不进入内核。
## Await 等待边界
`Await`(opcode 30,无参数)把栈顶 awaitable 换成结果,验证栈效果为1入1出。`ObjectPayload::poll_await()` 是非阻塞、无副作用的宿主协议,默认不支持;返回 Pending 或 Ready(借用 Value),也可返回 AwaitFailed(String)/AwaitCancelled。引擎验证 Ready 句柄并先retain结果,再释放原操作数;不可信宿主仍需遵守借用与非阻塞契约。Await 不被保守效果分析证明为只读,运行时也拒绝只读阶段。
在独立coroutine中,Pending保持已求值的栈顶、当前帧和返回契约,返回CoroutineProgress::Waiting { awaitable, instructions };awaitable是由暂停帧保活的借用身份。宿主等待条件满足后再次resume,仅重查同一个Await,不重放操作数表达式、前缀写入或IO请求。每次实际执行Await计入同一硬预算;宿主不应对未就绪对象反复resume来轮询。完成结果与错误沿普通执行/清理路径传播。非coroutine调用允许立即Ready的await,Pending则明确报错,不阻塞线程。调度、取消传播和循环等待检测属于宿主;本引擎不依赖Tessera Task。
LoadGlobal按文本名字分别观察函数globals及缺失时的captured builtins,不因无关全局写入而失效。可恢复只读Complete新增dependencies,reads保留兼容保守列表;精确验证/所有权见native-calls.md。