diff --git a/Cargo.lock b/Cargo.lock index 753151e1..4b470b46 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1095,7 +1095,7 @@ dependencies = [ [[package]] name = "oxide_builtins" -version = "0.8.27" +version = "0.8.28" dependencies = [ "chrono", "fast-float", @@ -1120,7 +1120,7 @@ dependencies = [ [[package]] name = "oxide_bytecode" -version = "0.8.27" +version = "0.8.28" dependencies = [ "num-bigint", "oxide_log", @@ -1128,7 +1128,7 @@ dependencies = [ [[package]] name = "oxide_cfg" -version = "0.8.27" +version = "0.8.28" dependencies = [ "oxide_bytecode", "oxide_emit", @@ -1139,7 +1139,7 @@ dependencies = [ [[package]] name = "oxide_cli" -version = "0.8.27" +version = "0.8.28" dependencies = [ "ansi_term", "bumpalo", @@ -1159,7 +1159,7 @@ dependencies = [ [[package]] name = "oxide_code_cache" -version = "0.8.27" +version = "0.8.28" dependencies = [ "lru", "oxide_bytecode", @@ -1168,7 +1168,7 @@ dependencies = [ [[package]] name = "oxide_compiler" -version = "0.8.27" +version = "0.8.28" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1187,7 +1187,7 @@ dependencies = [ [[package]] name = "oxide_dce" -version = "0.8.27" +version = "0.8.28" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1201,7 +1201,7 @@ dependencies = [ [[package]] name = "oxide_emit" -version = "0.8.27" +version = "0.8.28" dependencies = [ "num-bigint", "oxc_allocator", @@ -1216,7 +1216,7 @@ dependencies = [ [[package]] name = "oxide_ir" -version = "0.8.27" +version = "0.8.28" dependencies = [ "oxide_bytecode", "oxide_log", @@ -1225,7 +1225,7 @@ dependencies = [ [[package]] name = "oxide_kernel" -version = "0.8.27" +version = "0.8.28" dependencies = [ "bumpalo", "dashmap", @@ -1240,7 +1240,7 @@ dependencies = [ [[package]] name = "oxide_liveness" -version = "0.8.27" +version = "0.8.28" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1252,7 +1252,7 @@ dependencies = [ [[package]] name = "oxide_log" -version = "0.8.27" +version = "0.8.28" dependencies = [ "tracing", "tracing-appender", @@ -1261,7 +1261,7 @@ dependencies = [ [[package]] name = "oxide_parser" -version = "0.8.27" +version = "0.8.28" dependencies = [ "oxc_allocator", "oxc_ast", @@ -1276,7 +1276,7 @@ dependencies = [ [[package]] name = "oxide_regalloc" -version = "0.8.27" +version = "0.8.28" dependencies = [ "oxide_bytecode", "oxide_cfg", @@ -1292,7 +1292,7 @@ dependencies = [ [[package]] name = "oxide_runtime_api" -version = "0.8.27" +version = "0.8.28" dependencies = [ "num-bigint", "num-traits", @@ -1304,7 +1304,7 @@ dependencies = [ [[package]] name = "oxide_test262" -version = "0.8.27" +version = "0.8.28" dependencies = [ "oxide_bytecode", "oxide_compiler", @@ -1321,7 +1321,7 @@ dependencies = [ [[package]] name = "oxide_types" -version = "0.8.27" +version = "0.8.28" dependencies = [ "bumpalo", "hashbrown 0.14.5", @@ -1333,7 +1333,7 @@ dependencies = [ [[package]] name = "oxide_vm" -version = "0.8.27" +version = "0.8.28" dependencies = [ "bumpalo", "chrono", diff --git a/Cargo.toml b/Cargo.toml index 88aa0f99..6c7d8d6f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,7 @@ members = [ ] [workspace.package] -version = "0.8.27" +version = "0.8.28" edition = "2021" license = "MIT" rust-version = "1.80" diff --git a/crates/oxide_cli/src/server/eval.rs b/crates/oxide_cli/src/server/eval.rs new file mode 100644 index 00000000..ca5aa31c --- /dev/null +++ b/crates/oxide_cli/src/server/eval.rs @@ -0,0 +1,181 @@ +//! 执行请求路径:parse → compile → spawn → run → format → drop。 +//! +//! `handle_eval` 是纯函数,参数全注入、无全局状态;每请求用单一 +//! `catch_unwind` 包裹整条路径,panic 载荷转成错误帧,不 panic、不退出。 +//! +//! # release 边界 +//! 根 `Cargo.toml` 的 `[profile.release]` 为 `panic=abort`:release 下 +//! `catch_unwind` 是空操作、panic 直接终止进程,watchdog 是 release 崩溃后 +//! 恢复的唯一自动手段;debug 下本路径完整生效,是日常验证环境。 + +use std::sync::Arc; + +use oxide_compiler::compiler::{compiled_module_hash, Compiler}; +use oxide_kernel::kernel::KernelCore; +use oxide_parser::Allocator; +use oxide_vm::vm_pool::VmPool; + +use super::protocol::ServerResponse; + +/// 执行请求处理:解析 → 编译 → 取 VM → 执行 → 渲染完成值 → 归还池的六段路径。 +/// +/// # 步骤 +/// 1. parse:局部分配器加 `oxide_parser::parse`,失败时各条错误换行拼接成错误帧。 +/// 2. compile:`Compiler` 加 `CodeForge` 结构哈希缓存,失败以原文成错误帧。 +/// 3. spawn:从池取 `VmGuard`(闭包内创建,panic 时随展开被 drop)。 +/// 4. run:设每请求步数覆盖(含 None)后 `run`,`Err` 即错误帧。 +/// 5. format:成功值经 `crate::format_js_value` 渲染成完成值文本,`eval_ok` 封装。 +/// 6. drop:guard 在闭包出口 drop,干净 VM 经 `full_reset` 回池复用、被污染 VM 丢弃并新建替补。 +/// +/// # 边界与前提 +/// - guard 在 `catch_unwind` 闭包内创建:panic 时 guard 随展开在闭包边界被 drop, +/// `VmGuard::drop` 的 `thread::panicking()` 检查此刻为真,VM 按 dirty 丢弃。 +/// +/// # 副作用 +/// - 无(纯函数,参数全注入)。 +/// +/// # 注意事项 +/// - release 下 `panic=abort` 使 `catch_unwind` 空操作,watchdog 是崩溃后恢复的唯一自动手段。 +pub fn handle_eval(code: &str, max_steps: Option, kernel: &Arc, pool: &Arc) -> ServerResponse { + let response = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + // parse:局部分配器,解析失败各条换行拼接成错误帧。 + let allocator = Allocator::default(); + let program = match oxide_parser::parse(&allocator, code) { + Ok(program) => program, + Err(errors) => { + let message = errors.iter().map(|e| e.to_string()).collect::>().join("\n"); + return ServerResponse::eval_err(message); + } + }; + + // compile:结构哈希缓存,失败以原文成错误帧。 + let compiler = Compiler::new(); + let hash = compiled_module_hash(&program); + let module = match kernel.code_forge().get_or_insert_with(hash, || compiler.compile(&program)) { + Ok(module) => module, + Err(message) => return ServerResponse::eval_err(message), + }; + + // spawn:从池取 VM;guard 在闭包内创建,panic 时随展开被 drop。 + let mut guard = pool.spawn(); + + // run:设每请求步数覆盖(含 None)后执行。 + guard.vm_mut().set_max_steps(max_steps); + let result = guard.vm_mut().run(&module); + + // format:渲染完成值,`Err` 即错误帧。 + match result { + Ok(value) => { + let text = crate::format_js_value( + guard.vm(), + kernel.perm_interner().as_ref(), + kernel.shape_forge().as_ref(), + value, + ); + ServerResponse::eval_ok(text) + } + Err(message) => ServerResponse::eval_err(message), + } + // drop:guard 在闭包出口 drop,干净 VM 经 full_reset 回池复用、dirty 丢弃。 + })); + + match response { + Ok(resp) => resp, + Err(payload) => ServerResponse::eval_err(format!("internal panic: {}", panic_payload_str(&payload))), + } +} + +/// 从 `catch_unwind` 的 panic payload 提取可读文本:`&str` 与 `String` 两种 +/// 常见形态,其余返回占位文本。 +fn panic_payload_str(payload: &Box) -> String { + if let Some(s) = payload.downcast_ref::<&str>() { + return (*s).to_string(); + } + if let Some(s) = payload.downcast_ref::() { + return s.clone(); + } + "non-string payload".into() +} + +#[cfg(test)] +mod tests { + use super::*; + use oxide_kernel::kernel::KernelConfig; + + /// 测试环境:minimal 内核加单 VM 池(与池既有测试同口径)。 + fn test_env() -> (Arc, Arc) { + let kernel = KernelCore::new(KernelConfig::minimal()); + let pool = VmPool::new(Arc::clone(&kernel), 1, Some(2)); + (kernel, pool) + } + + /// 成功渲染:"1 + 1" 得完成值 "2",无错误。 + #[test] + fn eval_success_renders_value() { + let (kernel, pool) = test_env(); + let response = handle_eval("1 + 1", None, &kernel, &pool); + match response { + ServerResponse::EvalResult { ref value, ref error } => { + assert_eq!(value.as_deref(), Some("2"), "完成值应为 2:{response:?}"); + assert!(error.is_none(), "不应有错误:{response:?}"); + } + other => panic!("应得 EvalResult 帧,实得 {other:?}"), + } + } + + /// 解析错误帧:"function(" 得非空错误,无完成值。 + #[test] + fn eval_parse_error_frame() { + let (kernel, pool) = test_env(); + let response = handle_eval("function(", None, &kernel, &pool); + match response { + ServerResponse::EvalResult { ref value, ref error } => { + assert!(value.is_none(), "不应有完成值:{response:?}"); + assert!(error.as_deref().map(|e| !e.is_empty()).unwrap_or(false), "错误应非空:{response:?}"); + } + other => panic!("应得 EvalResult 帧,实得 {other:?}"), + } + } + + /// 运行时错误帧:throw 得含 uncaught 与 boom 的错误。 + #[test] + fn eval_runtime_error_frame() { + let (kernel, pool) = test_env(); + let response = handle_eval("throw new Error('boom')", None, &kernel, &pool); + match response { + ServerResponse::EvalResult { ref value, ref error } => { + assert!(value.is_none(), "不应有完成值:{response:?}"); + let err = error.as_deref().expect("应有错误"); + assert!(err.contains("uncaught"), "错误应含 uncaught:{err}"); + assert!(err.contains("boom"), "错误应含 boom:{err}"); + } + other => panic!("应得 EvalResult 帧,实得 {other:?}"), + } + } + + /// 步数超限帧:for(;;){} 加 Some(1000) 得含 step limit 的错误。 + #[test] + fn eval_step_limit_frame() { + let (kernel, pool) = test_env(); + let response = handle_eval("for(;;){}", Some(1000), &kernel, &pool); + match response { + ServerResponse::EvalResult { ref value, ref error } => { + assert!(value.is_none(), "不应有完成值:{response:?}"); + let err = error.as_deref().expect("应有错误"); + assert!(err.contains("step limit"), "错误应含 step limit:{err}"); + } + other => panic!("应得 EvalResult 帧,实得 {other:?}"), + } + } + + /// 池复用:连续两次成功 eval 后池可用数不变。 + #[test] + fn eval_pool_reuse() { + let (kernel, pool) = test_env(); + let before = pool.available_count(); + handle_eval("1 + 1", None, &kernel, &pool); + handle_eval("2 + 2", None, &kernel, &pool); + let after = pool.available_count(); + assert_eq!(before, after, "池可用数应不变:before={before} after={after}"); + } +} diff --git a/crates/oxide_cli/src/server/mod.rs b/crates/oxide_cli/src/server/mod.rs index ae5018d4..0d670257 100644 --- a/crates/oxide_cli/src/server/mod.rs +++ b/crates/oxide_cli/src/server/mod.rs @@ -1,11 +1,14 @@ -//! 持久 server 子系统:协议数据面、身份注册与 server 进程主体。 +//! 持久 server 子系统:协议数据面、身份注册、执行路径与 server 进程主体。 //! //! 持久 server 是常驻进程,复用预热池、近零 spawn 成本; //! 通信走 Unix socket + NDJSON(换行分隔 JSON)。 -//! 模块分三层:协议数据面(帧格式与 serde 结构体)、身份注册(sidecar 文件 -//! 与 liveness 检查原语)、server 进程主体(accept 循环、控制请求直接处理、 -//! 优雅退出)。执行路径与并发模型在后续任务。 +//! 模块分四层:协议数据面(帧格式与 serde 结构体)、身份注册(sidecar 文件 +//! 与 liveness 检查原语)、执行请求路径(parse → compile → spawn → run → +//! format → drop 六段纯函数)、server 进程主体(accept 循环、控制请求直接 +//! 处理、优雅退出)。并发模型在后续任务。 +/// 执行请求路径:parse → compile → spawn → run → format → drop 六段纯函数。 +pub mod eval; /// 协议数据面:NDJSON 帧格式与 serde 结构体。 pub mod protocol; /// server 进程主体:accept 循环、控制请求分派与优雅退出。 diff --git a/crates/oxide_cli/src/server/server.rs b/crates/oxide_cli/src/server/server.rs index 3fc6abba..92accf2c 100644 --- a/crates/oxide_cli/src/server/server.rs +++ b/crates/oxide_cli/src/server/server.rs @@ -22,8 +22,9 @@ use std::sync::Arc; use std::time::{Duration, Instant}; use oxide_kernel::kernel::{KernelConfig, KernelCore}; -use oxide_vm::vm_pool::{PoolCounters, VmPool}; +use oxide_vm::vm_pool::VmPool; +use super::eval; use super::protocol::{self, FrameReader, ProtocolError, ServerRequest, ServerResponse}; use super::sidecar::{self, ClaimResult}; @@ -72,12 +73,12 @@ impl ServerConfig { } } -/// server 运行上下文:池状态、启动时刻、关闭标志、在途计数。 +/// server 运行上下文:共享内核、启动时刻、关闭标志、在途计数。 /// -/// 池本体不进上下文(Vm 不跨线程),只持池的原子状态句柄; -/// 内核不跨线程共享(执行路径由后续任务接线)。 +/// 内核跨线程共享(`Arc` 是 Send/Sync);池本体不进上下文 +/// (Vm 不跨线程),每连接线程自建池(见 `handle_connection`)。 struct ServerContext { - pool_counters: Arc, + kernel: Arc, started_at: Instant, shutdown: Arc, in_flight: Arc, @@ -114,12 +115,11 @@ pub fn run_server(config: &ServerConfig) -> Result<(), ServerError> { // 绑定监听器:stale socket 文件删一次重试,失败回滚 sidecar。 let listener = bind_listener(config)?; - // 建内核与池:标准配置,池按 min_pool_size 预热。 + // 建内核(标准配置);池每连接线程自建(Vm 不跨线程,池本体不共享)。 let kernel = KernelCore::new(KernelConfig::standard()); - let pool = VmPool::new(Arc::clone(&kernel), kernel.config.min_pool_size, kernel.config.max_pool_size); let ctx = Arc::new(ServerContext { - pool_counters: pool.counters(), + kernel: Arc::clone(&kernel), started_at: Instant::now(), shutdown: Arc::new(AtomicBool::new(false)), in_flight: Arc::new(AtomicUsize::new(0)), @@ -132,9 +132,8 @@ pub fn run_server(config: &ServerConfig) -> Result<(), ServerError> { // 在途归零:轮询至零,5 秒安全超时后记警告继续。 wait_in_flight_drained(&ctx.in_flight, Duration::from_secs(5)); - // 按序释放:上下文(持池状态句柄)→ 池 → 内核。 + // 按序释放:上下文(持内核句柄)→ 内核。 drop(ctx); - drop(pool); drop(kernel); // 删 sidecar 与 socket 文件(尽力而为,文件已不存在不视为错误)。 @@ -232,12 +231,15 @@ fn handle_connection(stream: UnixStream, ctx: &ServerContext) { // 30 秒读超时:半开连接不永久占住线程。 let _ = stream.set_read_timeout(Some(Duration::from_secs(30))); + // 每连接自建池(Vm 不跨线程,池是每线程资产),按内核配置预热。 + let pool = VmPool::new(Arc::clone(&ctx.kernel), ctx.kernel.config.min_pool_size, ctx.kernel.config.max_pool_size); + let mut reader = FrameReader::new(io::BufReader::new(stream)); // 读帧:EOF、读超时、非法 UTF-8 均退出。 while let Ok(Some(frame)) = reader.read_frame() { // 分派:帧长超限按协议约定关连接;畸形帧以 Error 帧答复后继续。 let response = match protocol::parse_request(&frame) { - Ok(request) => dispatch_control(&request, ctx), + Ok(request) => dispatch_control(&request, ctx, &pool), Err(ProtocolError::FrameTooLarge) => break, Err(error) => error.to_error_frame(), }; @@ -255,17 +257,17 @@ fn handle_connection(stream: UnixStream, ctx: &ServerContext) { } } -/// 控制请求分派:五类控制请求直接应答,不 spawn 虚拟机。 +/// 控制请求分派:五类控制请求直接应答;执行请求走 `handle_eval`。 /// -/// 执行请求暂以协议错误帧答复(执行路径由后续任务补齐)。 -fn dispatch_control(request: &ServerRequest, ctx: &ServerContext) -> ServerResponse { +/// 池是每连接资产(`handle_connection` 创建),Status 响应读本连接池状态。 +fn dispatch_control(request: &ServerRequest, ctx: &ServerContext, pool: &Arc) -> ServerResponse { match request { ServerRequest::Version => ServerResponse::Version { version: env!("CARGO_PKG_VERSION").to_string(), }, ServerRequest::Status => ServerResponse::Status { - pool_available: ctx.pool_counters.available(), - pool_total: ctx.pool_counters.total(), + pool_available: pool.available_count(), + pool_total: pool.total_count(), uptime_ms: ctx.started_at.elapsed().as_millis() as u64, }, ServerRequest::Health => ServerResponse::Health { healthy: true }, @@ -279,9 +281,7 @@ fn dispatch_control(request: &ServerRequest, ctx: &ServerContext) -> ServerRespo ctx.shutdown.store(true, Ordering::SeqCst); ServerResponse::Shutdown } - ServerRequest::Eval { .. } => ServerResponse::Error { - message: "执行路径尚未实现".to_string(), - }, + ServerRequest::Eval { code, max_steps } => eval::handle_eval(code, *max_steps, &ctx.kernel, pool), } } @@ -449,9 +449,9 @@ mod tests { stop_server(&mut stream, handle); } - /// 执行请求得 Error 帧(尚未实现),不 panic。 + /// 执行请求得 EvalResult 帧:"1 + 1" 得完成值 "2",不 panic。 #[test] - fn eval_not_implemented() { + fn eval_request_returns_value() { let config = unique_paths("eval"); let handle = start_server(config.clone()); let mut stream = connect(&config); @@ -462,7 +462,10 @@ mod tests { max_steps: None, }, ); - assert!(matches!(response, ServerResponse::Error { .. }), "应得 Error 帧:{response:?}"); + assert!( + matches!(response, ServerResponse::EvalResult { ref value, .. } if value.as_deref() == Some("2")), + "应得完成值为 2 的 EvalResult 帧:{response:?}" + ); stop_server(&mut stream, handle); } diff --git a/crates/oxide_vm/src/vm/dispatch_loop.rs b/crates/oxide_vm/src/vm/dispatch_loop.rs index 671c38c3..b3839ba2 100644 --- a/crates/oxide_vm/src/vm/dispatch_loop.rs +++ b/crates/oxide_vm/src/vm/dispatch_loop.rs @@ -92,7 +92,9 @@ impl Vm { pub(crate) fn dispatch(&mut self) -> Result { // config.max_steps / max_alloc_bytes 逐指令只读且循环内不变:提到循环外, // 免每次经 kernel_core Arc 指针追寻读取(热点内仅有的 config 访问)。 - let max_steps = self.kernel_core.config.max_steps; + // 步数上限取每请求覆盖值.or(内核配置):覆盖优先、None 回退内核配置, + // 未设覆盖的宿主(CLI/REPL/bench/test262)读到的值与改动前逐位一致。 + let max_steps = self.max_steps_override.or(self.kernel_core.config.max_steps); let max_alloc_bytes = self.kernel_core.config.max_alloc_bytes; let mut steps: u64 = 0; // 小重入泵送盲区:native 终端循环泵送短 JS 重入时,顶层 steps 不推进、 diff --git a/crates/oxide_vm/src/vm/mod.rs b/crates/oxide_vm/src/vm/mod.rs index 0aa1bf63..c6d97201 100644 --- a/crates/oxide_vm/src/vm/mod.rs +++ b/crates/oxide_vm/src/vm/mod.rs @@ -391,6 +391,10 @@ pub struct Vm { /// `pc/opcode/flat_id/frames`,供监督者超时/崩溃杀子进程后读回挂死点; /// 默认 None(零输出零分配)。 pub(crate) pc_watch: Option, + /// 每请求步数上限覆盖:执行路径按请求设置,None 回退内核配置。 + /// 池回收的 `full_reset` 路径清位;`run()` 入口的 `clear_execution_state` + /// 刻意不清(清在那会把执行路径刚设的覆盖抹掉)。 + pub(crate) max_steps_override: Option, } impl Drop for Vm { @@ -467,6 +471,13 @@ impl Vm { self.pc_watch = path; } + /// 设置每请求步数上限覆盖:dispatch 主循环取 `覆盖值.or(内核配置)`, + /// 覆盖优先、None 回退内核配置。执行路径按请求必设(含 None),池回收的 + /// `full_reset` 路径清位。 + pub fn set_max_steps(&mut self, limit: Option) { + self.max_steps_override = limit; + } + /// 设置指令周期采样周期(2 的幂,0 关闭)。开启后 dispatch 主循环每 /// `period` 条指令记一条样本(flat_id、pc、opcode、frames 深度),run 末 /// 按 flat_id 聚合并向 stderr 输出 top-K 直方图;关闭时热路径仅一次 diff --git a/crates/oxide_vm/src/vm_pool.rs b/crates/oxide_vm/src/vm_pool.rs index da815052..3e24e702 100644 --- a/crates/oxide_vm/src/vm_pool.rs +++ b/crates/oxide_vm/src/vm_pool.rs @@ -179,6 +179,12 @@ impl VmGuard { pub fn vm_mut(&mut self) -> &mut Vm { self.vm.as_mut().expect("VmGuard has no VM") } + + /// 显式标记被借出的 VM 为 dirty:`Drop` 时丢弃该 VM 并新建替补, + /// 不对其执行 `full_reset`。供并发与未来显式弃用场景使用。 + pub fn mark_dirty(&mut self) { + self.dirty = true; + } } impl Drop for VmGuard { @@ -189,7 +195,9 @@ impl Drop for VmGuard { let mut inner = self.pool.inner.lock().unwrap(); - if self.dirty { + // panic 展开期(thread::panicking 为真)被 drop 的 VM 一律按 dirty 丢弃: + // 损坏 VM 上执行 full_reset 可能二次 panic,unwind 中二次 panic 即 abort。 + if self.dirty || std::thread::panicking() { vm_debug!("pool: discarding dirty vm"); let new_vm = self.pool.replace_vm(); inner.available.push(new_vm); diff --git a/crates/oxide_vm/src/vm_support.rs b/crates/oxide_vm/src/vm_support.rs index b5f40c66..4193d729 100644 --- a/crates/oxide_vm/src/vm_support.rs +++ b/crates/oxide_vm/src/vm_support.rs @@ -166,6 +166,7 @@ impl Vm { saved_table_gen_stack: Vec::new(), trace_instructions: false, pc_watch: None, + max_steps_override: None, }; vm.init_generator_intrinsics(); vm.init_promise_intrinsics(); @@ -309,6 +310,7 @@ impl Vm { saved_table_gen_stack: Vec::new(), trace_instructions: false, pc_watch: None, + max_steps_override: None, }; vm.init_generator_intrinsics(); vm.init_promise_intrinsics(); @@ -429,6 +431,9 @@ impl Vm { self.symbols.reset(); self.root_reg_limit = 0; self.active_reg_limit = 0; + // 步数上限覆盖随池回收清位:执行路径按请求必设覆盖(含 None), + // 此处清位保证回收的 VM 不带旧覆盖回到默认态,循环 VM 回到内核配置。 + self.max_steps_override = None; } /// 释放全部 session 堆数据:session 对象(本体 + 堆数据 + upvalue 列表) diff --git a/crates/oxide_vm/src/vm_support/tests.rs b/crates/oxide_vm/src/vm_support/tests.rs index 88063cf1..3bff8e32 100644 --- a/crates/oxide_vm/src/vm_support/tests.rs +++ b/crates/oxide_vm/src/vm_support/tests.rs @@ -1114,3 +1114,57 @@ fn full_reset_clears_symbol_state() { assert!(vm.symbols.symbol_descriptions.is_empty()); assert!(vm.symbols.symbol_registry.is_empty()); } + +/// 编译源文本为模块(供步数上限测试直接 `run` 并检查 `Result`)。 +fn compile_module(source: &str) -> Arc { + let allocator = oxide_parser::Allocator::default(); + let program = oxide_parser::parse(&allocator, source).expect("parse failed"); + let module = oxide_compiler::compiler::Compiler::new() + .compile(&program) + .expect("compile failed"); + Arc::new(module) +} + +/// 步数覆盖超限:`set_max_steps(Some(100))` 加无限循环返回 `Err`,文本含 `step limit`。 +#[test] +fn max_steps_override_exceeded_returns_err() { + let mut vm = Vm::new(); + vm.set_max_steps(Some(100)); + let module = compile_module("for(;;){}"); + let result = vm.run(&module); + assert!(result.is_err(), "无限循环带步数覆盖应返回 Err:{result:?}"); + let err = result.unwrap_err(); + assert!(err.contains("step limit"), "错误文本应含 step limit:{err}"); +} + +/// 覆盖随 `full_reset` 清除:`Some(100)` 超限失败后 `full_reset`,再跑 50 轮循环 +/// (步数远超 100)返回 `Ok`,证明覆盖值未残留(回退内核配置无上限)。 +#[test] +fn full_reset_clears_max_steps_override() { + let mut vm = Vm::new(); + vm.set_max_steps(Some(100)); + let infinite = compile_module("for(;;){}"); + assert!(vm.run(&infinite).is_err(), "无限循环应超限"); + + vm.full_reset(); + + // 50 轮循环步数远超 100:覆盖清除后无上限,应返回 Ok。 + let moderate = compile_module("let n = 0; for (let i = 0; i < 50; i++) { n += i; } n"); + assert!(vm.run(&moderate).is_ok(), "full_reset 后 50 轮循环不应被旧覆盖限制"); +} + +/// `set_max_steps(None)` 回退内核配置:内核配置 `max_steps` 置 `Some(100)` 的独立 +/// kernel 上,覆盖置 None 后超限仍被限(`or` 回退语义)。 +#[test] +fn max_steps_none_falls_back_to_kernel_config() { + let mut config = KernelConfig::minimal(); + config.max_steps = Some(100); + let core = KernelCore::new(config); + let mut vm = Vm::with_kernel_core(Arc::clone(&core)); + vm.set_max_steps(None); + let module = compile_module("for(;;){}"); + let result = vm.run(&module); + assert!(result.is_err(), "内核配置限步下无限循环应返回 Err:{result:?}"); + let err = result.unwrap_err(); + assert!(err.contains("step limit"), "错误文本应含 step limit:{err}"); +}