feat(vm): 新增 MOV/SPILL/UNSPILL 执行语义 + spill 栈帧边界(D-06/D-07/D-08)
This commit is contained in:
@@ -29,6 +29,7 @@ rustc-hash.workspace = true
|
||||
oxide_compiler = { path = "../oxide_compiler" }
|
||||
proptest.workspace = true
|
||||
oxide_parser = { path = "../oxide_parser" }
|
||||
oxide_ir = { path = "../oxide_ir" }
|
||||
criterion = { version = "0.5", features = ["html_reports"] }
|
||||
|
||||
[[bench]]
|
||||
|
||||
@@ -391,6 +391,7 @@ impl Vm {
|
||||
function_name,
|
||||
caller_reg_limit,
|
||||
saved_reg_offset,
|
||||
spill_offset: self.spill_stack.len() as u32,
|
||||
saved_this,
|
||||
saved_new_target,
|
||||
callee: super_ctor,
|
||||
|
||||
@@ -75,6 +75,44 @@ impl Vm {
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
/// SPILL:regs[rd] → spill_stack[帧基址 + slot]。ext 字 = slot u16(D-06)。
|
||||
/// 帧基址 = 当前 CallFrame.spill_offset(顶层模块无帧 = 0,D-08)。
|
||||
#[inline(always)]
|
||||
pub(crate) fn dispatch_spill(&mut self, rd: usize) -> Result<(), String> {
|
||||
let slot = self.bytecode[self.pc] as usize;
|
||||
self.pc += 1;
|
||||
if slot > u16::MAX as usize {
|
||||
return Err(format!("SPILL slot {slot} out of range (max 65535)"));
|
||||
}
|
||||
let base = self.frames.last().map(|f| f.spill_offset as usize).unwrap_or(0);
|
||||
let idx = base + slot;
|
||||
if idx >= self.spill_stack.len() {
|
||||
self.spill_stack.resize(idx + 1, JsValue::undefined());
|
||||
}
|
||||
vm_trace!("SPILL r{} -> slot[{}] base={}", rd, slot, base);
|
||||
self.spill_stack[idx] = self.regs[rd];
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// UNSPILL:spill_stack[帧基址 + slot] → regs[rd]。越界读 undefined(防御,D-06 slot 由 RegAlloc 保证在界内)。
|
||||
#[inline(always)]
|
||||
pub(crate) fn dispatch_unspill(&mut self, rd: usize) -> Result<(), String> {
|
||||
let slot = self.bytecode[self.pc] as usize;
|
||||
self.pc += 1;
|
||||
if slot > u16::MAX as usize {
|
||||
return Err(format!("UNSPILL slot {slot} out of range (max 65535)"));
|
||||
}
|
||||
let base = self.frames.last().map(|f| f.spill_offset as usize).unwrap_or(0);
|
||||
let idx = base + slot;
|
||||
vm_trace!("UNSPILL r{} <- slot[{}] base={}", rd, slot, base);
|
||||
self.regs[rd] = if idx < self.spill_stack.len() {
|
||||
self.spill_stack[idx]
|
||||
} else {
|
||||
JsValue::undefined()
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[inline(always)]
|
||||
pub(crate) fn dispatch_new_object(&mut self, rd: usize) {
|
||||
vm_trace!("NEW_OBJECT rd={}", rd);
|
||||
|
||||
@@ -506,6 +506,9 @@ fn rewrite_vm_roots(vm: &mut Vm, forwarding: &HashMap<*mut JsObject, *mut JsObje
|
||||
for value in &mut vm.save_stack {
|
||||
*value = rewrite_forwarded_value(*value, forwarding);
|
||||
}
|
||||
for value in &mut vm.spill_stack {
|
||||
*value = rewrite_forwarded_value(*value, forwarding);
|
||||
}
|
||||
for frame in &mut vm.frames {
|
||||
frame.saved_this = rewrite_forwarded_value(frame.saved_this, forwarding);
|
||||
frame.saved_new_target = rewrite_forwarded_value(frame.saved_new_target, forwarding);
|
||||
@@ -586,6 +589,7 @@ mod tests {
|
||||
function_name: 0,
|
||||
caller_reg_limit: 1,
|
||||
saved_reg_offset: 0,
|
||||
spill_offset: 0,
|
||||
saved_this: JsValue::from_js_object(this_session),
|
||||
saved_new_target: JsValue::from_js_object(child_session),
|
||||
callee: JsValue::from_js_object(child_session),
|
||||
|
||||
@@ -110,6 +110,8 @@ pub struct CallFrame {
|
||||
pub function_name: u32,
|
||||
pub caller_reg_limit: u8,
|
||||
pub saved_reg_offset: u32,
|
||||
/// push 时记录 spill_stack 长度(D-08 帧边界,SPILL/UNSPILL 基址)。
|
||||
pub spill_offset: u32,
|
||||
pub saved_this: JsValue,
|
||||
pub saved_new_target: JsValue,
|
||||
pub callee: JsValue,
|
||||
@@ -159,6 +161,7 @@ pub(crate) struct InlineSyncState {
|
||||
pub(crate) saved_bytecode_stack: Vec<Vec<opcode::Instr>>,
|
||||
pub(crate) saved_immutables_stack: Vec<*const [JsValue]>,
|
||||
pub(crate) save_stack: Vec<JsValue>,
|
||||
pub(crate) spill_stack: Vec<JsValue>,
|
||||
pub(crate) cell_stack: Vec<Vec<*mut Cell>>,
|
||||
}
|
||||
|
||||
@@ -191,6 +194,9 @@ pub struct Vm {
|
||||
/// registers (`regs[..caller_reg_limit]`) here at `saved_reg_offset`; restore copies
|
||||
/// them back and truncates. Capacity is retained across calls — zero per-call heap alloc.
|
||||
pub(crate) save_stack: Vec<JsValue>,
|
||||
/// VM 级 spill 栈(D-08,仿 save_stack 帧边界)。`CallFrame.spill_offset` 定位本帧区,
|
||||
/// 调用子函数时从边界后分配,restore_frame 截断恢复。
|
||||
pub(crate) spill_stack: Vec<JsValue>,
|
||||
pub(crate) try_stack: Vec<TryHandler>,
|
||||
pub(crate) exception_value: Option<JsValue>,
|
||||
/// Side-channel carrying the JsValue thrown by a sync call whose error was flattened
|
||||
@@ -385,6 +391,10 @@ impl Vm {
|
||||
for &v in &self.save_stack {
|
||||
f(v);
|
||||
}
|
||||
// spill 栈是 session GC 根(漏根 → 溢出值被回收 → use-after-free)
|
||||
for &v in &self.spill_stack {
|
||||
f(v);
|
||||
}
|
||||
for cell_vec in &self.cell_stack {
|
||||
for &cell_ptr in cell_vec {
|
||||
if cell_ptr.is_null() {
|
||||
@@ -777,6 +787,7 @@ impl Vm {
|
||||
function_name,
|
||||
caller_reg_limit,
|
||||
saved_reg_offset,
|
||||
spill_offset: self.spill_stack.len() as u32,
|
||||
saved_this,
|
||||
saved_new_target,
|
||||
callee,
|
||||
@@ -989,6 +1000,19 @@ impl Vm {
|
||||
Err(e) => return Err(e),
|
||||
},
|
||||
|
||||
OpCode::MOV => {
|
||||
vm_trace!("MOV r{} = r{}", rd, a);
|
||||
self.regs[rd] = self.regs[a];
|
||||
}
|
||||
|
||||
OpCode::SPILL => {
|
||||
self.dispatch_spill(rd)?;
|
||||
}
|
||||
|
||||
OpCode::UNSPILL => {
|
||||
self.dispatch_unspill(rd)?;
|
||||
}
|
||||
|
||||
OpCode::CALL => match self.dispatch_call(rd, a, b) {
|
||||
Ok(true) => continue,
|
||||
Ok(false) => {}
|
||||
@@ -1461,6 +1485,7 @@ mod tests {
|
||||
function_name: 0,
|
||||
caller_reg_limit: 2,
|
||||
saved_reg_offset: 0,
|
||||
spill_offset: 0,
|
||||
saved_this: JsValue::undefined(),
|
||||
saved_new_target: JsValue::undefined(),
|
||||
callee: JsValue::undefined(),
|
||||
|
||||
@@ -130,6 +130,7 @@ impl Vm {
|
||||
.unwrap_or(0),
|
||||
caller_reg_limit,
|
||||
saved_reg_offset,
|
||||
spill_offset: self.spill_stack.len() as u32,
|
||||
saved_this,
|
||||
saved_new_target,
|
||||
callee: constructor,
|
||||
|
||||
@@ -60,6 +60,7 @@ impl Vm {
|
||||
saved_bytecode_stack: std::mem::take(&mut self.saved_bytecode_stack),
|
||||
saved_immutables_stack: std::mem::take(&mut self.saved_immutables_stack),
|
||||
save_stack: std::mem::take(&mut self.save_stack),
|
||||
spill_stack: std::mem::take(&mut self.spill_stack),
|
||||
cell_stack: std::mem::take(&mut self.cell_stack),
|
||||
});
|
||||
|
||||
@@ -111,6 +112,7 @@ impl Vm {
|
||||
self.saved_bytecode_stack = saved.saved_bytecode_stack;
|
||||
self.saved_immutables_stack = saved.saved_immutables_stack;
|
||||
self.save_stack = saved.save_stack;
|
||||
self.spill_stack = saved.spill_stack;
|
||||
self.cell_stack = saved.cell_stack;
|
||||
|
||||
result
|
||||
@@ -136,6 +138,8 @@ impl Vm {
|
||||
let len = frame.caller_reg_limit as usize;
|
||||
self.regs[..len].copy_from_slice(&self.save_stack[offset..offset + len]);
|
||||
self.save_stack.truncate(offset);
|
||||
// spill 帧边界截断(D-08,仿 save_stack 窗口):子函数 spill 数据在帧恢复后丢弃
|
||||
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;
|
||||
|
||||
@@ -37,6 +37,7 @@ impl Vm {
|
||||
saved_bytecode_stack: Vec::new(),
|
||||
saved_immutables_stack: Vec::new(),
|
||||
save_stack: Vec::new(),
|
||||
spill_stack: Vec::new(),
|
||||
try_stack: Vec::new(),
|
||||
exception_value: None,
|
||||
last_uncaught_value: None,
|
||||
@@ -100,6 +101,7 @@ impl Vm {
|
||||
saved_bytecode_stack: Vec::new(),
|
||||
saved_immutables_stack: Vec::new(),
|
||||
save_stack: Vec::new(),
|
||||
spill_stack: Vec::new(),
|
||||
try_stack: Vec::new(),
|
||||
exception_value: None,
|
||||
last_uncaught_value: None,
|
||||
@@ -213,6 +215,7 @@ impl Vm {
|
||||
self.saved_bytecode_stack.clear();
|
||||
self.saved_immutables_stack.clear();
|
||||
self.save_stack.clear();
|
||||
self.spill_stack.clear();
|
||||
self.cell_stack.clear();
|
||||
self.temp_immutables.clear();
|
||||
self.try_stack.clear();
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
//! VM spill 栈执行语义测试(D-06/D-07/D-08):MOV 复制、SPILL/UNSPILL 往返、
|
||||
//! 帧边界(父 spill 跨子调用完好)、嵌套双 spill 交互、UNSPILL 越界防御。
|
||||
//!
|
||||
//! 手工 IR 是唯一可达路径——RegAlloc 未接入,真实 JS 编译产物不会含三指令。
|
||||
|
||||
use oxide_bytecode::module::Constant;
|
||||
use oxide_bytecode::opcode::OpCode;
|
||||
use oxide_ir::inst::Inst;
|
||||
use oxide_ir::operand::Operand;
|
||||
use oxide_ir::IRFunction;
|
||||
use oxide_vm::vm::Vm;
|
||||
|
||||
fn run_ir(ir: IRFunction) -> String {
|
||||
let module = oxide_ir::lower::lower(&ir).expect("lower failed");
|
||||
let mut vm = Vm::new();
|
||||
match vm.run(&module) {
|
||||
Ok(v) => format!("{v}"),
|
||||
Err(e) => format!("vm error: {e}"),
|
||||
}
|
||||
}
|
||||
|
||||
fn int_module(insts: Vec<Inst>, value: i32, n_registers: u8, nested: Vec<IRFunction>) -> IRFunction {
|
||||
let mut f = IRFunction::new();
|
||||
f.insts = insts;
|
||||
f.constants = vec![Constant::Int(value)];
|
||||
f.n_registers = n_registers as u32;
|
||||
f.nested = nested;
|
||||
f
|
||||
}
|
||||
|
||||
/// 测试 1:MOV 寄存器复制语义(D-07)。r3=42 → MOV r5=r3 → HALT 输出 42。
|
||||
#[test]
|
||||
fn mov_copies_register() {
|
||||
let insts = vec![
|
||||
Inst::load_const(Operand::Reg(3), 0),
|
||||
Inst::inst_mov(Operand::Reg(5), Operand::Reg(3)),
|
||||
Inst::inst_mov(Operand::Reg(0), Operand::Reg(5)),
|
||||
Inst::new(OpCode::HALT, Operand::None, Operand::None, Operand::None),
|
||||
];
|
||||
let out = run_ir(int_module(insts, 42, 6, Vec::new()));
|
||||
assert_eq!(out, "42");
|
||||
}
|
||||
|
||||
/// 测试 2:SPILL→UNSPILL 往返(D-06)。SPILL r3 slot0 → MOV 弄脏 r5 → UNSPILL r5 slot0 → 读回 42。
|
||||
#[test]
|
||||
fn spill_unspill_roundtrip() {
|
||||
let insts = vec![
|
||||
Inst::load_const(Operand::Reg(3), 0),
|
||||
Inst::inst_spill(Operand::Reg(3), 0),
|
||||
Inst::inst_mov(Operand::Reg(5), Operand::Reg(3)),
|
||||
Inst::inst_unspill(Operand::Reg(5), 0),
|
||||
Inst::inst_mov(Operand::Reg(0), Operand::Reg(5)),
|
||||
Inst::new(OpCode::HALT, Operand::None, Operand::None, Operand::None),
|
||||
];
|
||||
let out = run_ir(int_module(insts, 42, 6, Vec::new()));
|
||||
assert_eq!(out, "42");
|
||||
}
|
||||
|
||||
/// 测试 3:帧边界——父函数 SPILL 后 CALL 子函数,子函数返回后父 UNSPILL 仍得 42(D-08)。
|
||||
#[test]
|
||||
fn frame_boundary_parent_spill_survives_call() {
|
||||
let child = int_module(
|
||||
vec![
|
||||
Inst::load_const(Operand::Reg(3), 0),
|
||||
Inst::new(OpCode::RETURN, Operand::Reg(3), Operand::None, Operand::None),
|
||||
],
|
||||
99,
|
||||
4,
|
||||
Vec::new(),
|
||||
);
|
||||
let insts = vec![
|
||||
Inst::load_const(Operand::Reg(3), 0),
|
||||
Inst::inst_spill(Operand::Reg(3), 0),
|
||||
Inst::create_closure(Operand::Reg(4), 1),
|
||||
Inst::call(Operand::Reg(4), Operand::This, Operand::Reg(4), 0),
|
||||
Inst::inst_unspill(Operand::Reg(5), 0),
|
||||
Inst::inst_mov(Operand::Reg(0), Operand::Reg(5)),
|
||||
Inst::new(OpCode::HALT, Operand::None, Operand::None, Operand::None),
|
||||
];
|
||||
let out = run_ir(int_module(insts, 42, 6, vec![child]));
|
||||
assert_eq!(out, "42", "父 spill 数据在子调用后必须完好");
|
||||
}
|
||||
|
||||
/// 测试 4:嵌套交互——父 SPILL 42、子函数自身也 SPILL(子帧基址 = 父已用长度 1),父 UNSPILL 仍得 42。
|
||||
#[test]
|
||||
fn nested_call_both_spill() {
|
||||
let child = int_module(
|
||||
vec![
|
||||
Inst::load_const(Operand::Reg(3), 0),
|
||||
Inst::inst_spill(Operand::Reg(3), 0),
|
||||
Inst::new(OpCode::RETURN, Operand::Reg(3), Operand::None, Operand::None),
|
||||
],
|
||||
99,
|
||||
4,
|
||||
Vec::new(),
|
||||
);
|
||||
let insts = vec![
|
||||
Inst::load_const(Operand::Reg(3), 0),
|
||||
Inst::inst_spill(Operand::Reg(3), 0),
|
||||
Inst::create_closure(Operand::Reg(4), 1),
|
||||
Inst::call(Operand::Reg(4), Operand::This, Operand::Reg(4), 0),
|
||||
Inst::inst_unspill(Operand::Reg(5), 0),
|
||||
Inst::inst_mov(Operand::Reg(0), Operand::Reg(5)),
|
||||
Inst::new(OpCode::HALT, Operand::None, Operand::None, Operand::None),
|
||||
];
|
||||
let out = run_ir(int_module(insts, 42, 6, vec![child]));
|
||||
assert_eq!(out, "42", "子帧 spill 截断后父数据必须完好");
|
||||
}
|
||||
|
||||
/// 测试 5:UNSPILL 空栈越界防御——读 undefined 不 panic。
|
||||
#[test]
|
||||
fn unspill_empty_stack_returns_undefined() {
|
||||
let insts = vec![
|
||||
Inst::inst_unspill(Operand::Reg(5), 0),
|
||||
Inst::inst_mov(Operand::Reg(0), Operand::Reg(5)),
|
||||
Inst::new(OpCode::HALT, Operand::None, Operand::None, Operand::None),
|
||||
];
|
||||
let out = run_ir(int_module(insts, 42, 6, Vec::new()));
|
||||
assert_eq!(out, "undefined");
|
||||
}
|
||||
Reference in New Issue
Block a user