diff --git a/crates/core/src/README.md b/crates/core/src/README.md index cd94d7b..a668eb5 100644 --- a/crates/core/src/README.md +++ b/crates/core/src/README.md @@ -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、结果根转移与失败清理 | ## 常见修改入口 diff --git a/crates/core/src/eval.rs b/crates/core/src/eval.rs index ae12e27..e6509af 100644 --- a/crates/core/src/eval.rs +++ b/crates/core/src/eval.rs @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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, limits: ExecutionLimits, executed: &mut usize, - ) -> Result { + slice_end: usize, + ) -> Result, 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 => { diff --git a/crates/core/src/frame.rs b/crates/core/src/frame.rs index 6130e9a..8076242 100644 --- a/crates/core/src/frame.rs +++ b/crates/core/src/frame.rs @@ -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, + instructions: usize, + }, +} + +#[derive(Debug)] +pub(crate) struct SuspendedCall { + pub frames: Vec, + pub scope: usize, + pub limits: ExecutionLimits, + pub executed: usize, +} diff --git a/crates/core/src/lib.rs b/crates/core/src/lib.rs index 4cd5274..ca872db 100644 --- a/crates/core/src/lib.rs +++ b/crates/core/src/lib.rs @@ -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::{ diff --git a/crates/core/src/vm.rs b/crates/core/src/vm.rs index db76a42..6f67be4 100644 --- a/crates/core/src/vm.rs +++ b/crates/core/src/vm.rs @@ -12,6 +12,7 @@ pub struct Vm { pub(crate) objects: ObjectStore, pub(crate) last_traceback: Vec, pub(crate) source_type_checks: bool, + pub(crate) suspended_call: Option, } impl Vm { @@ -166,6 +167,7 @@ impl Vm { objects: ObjectStore::new()?, last_traceback: Vec::new(), source_type_checks: false, + suspended_call: None, }) } diff --git a/crates/core/tests/README.md b/crates/core/tests/README.md index 05b5a09..10d20be 100644 --- a/crates/core/tests/README.md +++ b/crates/core/tests/README.md @@ -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) | 源码/原生方法覆写参数逆变/返回协变、联合参数、已证明源码/调用图只读、全局/闭包/接收者重绑定、控制流汇合与原生效果及共享赋值保守分类、祖先替换原子性、动态名称重绑定/前向名称与体前拒绝/恢复及清理;仅远端执行 | diff --git a/crates/core/tests/execution.rs b/crates/core/tests/execution.rs index b441461..a647960 100644 --- a/crates/core/tests/execution.rs +++ b/crates/core/tests/execution.rs @@ -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); +} diff --git a/docs/README.md b/docs/README.md index 734ef25..c80a0f6 100644 --- a/docs/README.md +++ b/docs/README.md @@ -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) | 独立宿主仓库 | diff --git a/docs/runtime/bytecode.md b/docs/runtime/bytecode.md index ef85929..10b3788 100644 --- a/docs/runtime/bytecode.md +++ b/docs/runtime/bytecode.md @@ -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。