fix: Function.prototype.apply 解除实参上限,native 大实参集走 spill 溢出区

apply 曾把实参数组静默截断到 55 个(寄存器窗口限制),导致
String.fromCodePoint.apply(null, codePoints) 这类大参数集调用只取前 55 个。
改为直接物化全部实参经 TailCall 下发:字节码目标走帧参数区(spill 栈)任意
个数;native 目标新增 spill 溢出区通道,寄存器窗口(253)之外的实参转存
spill 栈(GC 根、嵌套安全),builtin 经 VmHost::native_arg_count/native_arg_at
读取,String.fromCodePoint 迁移为新接口。大实参集不再静默截断。
This commit is contained in:
2026-08-15 20:31:32 +08:00
parent 1b9974fe7f
commit 1c70d0343d
6 changed files with 143 additions and 28 deletions
+9 -16
View File
@@ -90,7 +90,7 @@ pub fn function_call<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
}
/// `Function.prototype.apply(thisArg, argsArray)`:以指定 this 和参数数组调用目标函数。
/// 数组元素拷入寄存器(上限受寄存器空间限制,最多 55 个参数)。
/// 数组元素直接物化为实参切片经 TailCall 下发(帧参数区),不受寄存器窗口限制。
pub fn function_apply<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
if args.is_empty() {
return NativeResult::Err(JsValue::undefined());
@@ -98,7 +98,7 @@ pub fn function_apply<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
let target_val = vm.reg(args[0]);
let this_val = if args.len() > 1 { vm.reg(args[1]) } else { JsValue::undefined() };
let arg_regs: Vec<u8>;
let mut call_args: Vec<JsValue> = Vec::new();
if args.len() > 2 {
let arr_val = vm.reg(args[2]);
if arr_val.is_object() {
@@ -106,27 +106,20 @@ pub fn function_apply<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
if !arr_ptr.is_null() {
let arr = unsafe { &*arr_ptr };
if arr.is_array() {
let max_args = 55usize; // base=200,255 号寄存器之前的安全上限。
let n = arr.prop_count() as usize;
let n = n.min(max_args);
let base = 200u8;
arg_regs = (0..n).map(|i| base + i as u8).collect();
call_args.reserve(n);
for i in 0..n {
vm.set_reg(base + i as u8, arr.get_prop_at(i));
call_args.push(arr.get_prop_at(i));
}
} else {
arg_regs = Vec::new();
}
} else {
arg_regs = Vec::new();
}
} else {
arg_regs = Vec::new();
}
} else {
arg_regs = Vec::new();
}
invoke_target(vm, target_val, this_val, &arg_regs)
NativeResult::TailCall {
callee: target_val,
this: this_val,
args: call_args,
}
}
/// `Function.prototype.bind(thisArg, ...args)`:返回绑定 this 与前置实参的新包装函数,
+5 -2
View File
@@ -72,8 +72,11 @@ pub fn string_from_char_code<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult
pub fn string_from_code_point<H: VmHost>(vm: &mut H, args: &[u8]) -> NativeResult {
builtins_debug!("String.fromCodePoint called with {} args", args.len());
let mut out = String::new();
for &arg_reg in args.iter().skip(1) {
let n = match oxide_runtime_api::to_number_full(vm.reg(arg_reg), vm) {
// 经 native_arg_count/native_arg_at 读取:大实参集(如 harness 的
// `String.fromCodePoint.apply(null, codePoints)` 每块 10000 码位)在寄存器
// 窗口外走 spill 溢出区,小实参集与既有寄存器路径一致。
for i in 0..vm.native_arg_count(args) {
let n = match oxide_runtime_api::to_number_full(vm.native_arg_at(args, i), vm) {
Ok(n) => n,
Err(_) => {
// ToNumber 触发对象 valueOf/toString 抛出的原生异常须原样传播,
+30
View File
@@ -72,6 +72,36 @@ pub trait VmHost {
fn reg(&self, idx: u8) -> JsValue;
fn set_reg(&mut self, idx: u8, val: JsValue);
/// 当前 native 调用 spill 溢出区的实参个数(寄存器窗口 253 之外的部分)。
///
/// # 边界与前提
/// - 仅在 native 实现内部、实参仍有效时读取;无溢出时为 0。
fn native_overflow_count(&self) -> usize;
/// 读 spill 溢出区第 `i` 个实参(0 基,相对溢出区起点)。
///
/// # 边界与前提
/// - `i` 必须小于 [`Self::native_overflow_count`]。
fn native_overflow_at(&self, i: usize) -> JsValue;
/// 当前 native 调用的完整实参个数(寄存器窗口 + spill 溢出区,不含 receiver)。
///
/// 供支持大实参集的 builtin(如 `String.fromCodePoint`)遍历全部实参;
/// 未迁移的 builtin 仍按 `args` 索引读寄存器,行为不变。
fn native_arg_count(&self, args: &[u8]) -> usize {
args.len().saturating_sub(1) + self.native_overflow_count()
}
/// 第 `idx` 个实参(0 基,不含 receiver):窗口内读寄存器,窗口外读 spill 溢出区。
///
/// # 边界与前提
/// - `idx` 必须小于 [`Self::native_arg_count`]。
fn native_arg_at(&self, args: &[u8], idx: usize) -> JsValue {
let window = args.len().saturating_sub(1);
if idx < window {
self.reg(args[idx + 1])
} else {
self.native_overflow_at(idx - window)
}
}
// 对象分配 / 字符串创建
fn alloc_object(&mut self, obj: JsObject) -> *mut JsObject;
fn new_string(&mut self, s: &str) -> JsValue;
+56 -10
View File
@@ -379,6 +379,12 @@ pub struct Vm {
/// VM 级 spill 栈。`CallFrame.spill_offset` 定位本帧区:调用子函数时从边界后分配,
/// 帧恢复时截断到边界,子函数 spill 数据随帧丢弃。
pub(crate) spill_stack: Vec<JsValue>,
/// 本次 native 调用的 spill 溢出实参区:`spill_stack[base..base+count)`。
/// 实参数超过寄存器窗口(253)时,窗口外的实参转存 spill 栈(GC 根),
/// native 侧经 `VmHost::native_arg_count`/`native_arg_at` 读取。仅在一次
/// native 调用期间有效,调用返回前截断回收。
pub(crate) native_overflow_base: usize,
pub(crate) native_overflow_count: usize,
pub(crate) try_stack: Vec<TryHandler>,
pub(crate) exception_value: Option<JsValue>,
/// 同步调用抛出的原始 JsValue 侧通道:`call_function_sync`/`unwind` 把错误展平为
@@ -1143,9 +1149,16 @@ impl Vm {
self.raise_error_kind("RangeError", "Maximum call stack size exceeded")?;
return Ok(JsValue::undefined());
}
// 大实参集(超过寄存器窗口):窗口外的实参转存 spill 栈溢出区(GC 根),
// native 侧经 native_arg_count/native_arg_at 读取;窗口内仍按寄存器
// 协议打包,保证未迁移的 builtin 行为不变。
let overflow_base = self.spill_stack.len();
let saved_overflow_base = self.native_overflow_base;
let saved_overflow_count = self.native_overflow_count;
if args.len() > Self::SYNC_NATIVE_ARG_LIMIT {
self.raise_error_kind("RangeError", "Maximum call stack size exceeded")?;
return Ok(JsValue::undefined());
self.spill_stack.extend_from_slice(&args[Self::SYNC_NATIVE_ARG_LIMIT..]);
self.native_overflow_base = overflow_base;
self.native_overflow_count = args.len() - Self::SYNC_NATIVE_ARG_LIMIT;
}
// native 回调只写 regs[0..args.len()] 实参区 + regs[253]/[254](receiver/callee),
// 窗口 = 调用方活动寄存器 ∪ 实参写入区;窗口外槽回调不触碰,无需保存。
@@ -1155,13 +1168,20 @@ impl Vm {
saved_window.extend_from_slice(&self.regs[..window]);
let saved_r253 = self.regs[253];
let saved_r254 = self.regs[254];
let arg_regs = self.pack_sync_native_call_args(receiver, callee, args);
let pack_limit = args.len().min(Self::SYNC_NATIVE_ARG_LIMIT);
let arg_regs = self.pack_sync_native_call_args(receiver, callee, &args[..pack_limit]);
// SAFETY: native_fn 经 set_native_fn 以合法 NativeFn 指针设置;
// native_fn_ptr_to_fn 是 NativeFnPtr → NativeFn 的唯一强制转换点。
let func: NativeFn = unsafe { native_fn_ptr_to_fn(native_fn) };
self.native_call_depth += 1;
let result = func(self, &arg_regs);
self.native_call_depth -= 1;
// 溢出区随本次调用结束截断回收,并还原外层的溢出区描述(嵌套调用安全)。
if args.len() > Self::SYNC_NATIVE_ARG_LIMIT {
self.spill_stack.truncate(overflow_base);
}
self.native_overflow_base = saved_overflow_base;
self.native_overflow_count = saved_overflow_count;
// 窗口回拷 + regs[253]/[254] 单回,缓冲归还池复用。
self.regs[..window].copy_from_slice(&saved_window);
self.regs[253] = saved_r253;
@@ -1975,6 +1995,12 @@ impl oxide_runtime_api::VmHost for Vm {
fn set_reg(&mut self, idx: u8, val: JsValue) {
self.set_reg(idx, val);
}
fn native_overflow_count(&self) -> usize {
self.native_overflow_count
}
fn native_overflow_at(&self, i: usize) -> JsValue {
self.spill_stack[self.native_overflow_base + i]
}
fn alloc_object(&mut self, obj: JsObject) -> *mut JsObject {
self.alloc_object(obj)
}
@@ -2204,7 +2230,7 @@ fn rehome_subtree(module: &CompiledModule, base: u32, out: &mut Vec<Arc<Compiled
mod tests {
use super::{opcode, JsValue, TryHandler, Vm};
use oxide_bytecode::module::CompiledModule;
use oxide_runtime_api::NativeResult;
use oxide_runtime_api::{NativeResult, VmHost};
use oxide_types::object::NativeFnPtr;
use oxide_types::object::{JsObject, PropAttributes};
use std::sync::{Arc, OnceLock};
@@ -2244,6 +2270,15 @@ mod tests {
NativeResult::Ok(JsValue::int(args.len().saturating_sub(1) as i32))
}
fn native_return_full_arg_count(vm: &mut Vm, args: &[u8]) -> NativeResult {
NativeResult::Ok(JsValue::int(vm.native_arg_count(args) as i32))
}
fn native_return_last_full_arg(vm: &mut Vm, args: &[u8]) -> NativeResult {
let n = vm.native_arg_count(args);
NativeResult::Ok(vm.native_arg_at(args, n - 1))
}
fn native_nested_inline_254(vm: &mut Vm, args: &[u8]) -> NativeResult {
// 外层 native 回调:receiver 落 regs[253](native 分支单存槽),内嵌执行
// n_registers=254 的 inline 字节码回调后,receiver 槽必须保持外层值。
@@ -2565,24 +2600,35 @@ mod tests {
}
#[test]
fn call_function_sync_rejects_unrepresentable_native_arity_without_register_corruption() {
fn call_function_sync_overflow_preserves_large_native_arity_and_registers() {
// 大实参集(超寄存器窗口 253)经 spill 溢出区完整送达 native:
// 全量计数与末位实参都可读,且调用方寄存器不被打包过程污染。
let mut vm = Vm::new();
let callee = native_function(&mut vm, native_return_arg_count);
let callee = native_function(&mut vm, native_return_full_arg_count);
vm.set_reg(1, JsValue::int(7));
vm.set_reg(253, JsValue::int(11));
vm.set_reg(254, JsValue::int(12));
vm.set_reg(255, JsValue::int(13));
let args = vec![JsValue::undefined(); Vm::SYNC_NATIVE_ARG_LIMIT + 1];
let err = vm
let args: Vec<JsValue> = (0..(Vm::SYNC_NATIVE_ARG_LIMIT + 100)).map(|i| JsValue::int(i as i32)).collect();
let count = vm
.call_function_sync(callee, JsValue::undefined(), &args)
.expect_err("too many args should fail cleanly");
.expect("大实参集应经 spill 溢出区完整送达 native");
assert_eq!(count, JsValue::int(args.len() as i32));
let last_callee = native_function(&mut vm, native_return_last_full_arg);
let last = vm
.call_function_sync(last_callee, JsValue::undefined(), &args)
.expect("溢出区末位实参应可读");
assert_eq!(last, JsValue::int((args.len() - 1) as i32));
assert!(err.contains("Maximum call stack size exceeded"), "unexpected error: {err}");
assert_eq!(vm.reg(1), JsValue::int(7));
assert_eq!(vm.reg(253), JsValue::int(11));
assert_eq!(vm.reg(254), JsValue::int(12));
assert_eq!(vm.reg(255), JsValue::int(13));
// 溢出区随调用结束截断回收,不残留 spill 增长。
assert!(vm.spill_stack.is_empty(), "溢出区应已回收: {:?}", vm.spill_stack);
assert_eq!(vm.native_overflow_count, 0);
}
/// 构造 `n_registers = 254` 的子模块:RegAlloc 合法产物(builtin 槽落 253 或
+6
View File
@@ -60,6 +60,8 @@ impl Vm {
saved_immutables_stack: Vec::new(),
save_stack: Vec::new(),
spill_stack: Vec::new(),
native_overflow_base: 0,
native_overflow_count: 0,
try_stack: Vec::new(),
exception_value: None,
last_uncaught_value: None,
@@ -162,6 +164,8 @@ impl Vm {
saved_immutables_stack: Vec::new(),
save_stack: Vec::new(),
spill_stack: Vec::new(),
native_overflow_base: 0,
native_overflow_count: 0,
try_stack: Vec::new(),
exception_value: None,
last_uncaught_value: None,
@@ -353,6 +357,8 @@ impl Vm {
self.saved_immutables_stack.clear();
self.save_stack.clear();
self.spill_stack.clear();
self.native_overflow_base = 0;
self.native_overflow_count = 0;
self.cell_stack.clear();
self.try_stack.clear();
self.exception_value = None;
@@ -116,6 +116,43 @@ fn function_apply_returns_object_stays_valid() {
assert_num(result, 10.0);
}
#[test]
fn function_apply_passes_large_arg_array_to_bytecode_target() {
// 回归:apply 曾把实参数组静默截断到 55 个;大参数集必须完整到达目标函数。
let mut vm = Vm::new();
let result = eval(
&mut vm,
"var a = []; for (var i = 0; i < 1000; i++) { a[i] = i; } (function(){ return arguments.length; }).apply(null, a)",
)
.unwrap();
assert_eq!(result, JsValue::int(1000));
}
#[test]
fn function_apply_large_args_reach_last_element() {
// 大参数集经 spill 溢出区送达后,末位实参可被目标读取(不止计数正确)。
let mut vm = Vm::new();
let result = eval(
&mut vm,
"var a = []; for (var i = 0; i < 300; i++) { a[i] = i; } (function(){ return arguments[299]; }).apply(null, a)",
)
.unwrap();
assert_num(result, 299.0);
}
#[test]
fn function_apply_large_args_to_native_from_code_point() {
// native 目标(String.fromCodePoint)接收大实参集:整串完整拼接,验证
// harness `String.fromCodePoint.apply(null, codePoints)` 不再截断。
let mut vm = Vm::new();
let result = eval(
&mut vm,
"var a = []; for (var i = 0; i < 1000; i++) { a[i] = 65 + (i % 26); } String.fromCodePoint.apply(null, a).length",
)
.unwrap();
assert_eq!(result, JsValue::int(1000));
}
#[test]
fn getter_returns_object_stays_valid() {
// 同类 bug:字节码 accessor 经 ordinary_get 同步路径(target_reg=None)在子 VM