mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-23 18:21:13 +08:00
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:
@@ -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 与前置实参的新包装函数,
|
||||
|
||||
@@ -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 抛出的原生异常须原样传播,
|
||||
|
||||
@@ -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
@@ -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 或
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user