From 2239eeac244856196a9a458ac340dd3228e3c4dd Mon Sep 17 00:00:00 2001 From: pjh456 <147148383@qq.com> Date: Sat, 15 Aug 2026 09:56:22 +0800 Subject: [PATCH] =?UTF-8?q?perf:=20=E5=AD=97=E8=8A=82=E7=A0=81=E8=B0=83?= =?UTF-8?q?=E7=94=A8=E5=8E=8B=E5=B8=A7=E7=AA=97=E5=8F=A3=E6=8C=89=E8=B0=83?= =?UTF-8?q?=E7=94=A8=E7=82=B9=E5=AD=98=E6=B4=BB=E4=B8=8A=E7=95=8C=E6=8B=B7?= =?UTF-8?q?=E8=B4=9D=E5=87=8F=E5=B0=91=E6=95=B4=E7=AA=97=E6=90=AC=E7=A7=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RegAlloc 改写后补第三次 liveness,为每条 CALL/NEW/SUPER_CALL 计算调用点 存活上界(最大存活物理槽号 + 1,254/255 由帧单独保存不计入),编码进 ext 高 8 位(nargs 低 8 位不变)。三处手写压帧窗口 = min(调用方活动寄存 器, 存活上界),restore_frame 按窗口回拷;CallFrame 新增 caller_active_reg_limit 保存调用方真实活动上限,恢复时不随截断窗口缩水。 生成器/异步函数体调用点保持全量窗口。spread 调用与运行时发起压帧 (accessor/内联)走全量路径不受影响。 --- crates/oxide_compiler/src/compiler.rs | 5 + crates/oxide_ir/src/inst.rs | 11 +- crates/oxide_regalloc/src/call_window.rs | 143 ++++++++++++ crates/oxide_regalloc/src/lib.rs | 2 + crates/oxide_vm/src/dispatch/call.rs | 20 +- crates/oxide_vm/src/session_gc.rs | 1 + crates/oxide_vm/src/suspended.rs | 2 + crates/oxide_vm/src/vm.rs | 30 ++- crates/oxide_vm/src/vm_dispatch_ctrl.rs | 3 + crates/oxide_vm/src/vm_dispatch_misc.rs | 6 +- crates/oxide_vm/src/vm_props.rs | 2 + crates/oxide_vm/src/vm_runtime.rs | 14 +- .../oxide_vm/tests/call_frame_window_tests.rs | 220 ++++++++++++++++++ 13 files changed, 448 insertions(+), 11 deletions(-) create mode 100644 crates/oxide_regalloc/src/call_window.rs create mode 100644 crates/oxide_vm/tests/call_frame_window_tests.rs diff --git a/crates/oxide_compiler/src/compiler.rs b/crates/oxide_compiler/src/compiler.rs index 1c74d91..ad14ed0 100644 --- a/crates/oxide_compiler/src/compiler.rs +++ b/crates/oxide_compiler/src/compiler.rs @@ -77,6 +77,11 @@ impl Compiler { let cfg2 = oxide_cfg::build_cfg(&ir); // 精确 DCE 删指令后 inst 下标位移,CFG 重建 let live2 = oxide_liveness::liveness(&ir, &cfg2); // 重算,绝不用过期 live oxide_regalloc::alloc(&mut ir, &live2)?; // 无可行染色 → RangeError 上抛 + // RegAlloc 改写(vreg→phys + spill 插入)后补第三次 liveness:为调用点 + // 存活上界提供物理级活集(改写过后的 LiveInfo 已过期,必须重算)。 + let cfg3 = oxide_cfg::build_cfg(&ir); + let live3 = oxide_liveness::liveness(&ir, &cfg3); + oxide_regalloc::encode_call_window(&mut ir, &live3); crate::compiler_debug!("compile: after regalloc {} insts", ir.insts.len()); } crate::compiler_debug!("compile: done, {} instructions", ir.insts.len()); diff --git a/crates/oxide_ir/src/inst.rs b/crates/oxide_ir/src/inst.rs index b0494d9..a2b8268 100644 --- a/crates/oxide_ir/src/inst.rs +++ b/crates/oxide_ir/src/inst.rs @@ -124,9 +124,12 @@ impl Inst { Self::with_ext(OpCode::COMPOUND_MEMBER_USHR, obj, val, key, &[0; IC_EXT_WORDS]) } - // ── Call 系:ext = [nargs] ── + // ── Call 系:ext = [nargs | (存活上界 << 8)] ── + // 低 8 位 nargs;高 8 位由 RegAlloc 后 `encode_call_window` 回填调用点存活 + // 上界(窗口 regs[0..上界],0 = 全量)。emit 构造时高 8 位恒 0。 - /// 普通函数调用:rd=callee,a=this,b=首参,ext=\[nargs\]。参数从 `first_arg` 起连续占 nargs 个寄存器。 + /// 普通函数调用:rd=callee,a=this,b=首参,ext=[nargs|(上界<<8)]。 + /// 参数从 `first_arg` 起连续占 nargs 个寄存器。 pub fn call(callee: Operand, this: Operand, first_arg: Operand, nargs: u8) -> Self { Self::with_ext(OpCode::CALL, callee, this, first_arg, &[nargs as u32]) } @@ -136,12 +139,12 @@ impl Inst { Self::with_ext(OpCode::CALL_NATIVE, callee, this, first_arg, &[nargs as u32]) } - /// `new` 表达式:结果写入 `result`,构造函数为 `constructor`。 + /// `new` 表达式:结果写入 `result`,构造函数为 `constructor`。ext 编码同 `call`。 pub fn new_expression(result: Operand, constructor: Operand, first_arg: Operand, nargs: u8) -> Self { Self::with_ext(OpCode::NEW_EXPRESSION, result, constructor, first_arg, &[nargs as u32]) } - /// 派生类构造中的 `super(...)`:结果写入 `result`。 + /// 派生类构造中的 `super(...)`:结果写入 `result`。ext 编码同 `call`。 pub fn super_call(result: Operand, first_arg: Operand, nargs: u8) -> Self { Self::with_ext(OpCode::SUPER_CALL, result, first_arg, Operand::None, &[nargs as u32]) } diff --git a/crates/oxide_regalloc/src/call_window.rs b/crates/oxide_regalloc/src/call_window.rs new file mode 100644 index 0000000..b09240d --- /dev/null +++ b/crates/oxide_regalloc/src/call_window.rs @@ -0,0 +1,143 @@ +//! 调用点存活上界编码:RegAlloc 改写后按物理存活集为调用指令回填 ext 高 8 位。 +//! +//! 调用指令 `ext[0] = nargs(低 8 位) | 存活上界(高 8 位)`。上界 = 调用点存活 +//! 物理寄存器最大槽号 + 1(即压帧窗口 `regs[0..上界]`);上界 0 表示未编码 +//! (运行时回退到调用方 `active_reg_limit` 全量窗口)。`regs[254]/[255]` +//! (this/new.target)由帧单独保存不占窗口,计算时排除。生成器 / 异步函数体 +//! 内部调用点不编码——挂起恢复按全量寄存器快照搬移,保持保守语义。 + +use oxide_bytecode::opcode::OpCode; +use oxide_ir::IRFunction; +use oxide_liveness::LiveInfo; + +/// 为每个字节码调用指令(CALL/NEW_EXPRESSION/SUPER_CALL)编码存活上界到 ext 高 8 位。 +/// 嵌套子函数递归处理:每个子函数独立重跑 CFG + liveness(与 `alloc` 同纪律)。 +/// +/// # 边界与前提 +/// - `live` 与 `f.insts` 维度不符时内部重算(防过期 LiveInfo 下标错位) +/// - 生成器 / 异步函数体内部调用点不编码(保持全量窗口) +/// - 只改写 `ext[0]` 高 8 位,nargs 低 8 位不动;无存活时保持 0(全量) +pub fn encode_call_window(f: &mut IRFunction, live: &LiveInfo) { + if f.insts.is_empty() && f.nested.is_empty() { + return; + } + // LiveInfo 维度守卫:与 alloc 相同纪律,过期则重算。 + let live = if live.inst_live_before.len() == f.insts.len() && !f.insts.is_empty() { + live.clone() + } else { + let cfg = oxide_cfg::build_cfg(f); + oxide_liveness::liveness(f, &cfg) + }; + // 生成器 / 异步函数体:调用点保持全量窗口(挂起恢复按全量寄存器快照搬移)。 + if !f.is_generator && !f.is_async { + for (i, inst) in f.insts.iter_mut().enumerate() { + if matches!(inst.op, OpCode::CALL | OpCode::NEW_EXPRESSION | OpCode::SUPER_CALL) { + let after = &live.inst_live_after[i]; + // 存活上界 = 最大存活槽号 + 1;254/255 由帧单独保存,不进窗口。 + let mut upper = 0u32; + for (reg, &alive) in after.iter().enumerate().take(254) { + if alive { + upper = (reg + 1) as u32; + } + } + if upper > 0 { + inst.ext[0] = (inst.ext[0] & 0xFF) | (upper << 8); + } + } + } + } + for child in &mut f.nested { + let cfg = oxide_cfg::build_cfg(child); + let l = oxide_liveness::liveness(child, &cfg); + encode_call_window(child, &l); + } +} + +#[cfg(test)] +mod tests { + use super::*; + use oxide_ir::inst::Inst; + use oxide_ir::operand::Operand; + + #[test] + fn call_window_packs_upper_bound_from_live_after() { + // 0: CALL(r1=callee, r2=this, r3=首参, 1 参) def {0} use {1,2,3} + // 1: ADD r5 = r4 + r4 r4 跨调用存活 + // 2: RETURN r5 + let mut f = IRFunction::new(); + f.insts.push(Inst::call(Operand::Reg(1), Operand::Reg(2), Operand::Reg(3), 1)); + f.insts + .push(Inst::new(OpCode::ADD, Operand::Reg(5), Operand::Reg(4), Operand::Reg(4))); + f.insts + .push(Inst::new(OpCode::RETURN, Operand::Reg(5), Operand::None, Operand::None)); + let cfg = oxide_cfg::build_cfg(&f); + let live = oxide_liveness::liveness(&f, &cfg); + encode_call_window(&mut f, &live); + // 存活集 {4} → 上界 5,ext = 1 | (5 << 8) + assert_eq!(f.insts[0].ext[0], 1 | (5 << 8)); + } + + #[test] + fn call_window_zero_when_nothing_survives() { + // 0: CALL(结果未用) 1: LOAD_CONST r6 2: RETURN r6 + let mut f = IRFunction::new(); + f.insts.push(Inst::call(Operand::Reg(1), Operand::Reg(2), Operand::Reg(3), 1)); + f.insts.push(Inst::load_const(Operand::Reg(6), 0)); + f.insts + .push(Inst::new(OpCode::RETURN, Operand::Reg(6), Operand::None, Operand::None)); + let cfg = oxide_cfg::build_cfg(&f); + let live = oxide_liveness::liveness(&f, &cfg); + encode_call_window(&mut f, &live); + // 无跨调用存活 → 保持全量(ext 高 8 位 = 0) + assert_eq!(f.insts[0].ext[0], 1); + } + + #[test] + fn call_window_excludes_this_and_new_target() { + // 0: LOAD_CONST r6 1: CALL 2: LOAD_VAR r8, This(254 跨调用存活) + // 3: ADD r7=r6+r8 4: RETURN r7 + let mut f = IRFunction::new(); + f.insts.push(Inst::load_const(Operand::Reg(6), 0)); + f.insts.push(Inst::call(Operand::Reg(1), Operand::Reg(2), Operand::Reg(3), 1)); + f.insts + .push(Inst::new(OpCode::LOAD_VAR, Operand::Reg(8), Operand::This, Operand::None)); + f.insts + .push(Inst::new(OpCode::ADD, Operand::Reg(7), Operand::Reg(6), Operand::Reg(8))); + f.insts + .push(Inst::new(OpCode::RETURN, Operand::Reg(7), Operand::None, Operand::None)); + let cfg = oxide_cfg::build_cfg(&f); + let live = oxide_liveness::liveness(&f, &cfg); + encode_call_window(&mut f, &live); + // 存活 {6, 254} → 上界 7(254 由帧单独保存,不放大窗口) + assert_eq!(f.insts[1].ext[0], 1 | (7 << 8)); + } + + #[test] + fn call_window_skips_generator_body() { + let mut f = IRFunction::new(); + f.is_generator = true; + f.insts.push(Inst::call(Operand::Reg(1), Operand::Reg(2), Operand::Reg(3), 1)); + f.insts + .push(Inst::new(OpCode::ADD, Operand::Reg(5), Operand::Reg(4), Operand::Reg(4))); + f.insts + .push(Inst::new(OpCode::RETURN, Operand::Reg(5), Operand::None, Operand::None)); + let cfg = oxide_cfg::build_cfg(&f); + let live = oxide_liveness::liveness(&f, &cfg); + encode_call_window(&mut f, &live); + // 生成器体不编码 → ext 保持纯 nargs + assert_eq!(f.insts[0].ext[0], 1); + } + + #[test] + fn call_window_stale_live_reruns() { + // 空 LiveInfo(维度不符)→ 内部重跑,仍正确编码。 + let mut f = IRFunction::new(); + f.insts.push(Inst::call(Operand::Reg(1), Operand::Reg(2), Operand::Reg(3), 1)); + f.insts + .push(Inst::new(OpCode::ADD, Operand::Reg(5), Operand::Reg(4), Operand::Reg(4))); + f.insts + .push(Inst::new(OpCode::RETURN, Operand::Reg(5), Operand::None, Operand::None)); + encode_call_window(&mut f, &LiveInfo::new()); + assert_eq!(f.insts[0].ext[0], 1 | (5 << 8)); + } +} diff --git a/crates/oxide_regalloc/src/lib.rs b/crates/oxide_regalloc/src/lib.rs index 7b43cca..aebe5d7 100644 --- a/crates/oxide_regalloc/src/lib.rs +++ b/crates/oxide_regalloc/src/lib.rs @@ -7,6 +7,7 @@ //! (元数据回写)。`alloc()` 是完整改写 pass 入口,`color()` 只产决策。 mod alloc_map; +mod call_window; mod color; mod finish; mod graph; @@ -14,6 +15,7 @@ mod regalloc_log; mod rewrite; pub use alloc_map::{Alloc, AllocMap, FreshKind, FreshVreg, SpillPlan}; +pub use call_window::encode_call_window; use oxide_ir::IRFunction; use oxide_liveness::LiveInfo; diff --git a/crates/oxide_vm/src/dispatch/call.rs b/crates/oxide_vm/src/dispatch/call.rs index 2306a5a..43d7279 100644 --- a/crates/oxide_vm/src/dispatch/call.rs +++ b/crates/oxide_vm/src/dispatch/call.rs @@ -87,7 +87,16 @@ impl Vm { } } } - self.push_bytecode_frame(callee, this, &args, None, None, JsValue::undefined(), FrameContinuation::None) + self.push_bytecode_frame( + callee, + this, + &args, + None, + None, + JsValue::undefined(), + FrameContinuation::None, + 0, + ) } } } @@ -389,6 +398,8 @@ impl Vm { let ext = self.bytecode[self.pc]; self.pc += 1; let arg_count = (ext & 0xFF) as usize; + // ext 高 8 位 = 调用点存活上界(0 = 未编码/全量),压帧窗口按此截断。 + let call_window = (ext >> 8) as u8; let Some(frame) = self.frames.last() else { self.raise_error_kind("ReferenceError", "super() used outside class constructor")?; @@ -483,7 +494,8 @@ impl Vm { let sub_n_args = self.sub_modules[sub_idx].n_args as usize; let sub_n_registers = self.sub_modules[sub_idx].n_registers; let sub_param_base = self.sub_modules[sub_idx].param_base as usize; - let caller_reg_limit = self.active_reg_limit.max(1); + let caller_active_reg_limit = self.active_reg_limit.max(1); + let caller_reg_limit = self.call_window_limit(caller_active_reg_limit, call_window); let saved_reg_offset = self.save_stack.len() as u32; self.save_stack.extend_from_slice(&self.regs[..caller_reg_limit as usize]); let saved_this = self.regs[254]; @@ -517,6 +529,7 @@ impl Vm { return_addr: self.pc, function_name, caller_reg_limit, + caller_active_reg_limit, saved_reg_offset, spill_offset: self.spill_stack.len() as u32, arguments_base: args_base, @@ -786,6 +799,7 @@ impl Vm { None, JsValue::undefined(), FrameContinuation::None, + 0, )?; return Ok(true); } @@ -892,6 +906,7 @@ impl Vm { Some(new_obj_val), constructor, FrameContinuation::None, + 0, )?; Ok(true) } else { @@ -979,6 +994,7 @@ impl Vm { Some(derived_this), new_target, FrameContinuation::None, + 0, )?; Ok(true) } else { diff --git a/crates/oxide_vm/src/session_gc.rs b/crates/oxide_vm/src/session_gc.rs index 8668f90..59af496 100644 --- a/crates/oxide_vm/src/session_gc.rs +++ b/crates/oxide_vm/src/session_gc.rs @@ -667,6 +667,7 @@ mod tests { return_addr: 0, function_name: 0, caller_reg_limit: 1, + caller_active_reg_limit: 1, saved_reg_offset: 0, spill_offset: 0, arguments_base: 0, diff --git a/crates/oxide_vm/src/suspended.rs b/crates/oxide_vm/src/suspended.rs index f07f996..5cdd31e 100644 --- a/crates/oxide_vm/src/suspended.rs +++ b/crates/oxide_vm/src/suspended.rs @@ -272,6 +272,7 @@ impl SuspendedFrame { return_addr: f.return_addr, function_name: f.function_name, caller_reg_limit: f.caller_reg_limit, + caller_active_reg_limit: f.caller_active_reg_limit, saved_reg_offset: f.saved_reg_offset, spill_offset: f.spill_offset, arguments_base: f.arguments_base, @@ -331,6 +332,7 @@ mod tests { return_addr: 7, function_name: 1, caller_reg_limit: 2, + caller_active_reg_limit: 2, saved_reg_offset: 3, spill_offset: 4, arguments_base: 5, diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index 5be87e9..2515dee 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -170,7 +170,13 @@ pub enum FrameContinuation { pub struct CallFrame { pub return_addr: usize, pub function_name: u32, + /// 压帧保存的调用方寄存器窗口长度(= save_stack 中本帧段大小)。 + /// 普通字节码调用按调用点存活上界截断;运行时发起(accessor/内联)为 + /// 调用方 `active_reg_limit` 全量。 pub caller_reg_limit: u8, + /// 压帧时刻调用方 `active_reg_limit`(真实值):帧恢复时据此还原,与 + /// `caller_reg_limit`(可能被存活上界截断的窗口)解耦。 + pub caller_active_reg_limit: u8, pub saved_reg_offset: u32, /// 记录本帧 spill 栈起始长度,作为 SPILL/UNSPILL 的帧边界基址(push 时快照)。 pub spill_offset: u32, @@ -1139,10 +1145,24 @@ impl Vm { self.call_bytecode_function_inline(callee, callee_obj, receiver, args) } + /// 压帧窗口上界:调用方活动寄存器与调用点存活上界取 min。 + /// + /// # 边界与前提 + /// - `call_window` 为 CALL ext 高 8 位编码的存活上界;0 = 未编码(回退全量)。 + /// - 窗口至少为 1(reg 0 恒为调用结果槽)。 + pub(crate) fn call_window_limit(&self, caller_active_reg_limit: u8, call_window: u8) -> u8 { + if call_window == 0 { + caller_active_reg_limit + } else { + caller_active_reg_limit.min(call_window) + } + .max(1) + } + #[expect(clippy::too_many_arguments)] pub(crate) fn push_bytecode_frame( &mut self, callee: JsValue, this_value: JsValue, args: &[JsValue], construct_result_reg: Option, - constructed_this: Option, new_target: JsValue, continuation: FrameContinuation, + constructed_this: Option, new_target: JsValue, continuation: FrameContinuation, call_window: u8, ) -> Result<(), String> { vm_trace!( "push_bytecode_frame: depth={}, args={}, continuation={:?}", @@ -1174,7 +1194,11 @@ impl Vm { let sub_n_registers = self.sub_modules[sub_idx].n_registers; let sub_param_base = self.sub_modules[sub_idx].param_base as usize; let sub_is_arrow = self.sub_modules[sub_idx].is_arrow; - let caller_reg_limit = self.active_reg_limit.max(1); + // 窗口 = min(调用方活动寄存器, 存活上界);call_window=0 表示调用方全量 + // (运行时发起路径 / 未编码的旧模块)。恢复按窗口回拷,active_reg_limit + // 仍还原为调用方真实值(caller_active_reg_limit)。 + let caller_active_reg_limit = self.active_reg_limit.max(1); + let caller_reg_limit = self.call_window_limit(caller_active_reg_limit, call_window); let saved_reg_offset = self.save_stack.len() as u32; self.save_stack.extend_from_slice(&self.regs[..caller_reg_limit as usize]); let saved_this = self.regs[254]; @@ -1205,6 +1229,7 @@ impl Vm { return_addr: self.pc, function_name, caller_reg_limit, + caller_active_reg_limit, saved_reg_offset, spill_offset: self.spill_stack.len() as u32, arguments_base: args_base, @@ -2213,6 +2238,7 @@ mod tests { return_addr: 1, function_name: 0, caller_reg_limit: 2, + caller_active_reg_limit: 2, saved_reg_offset: 0, spill_offset: 0, arguments_base: 0, diff --git a/crates/oxide_vm/src/vm_dispatch_ctrl.rs b/crates/oxide_vm/src/vm_dispatch_ctrl.rs index 8f4ba52..3a2861f 100644 --- a/crates/oxide_vm/src/vm_dispatch_ctrl.rs +++ b/crates/oxide_vm/src/vm_dispatch_ctrl.rs @@ -25,6 +25,8 @@ impl Vm { let ext = self.bytecode[self.pc]; self.pc += 1; let arg_count = (ext & 0xFF) as usize; + // ext 高 8 位 = 调用点存活上界(0 = 未编码/全量),压帧窗口按此截断。 + let call_window = (ext >> 8) as u8; crate::vm_debug!("CALL rd={} this={} args={} depth={}", rd, this_reg, arg_count, self.frames.len()); if obj.native_fn().is_some() { @@ -65,6 +67,7 @@ impl Vm { None, JsValue::undefined(), FrameContinuation::None, + call_window, )?; return Ok(true); } diff --git a/crates/oxide_vm/src/vm_dispatch_misc.rs b/crates/oxide_vm/src/vm_dispatch_misc.rs index 8b3c8a5..6c6bed9 100644 --- a/crates/oxide_vm/src/vm_dispatch_misc.rs +++ b/crates/oxide_vm/src/vm_dispatch_misc.rs @@ -43,6 +43,8 @@ impl Vm { let ext = self.bytecode[self.pc]; self.pc += 1; let arg_count = (ext & 0xFF) as usize; + // ext 高 8 位 = 调用点存活上界(0 = 未编码/全量),压帧窗口按此截断。 + let call_window = (ext >> 8) as u8; let proto_ptr = &*self.object_prototype as *const JsObject as *mut JsObject; let new_obj = self.alloc_object(JsObject::new_empty( @@ -113,7 +115,8 @@ impl Vm { let sub_n_args = self.sub_modules[sub_idx].n_args as usize; let sub_n_registers = self.sub_modules[sub_idx].n_registers; let sub_param_base = self.sub_modules[sub_idx].param_base as usize; - let caller_reg_limit = self.active_reg_limit.max(1); + let caller_active_reg_limit = self.active_reg_limit.max(1); + let caller_reg_limit = self.call_window_limit(caller_active_reg_limit, call_window); let saved_reg_offset = self.save_stack.len() as u32; self.save_stack.extend_from_slice(&self.regs[..caller_reg_limit as usize]); let saved_this = self.regs[254]; @@ -149,6 +152,7 @@ impl Vm { .map(|name| self.kernel_core.perm_interner().intern(name).0) .unwrap_or(0), caller_reg_limit, + caller_active_reg_limit, saved_reg_offset, spill_offset: self.spill_stack.len() as u32, arguments_base: args_base, diff --git a/crates/oxide_vm/src/vm_props.rs b/crates/oxide_vm/src/vm_props.rs index bce4908..2b0e06a 100644 --- a/crates/oxide_vm/src/vm_props.rs +++ b/crates/oxide_vm/src/vm_props.rs @@ -131,6 +131,7 @@ impl Vm { None, JsValue::undefined(), FrameContinuation::AccessorGet { target_reg }, + 0, )?; self.accessor_frame_target_reg = Some(target_reg); Ok(true) @@ -330,6 +331,7 @@ impl Vm { None, JsValue::undefined(), FrameContinuation::AccessorSet, + 0, )?; Ok(()) } diff --git a/crates/oxide_vm/src/vm_runtime.rs b/crates/oxide_vm/src/vm_runtime.rs index 716efef..24f4069 100644 --- a/crates/oxide_vm/src/vm_runtime.rs +++ b/crates/oxide_vm/src/vm_runtime.rs @@ -202,7 +202,16 @@ impl Vm { self.saved_immutables_stack.clear(); self.cell_stack.clear(); self.inline_callee = None; - self.push_bytecode_frame(callee, this_value, args, None, None, JsValue::undefined(), FrameContinuation::None) + self.push_bytecode_frame( + callee, + this_value, + args, + None, + None, + JsValue::undefined(), + FrameContinuation::None, + 0, + ) } pub(crate) fn call_bytecode_function_inline( @@ -331,7 +340,8 @@ impl Vm { self.spill_stack.truncate(frame.spill_offset as usize); self.regs[254] = frame.saved_this; self.regs[255] = frame.saved_new_target; - self.active_reg_limit = frame.caller_reg_limit; + // active_reg_limit 还原为调用方真实值(caller_reg_limit 可能被存活上界截断)。 + self.active_reg_limit = frame.caller_active_reg_limit; self.pc = frame.return_addr; } diff --git a/crates/oxide_vm/tests/call_frame_window_tests.rs b/crates/oxide_vm/tests/call_frame_window_tests.rs new file mode 100644 index 0000000..e269f46 --- /dev/null +++ b/crates/oxide_vm/tests/call_frame_window_tests.rs @@ -0,0 +1,220 @@ +//! 调用帧窗口按存活上界拷贝的语义测试:验证压帧窗口缩小后调用方存活寄存器、 +//! this/new.target、spill 区与异常展开在各类调用路径下恢复正确。 + +use oxide_compiler::compiler::Compiler; +use oxide_parser::Allocator; +use oxide_vm::promise::promise_settled_value; +use oxide_vm::vm::Vm; +use oxide_vm::JsValue; + +/// 执行源码并格式化顶层结果;Promise 结果取其 drain 后的 settled 值。 +fn eval(source: &str) -> String { + let allocator = Allocator::default(); + let program = match oxide_parser::parse(&allocator, source) { + Ok(p) => p, + Err(e) => return format!("parse error: {}", e[0].message), + }; + let module = match Compiler::new().compile(&program) { + Ok(m) => m, + Err(e) => return format!("compile error: {e}"), + }; + let mut vm = Vm::new(); + match vm.run(&module) { + Ok(result) => format_value(&vm, result), + Err(e) => format!("vm error: {e}"), + } +} + +fn format_value(vm: &Vm, val: JsValue) -> String { + if val.is_string() { + format!("\"{}\"", vm.lookup_str(val).unwrap_or_default()) + } else if val.is_object() { + let obj = unsafe { &*val.as_js_object_ptr() }; + if obj.is_promise_obj() { + match promise_settled_value(obj) { + Some((true, v)) => format_value(vm, v), + Some((false, v)) => format!("", format_value(vm, v)), + None => "".to_string(), + } + } else { + "[object]".to_string() + } + } else { + format!("{val}") + } +} + +#[test] +fn call_window_basic_call_preserves_live_registers() { + // 多值跨调用存活:sink 之后 x2..x5 仍要参与求和。 + let src = "function sink(a){ return a; } \ + function caller(){ var x1=1,x2=2,x3=3,x4=4,x5=5; \ + var r = sink(x1); return r + x2 + x3 + x4 + x5; } \ + caller()"; + assert_eq!(eval(src), "15"); +} + +#[test] +fn call_window_nested_calls_keep_outer_values() { + // outer 的 base 跨 mid/inner 两层调用存活,返回后仍参与求和。 + let src = "function inner(a,b){ return a*b; } \ + function mid(x){ return inner(x+1, x+2); } \ + function outer(){ var base = 10; return mid(base) + base; } \ + outer()"; + assert_eq!(eval(src), "142"); +} + +#[test] +fn call_window_deep_recursion_returns_correct() { + let src = "function fib(n){ if (n < 2) return n; return fib(n-1) + fib(n-2); } fib(18)"; + assert_eq!(eval(src), "2584"); +} + +#[test] +fn call_window_recursive_counter_accumulates() { + // 每层调用点都有跨调用存活的累加器,验证窗口逐层截断不丢值。 + let src = "function sum(n){ if (n == 0) return 0; return n + sum(n-1); } sum(200)"; + assert_eq!(eval(src), "20100"); +} + +#[test] +fn call_window_closure_upvalue_survives_calls() { + let src = "function outer(){ var count = 0; \ + function inc(step){ count += step; return count; } \ + var a = inc(1); var b = inc(2); var c = inc(3); \ + return [a,b,c].join(','); } \ + outer()"; + assert_eq!(eval(src), "\"1,3,6\""); +} + +#[test] +fn call_window_generator_suspend_resume() { + let src = "function* gen(){ var a = 1; yield a; var b = yield a + 10; return a + b; } \ + var g = gen(); var r1 = g.next(); var r2 = g.next(100); var r3 = g.next(5); \ + [r1.value, r2.value, r3.value].join(',')"; + assert_eq!(eval(src), "\"1,11,6\""); +} + +#[test] +fn call_window_generator_calls_inside_body() { + // 生成器体内调用普通函数(生成器体调用点保持全量窗口),挂起恢复后值正确。 + let src = "function add(a,b){ return a+b; } \ + function* gen(){ var x = add(20, 22); yield x; x = add(x, 1); yield x; } \ + var g = gen(); var r1 = g.next(); var r2 = g.next(); \ + [r1.value, r2.value].join(',')"; + assert_eq!(eval(src), "\"42,43\""); +} + +#[test] +fn call_window_async_await_chain() { + let src = "async function f(){ var x = 40; var y = await 2; return x + y; } f()"; + assert_eq!(eval(src), "42"); +} + +#[test] +fn call_window_apply_call_bind() { + let src = "function f(a,b,c){ return [a,b,c].join('-'); } \ + f.apply(null, [1,2,3]) + '|' + f.call(null, 4,5,6) + '|' + f.bind(null,7,8)(9)"; + assert_eq!(eval(src), "\"1-2-3|4-5-6|7-8-9\""); +} + +#[test] +fn call_window_new_expression() { + let src = "function P(x){ this.x = x; this.y = x * 2; } \ + var p = new P(21); p.x + p.y"; + assert_eq!(eval(src), "63"); +} + +#[test] +fn call_window_derived_super_call() { + let src = "class A { constructor(v){ this.v = v; } } \ + class B extends A { constructor(v){ super(v + 1); this.w = v * 10; } } \ + var b = new B(4); b.v + b.w"; + assert_eq!(eval(src), "45"); +} + +#[test] +fn call_window_many_live_values_large_function() { + // 高槽存活值跨调用:callee 写入区大,调用方存活槽需在窗口内完整恢复。 + let src = "function heavy(a){ var t = a; for (var i = 0; i < 100; i++){ t += i; } return t; } \ + function caller(){ \ + var k1=1,k2=2,k3=3,k4=4,k5=5,k6=6,k7=7,k8=8,k9=9,k10=10; \ + var r = heavy(1); \ + return k1+k2+k3+k4+k5+k6+k7+k8+k9+k10 + r; } \ + caller()"; + assert_eq!(eval(src), "5006"); +} + +#[test] +fn call_window_accessor_getter_and_setter() { + // accessor 压帧走全量窗口路径,getter/setter 返回后写回目标寄存器正确。 + let src = "var obj = { _v: 0, get v(){ return this._v; }, set v(x){ this._v = x * 2; } }; \ + obj.v = 21; obj.v"; + assert_eq!(eval(src), "42"); +} + +#[test] +fn call_window_throw_unwind_restores_frame() { + let src = "function boom(){ throw 'err'; } \ + function caller(){ var keep = 99; try { boom(); } catch(e) { keep += 1; } return keep; } \ + caller()"; + assert_eq!(eval(src), "100"); +} + +#[test] +fn call_window_tail_call_through_native_dispatcher() { + // bound 函数尾调用返回字节码函数,窗口恢复与结果交付正确。 + let src = "function add(a,b){ return a + b; } \ + var bound = add.bind(null, 30); bound(12)"; + assert_eq!(eval(src), "42"); +} + +/// 本测试所用小程序中带 1 个扩展字的 opcode 集合(推进扫描用)。 +fn ext_words(op: oxide_bytecode::opcode::OpCode) -> usize { + use oxide_bytecode::opcode::OpCode; + match op { + OpCode::SPILL + | OpCode::UNSPILL + | OpCode::CALL + | OpCode::CALL_NATIVE + | OpCode::NEW_EXPRESSION + | OpCode::SUPER_CALL => 1, + _ => 0, + } +} + +#[test] +fn call_window_upper_bound_encoded_into_bytecode() { + // 编译产物级断言:CALL 的 ext 高 8 位编码了调用点存活上界且小于调用方 + // n_registers(窗口确实被截断,非停留在全量路径);nargs 低 8 位不变。 + let allocator = Allocator::default(); + let program = oxide_parser::parse( + &allocator, + "function f(x){ return x + 1; } \ + function g(){ var a = 1; var b = 2; var c = 3; var d = 4; return f(a) + b + c + d; } \ + g()", + ) + .unwrap(); + let module = Compiler::new().compile(&program).unwrap(); + let bc = &module.bytecode; + let mut i = 0; + let mut call_ext = None; + while i < bc.len() { + let op = oxide_bytecode::opcode::opcode(bc[i]); + if op == oxide_bytecode::opcode::OpCode::CALL { + call_ext = Some(bc[i + 1]); + i += 2; + } else { + i += 1 + ext_words(op); + } + } + let ext = call_ext.expect("脚本中应有 CALL 指令"); + assert_eq!(ext & 0xFF, 0, "g() 无实参,nargs 低 8 位保持 0"); + let upper = (ext >> 8) & 0xFF; + assert!(upper > 0, "调用点存活上界应被编码进 ext 高 8 位"); + assert!( + upper <= module.n_registers as u32, + "存活上界 {upper} 不应超过调用方 n_registers {}", + module.n_registers + ); +}