fix: inline 回调窗口保存上限提到 254 覆盖物理槽 253

RegAlloc 合法产物 n_registers=254(builtin 槽落 253 或高活度着色)的
inline 字节码回调会写 regs[253],而 save_inline_state 窗口截断
regs_end.min(253) 漏存该槽,restore 不回拷 → 调用方 active_reg_limit=254
时 regs[253] 活动值静默丢失,与 native 分支 saved_r253 单存不对称。
窗口上限 253→254 后保存 regs[0..=253] + 254/255 单存,与全量 256 语义
逐槽等价;同步修正 InlineSyncState 与 save_inline_state 文档。

补三条回归:窗口 254 往返(含槽 253)、n_registers=254 inline 回调不
覆盖调用方活动 regs[253]、native 回调内嵌 254 寄存器 inline 回调不污染
外层 receiver 槽。
This commit is contained in:
2026-08-15 04:16:59 +08:00
parent b479fb4a4d
commit f9f54214fa
2 changed files with 107 additions and 8 deletions
+80 -3
View File
@@ -255,8 +255,9 @@ impl Completion {
/// 放在堆上避免 JS 代码链式同步字节码调用(如 sort 比较器、accessor)时
/// 耗尽 Rust 栈。
pub(crate) struct InlineSyncState {
/// 寄存器窗口副本:`regs[0..len]`,len ≤ 253。`regs[254]/[255]` 不在此列,
/// 由 `saved_this`/`saved_new_target` 单独保存(callee 也会重写这两个槽)。
/// 寄存器窗口副本:`regs[0..len]`,len ≤ 254(含 RegAlloc 最高合法物理槽
/// 253)。`regs[254]/[255]` 不在此列,由 `saved_this`/`saved_new_target`
/// 单独保存(callee 也会重写这两个槽)。
pub(crate) regs: Box<[JsValue]>,
pub(crate) saved_this: JsValue,
pub(crate) saved_new_target: JsValue,
@@ -2103,7 +2104,7 @@ mod tests {
use oxide_runtime_api::NativeResult;
use oxide_types::object::NativeFnPtr;
use oxide_types::object::{JsObject, PropAttributes};
use std::sync::Arc;
use std::sync::{Arc, OnceLock};
fn native_return_7(_vm: &mut Vm, _args: &[u8]) -> NativeResult {
NativeResult::Ok(JsValue::int(7))
@@ -2140,6 +2141,23 @@ mod tests {
NativeResult::Ok(JsValue::int(args.len().saturating_sub(1) as i32))
}
fn native_nested_inline_254(vm: &mut Vm, args: &[u8]) -> NativeResult {
// 外层 native 回调:receiver 落 regs[253](native 分支单存槽),内嵌执行
// n_registers=254 的 inline 字节码回调后,receiver 槽必须保持外层值。
let receiver = vm.reg(args[0]);
let callee = vm.reg(args[1]);
let callee_obj = unsafe { &*callee.as_js_object_ptr() };
let mut call_args = Vec::with_capacity(args.len().saturating_sub(2));
for &r in &args[2..] {
call_args.push(vm.reg(r));
}
let kept = match vm.call_bytecode_function_inline(callee, callee_obj, receiver, &call_args) {
Ok(_) => vm.regs[253] == receiver,
Err(_) => false,
};
NativeResult::Ok(JsValue::int(if kept { 1 } else { 0 }))
}
fn native_function(vm: &mut Vm, f: crate::native::NativeFn) -> JsValue {
let proto = vm.session.builtin_world().function_proto.as_ptr() as *mut JsObject;
let mut obj = JsObject::new_empty(oxide_kernel::shape_forge::EMPTY_SHAPE_ID, JsValue::from_js_object(proto));
@@ -2447,4 +2465,63 @@ mod tests {
assert_eq!(vm.reg(254), JsValue::int(12));
assert_eq!(vm.reg(255), JsValue::int(13));
}
/// 构造 `n_registers = 254` 的子模块:RegAlloc 合法产物(builtin 槽落 253 或
/// 高活度着色),其函数体写物理槽 253 后返回。
fn sub_module_254_with_r253_write() -> CompiledModule {
CompiledModule {
bytecode: Arc::from(vec![
opcode::encode(opcode::OpCode::MOV, 253, 0, 0),
opcode::encode(opcode::OpCode::HALT, 0, 0, 0),
]),
n_registers: 254,
..CompiledModule::new()
}
}
#[test]
fn inline_callee_254_registers_preserves_caller_active_r253() {
// 窗口化边界回归:inline 回调 callee 的 n_registers = 254(写物理槽 253)
// 且调用方 active_reg_limit = 254(regs[253] 为活动值)时,调用后
// regs[253] 必须恢复为调用方值,不得被 callee 写值覆盖。
let mut vm = Vm::new();
vm.sub_modules = Arc::new(vec![Arc::new(CompiledModule::new()), Arc::new(sub_module_254_with_r253_write())]);
vm.immutables_cache
.extend((0..vm.sub_modules.len().saturating_sub(vm.immutables_cache.len())).map(|_| OnceLock::new()));
vm.active_reg_limit = 254;
vm.regs[253] = JsValue::int(42);
let callee = vm.create_function_object(1, false, false, false, false);
let callee_obj = unsafe { &*callee.as_js_object_ptr() };
let result = vm
.call_bytecode_function_inline(callee, callee_obj, JsValue::undefined(), &[])
.expect("inline call should succeed");
assert_eq!(result, JsValue::undefined());
assert_eq!(vm.regs[253], JsValue::int(42), "调用方 regs[253] 活动值不得被 callee 覆盖");
assert_eq!(vm.active_reg_limit, 254, "restore 应还原调用方活动寄存器上限");
}
#[test]
fn native_callback_nested_254_register_inline_callee_keeps_receiver() {
// 嵌套回归防线:native 回调体(receiver 落 regs[253])内嵌 n_registers=254
// 的 inline 字节码回调时,内层写 regs[253] 不得污染外层 receiver 槽。
let mut vm = Vm::new();
vm.sub_modules = Arc::new(vec![Arc::new(CompiledModule::new()), Arc::new(sub_module_254_with_r253_write())]);
vm.immutables_cache
.extend((0..vm.sub_modules.len().saturating_sub(vm.immutables_cache.len())).map(|_| OnceLock::new()));
vm.active_reg_limit = 254;
vm.regs[253] = JsValue::int(7);
vm.regs[254] = JsValue::int(8);
let inner_callee = vm.create_function_object(1, false, false, false, false);
let outer_native = native_function(&mut vm, native_nested_inline_254);
let result = vm
.call_function_sync(outer_native, JsValue::int(99), &[inner_callee])
.expect("native callback should succeed");
assert_eq!(result, JsValue::int(1), "内嵌 inline 回调后外层 receiver 槽必须保持原值");
assert_eq!(vm.regs[253], JsValue::int(7), "native 分支恢复后调用方 regs[253] 保持");
assert_eq!(vm.regs[254], JsValue::int(8), "native 分支恢复后调用方 regs[254] 保持");
}
}