feat: retain read-only execution frames across instruction slices

This commit is contained in:
2026-10-23 00:59:23 +08:00
parent e511e393d9
commit 1c729b4128
9 changed files with 298 additions and 10 deletions
+3 -3
View File
@@ -5,9 +5,9 @@
| 文件 | 职责 |
| --- | --- |
| [lib.rs](lib.rs) | VM 类型导出、objects 类型兼容重导出(含 ReadAccess/CallSignature/CallType/CallEffect)、宿主调用与字节码 rustdoc 示例 |
| [vm.rs](vm.rs) | Vm 拥有 ObjectStore 与错误位置记录;对象操作和宿主 new_class 和显式/类型化Trait注册/关系查询显式委托给 objects;函数/字段检查统一开关、统一参数区间/已知调用效果查询与函数类型配置 |
| [frame.rs](frame.rs) | EndorFrame、栈条目、源码与原生委托返回契约执行根、执行位置元数据、ExecutionLimits |
| [eval.rs](eval.rs) | execute/call、只读调用与依赖结果、参数绑定、原生实现前数量/类型/效果检查和原生/类型化Trait委托返回检查、可选注解/动态覆写兼容检查、源码类字段环境绑定、帧切换、取指分派、MakeFunction、结果根转移与失败清理 |
| [vm.rs](vm.rs) | Vm 拥有 ObjectStore、错误位置记录与挂起的只读执行状态;对象操作和宿主 new_class 和显式/类型化Trait注册/关系查询显式委托给 objects;函数/字段检查统一开关、统一参数区间/已知调用效果查询与函数类型配置 |
| [frame.rs](frame.rs) | EndorFrame、栈条目、源码与原生委托返回契约执行根、执行位置元数据、ExecutionLimits、ExecutionProgress 与 VM 内续算帧状态 |
| [eval.rs](eval.rs) | execute/call、只读调用/可恢复分片/取消与依赖结果、参数绑定、原生实现前数量/类型/效果检查和原生/类型化Trait委托返回检查、可选注解/动态覆写兼容检查、源码类字段环境绑定、帧切换、取指分派、MakeFunction、结果根转移与失败清理 |
## 常见修改入口
+147 -4
View File
@@ -1,4 +1,5 @@
use crate::frame::{NativeReturn, StackEntry};
use crate::ExecutionProgress;
use crate::frame::{NativeReturn, StackEntry, SuspendedCall};
use crate::{
CompareOp, EndorCellObject, EndorCodeObject, EndorFrame, EndorFunctionObject,
EndorStringObject, ExecutionLimits, ExecutionLocation, Instruction, ObjectRef, Opcode, Value,
@@ -69,6 +70,11 @@ impl Vm {
arguments: &[Value],
limits: ExecutionLimits,
) -> Result<Value, VmError> {
if self.suspended_call.is_some() {
return Err(VmError::UnsupportedOperation(
"call while read-only execution is suspended",
));
}
self.last_traceback.clear();
let mut executed = 0;
let frame = match self.prepare_call(function, arguments.to_vec(), &mut executed, limits)? {
@@ -80,7 +86,9 @@ impl Vm {
return Err(VmError::ExecutionLimit("frames"));
}
let mut frames = vec![frame];
let result = self.eval_frames(&mut frames, limits, &mut executed);
let result = self
.eval_frames(&mut frames, limits, &mut executed, usize::MAX)
.map(|value| value.expect("unbounded slice completes or fails"));
if result.is_err() {
self.last_traceback = frames
.iter()
@@ -98,6 +106,137 @@ impl Vm {
result
}
/// Begin a resumable read-only call. One VM owns at most one continuation.
/// A slice is a soft instruction quantum: native callbacks and delegation
/// complete atomically and remain subject to the cumulative hard limits.
pub fn start_read_only_call(
&mut self,
function: ObjectRef,
arguments: &[Value],
limits: ExecutionLimits,
slice: usize,
) -> Result<ExecutionProgress, VmError> {
if slice == 0 {
return Err(VmError::InvalidArgument("execution slice must be positive"));
}
if self.suspended_call.is_some() || self.objects.is_read_only() {
return Err(VmError::UnsupportedOperation(
"start suspended call inside another execution",
));
}
self.last_traceback.clear();
let scope = self.objects.begin_read_only();
let mut executed = 0;
let prepared = self.prepare_call(function, arguments.to_vec(), &mut executed, limits);
match prepared {
Ok(PreparedCall::Value(value)) => {
let reads = match self.objects.end_read_only(scope) {
Ok(reads) => reads,
Err(error) => {
self.drop_root(value);
return Err(error);
}
};
Ok(ExecutionProgress::Complete {
value,
reads,
instructions: executed,
})
}
Ok(PreparedCall::Frame(frame)) => {
if limits.max_frames == 0 {
self.clear_frame(frame);
self.objects.end_read_only(scope)?;
return Err(VmError::ExecutionLimit("frames"));
}
self.suspended_call = Some(SuspendedCall {
frames: vec![frame],
scope,
limits,
executed,
});
// Preparation counts toward the first quantum as well.
self.resume_read_only_to(slice)
}
Err(error) => {
self.objects.end_read_only(scope)?;
Err(error)
}
}
}
pub fn has_suspended_call(&self) -> bool {
self.suspended_call.is_some()
}
/// Resume existing frames, never replay earlier instructions. The read-only
/// scope remains active across Pending; preexisting heap writes stay barred.
pub fn resume_read_only_call(&mut self, slice: usize) -> Result<ExecutionProgress, VmError> {
if slice == 0 {
return Err(VmError::InvalidArgument("execution slice must be positive"));
}
let executed = self
.suspended_call
.as_ref()
.ok_or(VmError::InvalidArgument("no suspended call"))?
.executed;
self.resume_read_only_to(executed.saturating_add(slice))
}
fn resume_read_only_to(&mut self, slice_end: usize) -> Result<ExecutionProgress, VmError> {
let mut call = self
.suspended_call
.take()
.ok_or(VmError::InvalidArgument("no suspended call"))?;
let result = self.eval_frames(&mut call.frames, call.limits, &mut call.executed, slice_end);
if let Ok(None) = result {
let instructions = call.executed;
self.suspended_call = Some(call);
return Ok(ExecutionProgress::Pending { instructions });
}
if result.is_err() {
self.last_traceback = call
.frames
.iter()
.map(|frame| ExecutionLocation {
function: frame.name.clone(),
filename: frame.executable.filename().into(),
first_line: frame.executable.first_line(),
instruction_offset: frame.current_offset,
})
.collect();
}
while let Some(frame) = call.frames.pop() {
self.clear_frame(frame);
}
let reads = self.objects.end_read_only(call.scope);
match (result, reads) {
(Ok(Some(value)), Ok(reads)) => Ok(ExecutionProgress::Complete {
value,
reads,
instructions: call.executed,
}),
(Ok(Some(value)), Err(error)) => {
self.drop_root(value);
Err(error)
}
(Err(error), _) => Err(error),
(Ok(None), _) => unreachable!(),
}
}
/// Release all retained execution roots and close the read-only scope.
pub fn cancel_read_only_call(&mut self) -> Result<bool, VmError> {
let Some(mut call) = self.suspended_call.take() else {
return Ok(false);
};
while let Some(frame) = call.frames.pop() {
self.clear_frame(frame);
}
self.objects.end_read_only(call.scope)?;
Ok(true)
}
/// Outer-to-inner execution locations from the last failed call/execute.
/// These are copied metadata, not heap references or language exceptions.
pub fn last_traceback(&self) -> &[ExecutionLocation] {
@@ -613,11 +752,15 @@ impl Vm {
frames: &mut Vec<EndorFrame>,
limits: ExecutionLimits,
executed: &mut usize,
) -> Result<Value, VmError> {
slice_end: usize,
) -> Result<Option<Value>, VmError> {
loop {
if *executed >= limits.max_instructions {
return Err(VmError::ExecutionLimit("instructions"));
}
if *executed >= slice_end {
return Ok(None);
}
*executed += 1;
let frame = frames.last_mut().expect("an evaluation frame is active");
let decoded = frame.executable.instruction_at(frame.instruction_offset)?;
@@ -782,7 +925,7 @@ impl Vm {
if let Some(caller) = frames.last_mut() {
caller.stack.push(StackEntry::Value(result));
} else {
return Ok(result);
return Ok(Some(result));
}
}
Opcode::JumpForward | Opcode::JumpBackward => {
+22
View File
@@ -91,3 +91,25 @@ impl Default for ExecutionLimits {
}
}
}
/// A read-only call either retains its VM-owned continuation or returns an
/// owned value plus borrowed read dependencies. Instruction counts accumulate.
#[derive(Debug)]
pub enum ExecutionProgress {
Pending {
instructions: usize,
},
Complete {
value: Value,
reads: Vec<ObjectRef>,
instructions: usize,
},
}
#[derive(Debug)]
pub(crate) struct SuspendedCall {
pub frames: Vec<EndorFrame>,
pub scope: usize,
pub limits: ExecutionLimits,
pub executed: usize,
}
+1 -1
View File
@@ -42,7 +42,7 @@ mod eval;
mod frame;
mod vm;
pub use frame::{EndorFrame, ExecutionLimits, ExecutionLocation};
pub use frame::{EndorFrame, ExecutionLimits, ExecutionLocation, ExecutionProgress};
// Preserve existing embedding imports; objects owns these definitions.
pub use objects::ObjectStore;
pub use objects::{
+2
View File
@@ -12,6 +12,7 @@ pub struct Vm {
pub(crate) objects: ObjectStore,
pub(crate) last_traceback: Vec<ExecutionLocation>,
pub(crate) source_type_checks: bool,
pub(crate) suspended_call: Option<crate::frame::SuspendedCall>,
}
impl Vm {
@@ -166,6 +167,7 @@ impl Vm {
objects: ObjectStore::new()?,
last_traceback: Vec::new(),
source_type_checks: false,
suspended_call: None,
})
}
+1 -1
View File
@@ -6,7 +6,7 @@
| --- | --- |
| [runtime.rs](runtime.rs) | 空堆启动、默认操作、引用/借用、属性原子性、稳定地址、长链/循环、原生回调、VM 销毁 |
| [builtins.rs](builtins.rs) | 无损 Number、Unicode 字符串、Dict 顺序与键、Cell、函数环境/共享代码、异常数据 |
| [execution.rs](execution.rs) | wordcode 与校验、控制流、参数/默认值、嵌套帧、字节码函数创建/默认值/环境捕获、闭包、返回值存活、失败清理、限额 |
| [execution.rs](execution.rs) | wordcode 与校验、控制流、参数/默认值、嵌套帧、字节码函数创建/默认值/环境捕获、闭包、返回值存活、失败清理、限额、只读分片/取消/累计预算/暂停期间GC执行根 |
| [types.rs](types.rs) | 名义身份、继承与实例关系、类型边保活/循环回收、只读类型元数据与外来句柄、显式Trait身份/继承/参数约束及后代更新原子性;统一函数分类、原生数量元数据/副作用前拒绝/能力更新/默认参数与返回类型错误根清理;仅远端执行 |
| [typed_traits.rs](typed_traits.rs) | 类型化Trait登记、原子拒绝、源码检查关闭时的显式约束、宽参数调用、类型强边GC、体后实际能力结果检查与临时验证集合恢复;仅远端执行 |
| [method_contracts.rs](method_contracts.rs) | 源码/原生方法覆写参数逆变/返回协变、联合参数、已证明源码/调用图只读、全局/闭包/接收者重绑定、控制流汇合与原生效果及共享赋值保守分类、祖先替换原子性、动态名称重绑定/前向名称与体前拒绝/恢复及清理;仅远端执行 |
+117
View File
@@ -1386,3 +1386,120 @@ fn later_execution_failure_releases_a_new_function_and_its_temporary_default() {
vm.release(globals).unwrap();
assert_eq!(vm.object_count(), 0);
}
#[test]
fn suspended_read_only_call_keeps_roots_and_releases_them_on_completion() {
use endor_core::ExecutionProgress;
let mut vm = Vm::new().unwrap();
let shared = vm.new_object().unwrap();
vm.set_attr(shared, "value", Value::Number(7_i64.into()))
.unwrap();
let globals = vm.new_dict().unwrap();
let mut instructions = vec![simple(Opcode::Nop); 16];
instructions.extend([op(Opcode::LoadConst, 0), simple(Opcode::ReturnValue)]);
let code = make_code(
&mut vm,
&instructions,
vec![Value::Object(shared)],
CodeLayout::default(),
);
let function = vm
.new_function("sliced", code, globals, vec![], vec![])
.unwrap();
let mut progress = vm
.start_read_only_call(
function,
&[],
ExecutionLimits {
max_frames: 8,
max_instructions: 100,
},
1,
)
.unwrap();
assert!(matches!(
progress,
ExecutionProgress::Pending { instructions: 1 }
));
assert!(
vm.set_attr(shared, "value", Value::Number(8_i64.into()))
.is_err()
);
assert!(vm.call(function, &[]).is_err());
vm.release(function).unwrap();
vm.release(code).unwrap();
vm.release(globals).unwrap();
vm.release(shared).unwrap();
vm.collect_cycles();
loop {
match progress {
ExecutionProgress::Pending { .. } => progress = vm.resume_read_only_call(2).unwrap(),
ExecutionProgress::Complete {
value,
instructions,
..
} => {
assert_eq!(instructions, 18);
assert_eq!(value, Value::Object(shared));
assert_eq!(
vm.get_attr(shared, "value").unwrap(),
Value::Number(7_i64.into())
);
vm.release_value(value).unwrap();
break;
}
}
}
assert!(!vm.has_suspended_call());
assert_eq!(vm.object_count(), 0);
}
#[test]
fn suspended_call_cancellation_and_total_budget_restore_read_only_state() {
let mut vm = Vm::new().unwrap();
let shared = vm.new_object().unwrap();
let globals = vm.new_dict().unwrap();
let mut instructions = vec![simple(Opcode::Nop); 8];
instructions.extend([op(Opcode::LoadConst, 0), simple(Opcode::ReturnValue)]);
let code = make_code(
&mut vm,
&instructions,
vec![Value::Object(shared)],
CodeLayout::default(),
);
let function = vm
.new_function("budget", code, globals, vec![], vec![])
.unwrap();
let roots = vm.ref_count(function).unwrap();
vm.start_read_only_call(
function,
&[],
ExecutionLimits {
max_frames: 8,
max_instructions: 3,
},
1,
)
.unwrap();
assert!(vm.resume_read_only_call(0).is_err());
assert!(vm.has_suspended_call());
vm.resume_read_only_call(1).unwrap();
assert_eq!(
vm.resume_read_only_call(1).unwrap_err(),
VmError::ExecutionLimit("instructions")
);
assert!(!vm.has_suspended_call());
assert_eq!(vm.ref_count(function).unwrap(), roots);
vm.set_attr(shared, "after", Value::Bool(true)).unwrap();
vm.start_read_only_call(function, &[], ExecutionLimits::default(), 1)
.unwrap();
assert!(vm.cancel_read_only_call().unwrap());
assert!(!vm.cancel_read_only_call().unwrap());
assert_eq!(vm.ref_count(function).unwrap(), roots);
vm.set_attr(shared, "after", Value::Bool(false)).unwrap();
vm.release(function).unwrap();
vm.release(code).unwrap();
vm.release(globals).unwrap();
vm.release(shared).unwrap();
assert_eq!(vm.object_count(), 0);
}
+1 -1
View File
@@ -12,7 +12,7 @@
| 词法、语法、裸赋值和诊断 | [Frontend](runtime/frontend.md) | frontend/src/lexer.peg/grammar.peg |
| ASDL、AST 与编辑器接口 | [AST](runtime/ast.md) | frontend/Endor.asdl |
| 名称分析、编译和装载 | [Compiler](runtime/compiler.md) | compiler/src |
| 字节码、函数、共享引用赋值与限额 | [字节码 VM](runtime/bytecode.md) | objects/src、core/src |
| 字节码、函数、共享引用赋值、可恢复只读分片与限额 | [字节码 VM](runtime/bytecode.md) | objects/src、core/src |
| 引用与 VM 生命周期 | [生命周期](runtime/lifecycle.md) | objects/src、core/src |
| UTF-8 与地区 | [文本编码](runtime/text-encoding.md) | frontend、objects |
| 嵌入及宿主入口 | [使用引擎](operations/README.md) | 独立宿主仓库 |
+4
View File
@@ -158,6 +158,10 @@ fn main() -> Result<(), endor_core::VmError> {
`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。