mirror of
https://github.com/pjh456/OxideJS.git
synced 2026-10-26 03:31:12 +08:00
feat: rest 参数(function f(...args)),实参区收集数组
This commit is contained in:
@@ -172,6 +172,8 @@ define_opcodes! {
|
|||||||
|
|
||||||
// ── 对象 (0x63) ──
|
// ── 对象 (0x63) ──
|
||||||
CREATE_ARGUMENTS = 0x63 => "CREATE_ARGUMENTS",
|
CREATE_ARGUMENTS = 0x63 => "CREATE_ARGUMENTS",
|
||||||
|
// rest 参数数组:rd=目标寄存器,b=固定形参数(rest 之前的形参个数)。
|
||||||
|
CREATE_REST_ARRAY = 0x6E => "CREATE_REST_ARRAY",
|
||||||
|
|
||||||
// ── define 语义属性写入 (0x6D) ──
|
// ── define 语义属性写入 (0x6D) ──
|
||||||
DEFINE_PROP = 0x6D => "DEFINE_PROP",
|
DEFINE_PROP = 0x6D => "DEFINE_PROP",
|
||||||
@@ -363,6 +365,7 @@ mod tests {
|
|||||||
assert_eq!(OpCode::UNSPILL as u8, 0x0E);
|
assert_eq!(OpCode::UNSPILL as u8, 0x0E);
|
||||||
assert_eq!(OpCode::SPREAD_OBJECT as u8, 0x0F);
|
assert_eq!(OpCode::SPREAD_OBJECT as u8, 0x0F);
|
||||||
assert_eq!(OpCode::CREATE_ARGUMENTS as u8, 0x63);
|
assert_eq!(OpCode::CREATE_ARGUMENTS as u8, 0x63);
|
||||||
|
assert_eq!(OpCode::CREATE_REST_ARRAY as u8, 0x6E);
|
||||||
assert_eq!(OpCode::ADD.to_string(), "ADD");
|
assert_eq!(OpCode::ADD.to_string(), "ADD");
|
||||||
assert_eq!(OpCode::COMPOUND_MEMBER_EXP.to_string(), "COMPOUND_MEMBER_EXP");
|
assert_eq!(OpCode::COMPOUND_MEMBER_EXP.to_string(), "COMPOUND_MEMBER_EXP");
|
||||||
assert_eq!(OpCode::MOV.to_string(), "MOV");
|
assert_eq!(OpCode::MOV.to_string(), "MOV");
|
||||||
|
|||||||
@@ -19,6 +19,12 @@ impl Emitter {
|
|||||||
names.insert(bi.name.to_string());
|
names.insert(bi.name.to_string());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// rest 形参也是本函数作用域绑定,遮蔽外层同名变量。
|
||||||
|
if let Some(rest) = ¶ms.rest {
|
||||||
|
if let oxide_parser::BindingPattern::BindingIdentifier(bi) = &rest.rest.argument {
|
||||||
|
names.insert(bi.name.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
names
|
names
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -204,7 +204,8 @@ pub enum FunctionBodyContext {
|
|||||||
ClassElement,
|
ClassElement,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 参数规格:普通形参为标识符(可带默认值 initializer),解构形参用合成名 + 原始 pattern。
|
/// 参数规格:普通形参为标识符(可带默认值 initializer),解构形参用合成名 + 原始 pattern,
|
||||||
|
/// rest 形参为数组(无默认值,只能是最末形参)。
|
||||||
pub enum ParamSpec<'a> {
|
pub enum ParamSpec<'a> {
|
||||||
Identifier {
|
Identifier {
|
||||||
name: String,
|
name: String,
|
||||||
@@ -215,6 +216,9 @@ pub enum ParamSpec<'a> {
|
|||||||
pattern: &'a oxide_parser::BindingPattern<'a>,
|
pattern: &'a oxide_parser::BindingPattern<'a>,
|
||||||
initializer: Option<&'a Expression<'a>>,
|
initializer: Option<&'a Expression<'a>>,
|
||||||
},
|
},
|
||||||
|
Rest {
|
||||||
|
name: String,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ParamSpec<'_> {
|
impl ParamSpec<'_> {
|
||||||
@@ -222,6 +226,7 @@ impl ParamSpec<'_> {
|
|||||||
match self {
|
match self {
|
||||||
Self::Identifier { name, .. } => name,
|
Self::Identifier { name, .. } => name,
|
||||||
Self::Pattern { synthetic_name, .. } => synthetic_name,
|
Self::Pattern { synthetic_name, .. } => synthetic_name,
|
||||||
|
Self::Rest { name } => name,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -675,6 +680,21 @@ impl Emitter {
|
|||||||
|
|
||||||
// ── 闭包捕获分析(AST 级,时序无关)──
|
// ── 闭包捕获分析(AST 级,时序无关)──
|
||||||
|
|
||||||
|
/// 把 rest 形参的绑定模式追加为 `ParamSpec::Rest`:仅支持标识符形态(解构 rest 未支持)。
|
||||||
|
pub(crate) fn push_rest_param<'a>(
|
||||||
|
&self, argument: &'a oxide_parser::BindingPattern<'a>, out: &mut Vec<ParamSpec<'a>>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
match argument {
|
||||||
|
oxide_parser::BindingPattern::BindingIdentifier(bi) => {
|
||||||
|
out.push(ParamSpec::Rest {
|
||||||
|
name: bi.name.to_string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
_ => return Err("rest parameters with destructuring patterns not yet supported".into()),
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// 收集函数参数的绑定名(BindingIdentifier 形态)。
|
/// 收集函数参数的绑定名(BindingIdentifier 形态)。
|
||||||
pub(crate) fn extract_function_parts<'a>(
|
pub(crate) fn extract_function_parts<'a>(
|
||||||
&self, function: &'a oxide_parser::Function<'a>,
|
&self, function: &'a oxide_parser::Function<'a>,
|
||||||
@@ -697,6 +717,9 @@ impl Emitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if let Some(rest) = &function.params.rest {
|
||||||
|
self.push_rest_param(&rest.rest.argument, &mut param_specs)?;
|
||||||
|
}
|
||||||
let body_stmts: &[Statement] = if let Some(body) = &function.body { &body.statements } else { &[] };
|
let body_stmts: &[Statement] = if let Some(body) = &function.body { &body.statements } else { &[] };
|
||||||
Ok((param_specs, body_stmts))
|
Ok((param_specs, body_stmts))
|
||||||
}
|
}
|
||||||
@@ -828,12 +851,13 @@ impl Emitter {
|
|||||||
ctx.is_async = is_async;
|
ctx.is_async = is_async;
|
||||||
|
|
||||||
// length = 第一个带默认值形参之前的形参数(解构默认与标识符默认同规则);
|
// length = 第一个带默认值形参之前的形参数(解构默认与标识符默认同规则);
|
||||||
// rest 参数不在 param_specs 中(独立字段),天然不计入。
|
// rest 参数不计入 length(以 0 结尾即止)。
|
||||||
ctx.function_length = param_specs
|
ctx.function_length = param_specs
|
||||||
.iter()
|
.iter()
|
||||||
.take_while(|spec| match spec {
|
.take_while(|spec| match spec {
|
||||||
ParamSpec::Identifier { initializer, .. } => initializer.is_none(),
|
ParamSpec::Identifier { initializer, .. } => initializer.is_none(),
|
||||||
ParamSpec::Pattern { initializer, .. } => initializer.is_none(),
|
ParamSpec::Pattern { initializer, .. } => initializer.is_none(),
|
||||||
|
ParamSpec::Rest { .. } => false,
|
||||||
})
|
})
|
||||||
.count() as u32;
|
.count() as u32;
|
||||||
|
|
||||||
@@ -961,10 +985,15 @@ impl Emitter {
|
|||||||
ctx.inst(Inst::new(OpCode::RETURN, Operand::Reg(undef_reg), Operand::None, Operand::None));
|
ctx.inst(Inst::new(OpCode::RETURN, Operand::Reg(undef_reg), Operand::None, Operand::None));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 调用契约参数段只含固定形参:rest 是函数体内普通变量,不在 VM 实参传递区。
|
||||||
|
let fixed_count = param_specs
|
||||||
|
.iter()
|
||||||
|
.filter(|spec| !matches!(spec, ParamSpec::Rest { .. }))
|
||||||
|
.count() as u32;
|
||||||
let ir = ctx.assemble_ir(
|
let ir = ctx.assemble_ir(
|
||||||
oxide_ir::ParamLayout {
|
oxide_ir::ParamLayout {
|
||||||
base: param_base,
|
base: param_base,
|
||||||
count: param_specs.len() as u32,
|
count: fixed_count,
|
||||||
},
|
},
|
||||||
Some(parent_ctx),
|
Some(parent_ctx),
|
||||||
);
|
);
|
||||||
@@ -1006,6 +1035,7 @@ impl Emitter {
|
|||||||
// 内部闭包引用,需纳入捕获分析。
|
// 内部闭包引用,需纳入捕获分析。
|
||||||
self.collect_pattern_default_exprs(pattern, &mut param_defaults);
|
self.collect_pattern_default_exprs(pattern, &mut param_defaults);
|
||||||
}
|
}
|
||||||
|
ParamSpec::Rest { .. } => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ctx.own_bindings = self.collect_own_binding_names(¶m_names, body_stmts);
|
ctx.own_bindings = self.collect_own_binding_names(¶m_names, body_stmts);
|
||||||
@@ -1077,6 +1107,15 @@ impl Emitter {
|
|||||||
self.emit_default_if_undefined(reg, init, Some(name), ctx)?;
|
self.emit_default_if_undefined(reg, init, Some(name), ctx)?;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ParamSpec::Rest { name } => {
|
||||||
|
// rest 数组:从实参区收集固定形参之后的实参,绑定为普通变量。
|
||||||
|
let reg = ctx.lookup(name)?;
|
||||||
|
let fixed_count = param_specs
|
||||||
|
.iter()
|
||||||
|
.filter(|s| !matches!(s, ParamSpec::Rest { .. }))
|
||||||
|
.count() as u32;
|
||||||
|
ctx.inst(Inst::create_rest_array(Operand::Reg(reg), fixed_count));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,11 +12,6 @@ impl Emitter {
|
|||||||
fn emit_arrow_function_expression(
|
fn emit_arrow_function_expression(
|
||||||
&self, arrow: &oxide_parser::ArrowFunctionExpression, ctx: &mut CompileCtx,
|
&self, arrow: &oxide_parser::ArrowFunctionExpression, ctx: &mut CompileCtx,
|
||||||
) -> Result<u32, String> {
|
) -> Result<u32, String> {
|
||||||
// 未支持:箭头函数 rest 参数
|
|
||||||
if let Some(_rest) = &arrow.params.rest {
|
|
||||||
return Err("rest params in arrow functions not yet supported".into());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提取形参名(与函数表达式相同的形态)
|
// 提取形参名(与函数表达式相同的形态)
|
||||||
let mut param_names = Vec::new();
|
let mut param_names = Vec::new();
|
||||||
for (idx, param) in arrow.params.items.iter().enumerate() {
|
for (idx, param) in arrow.params.items.iter().enumerate() {
|
||||||
@@ -36,6 +31,9 @@ impl Emitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if let Some(rest) = &arrow.params.rest {
|
||||||
|
self.push_rest_param(&rest.rest.argument, &mut param_names)?;
|
||||||
|
}
|
||||||
|
|
||||||
// 表达式体:以 is_expression_body=true 编译;语句体:以 false 编译。
|
// 表达式体:以 is_expression_body=true 编译;语句体:以 false 编译。
|
||||||
let body_stmts = &arrow.body.statements;
|
let body_stmts = &arrow.body.statements;
|
||||||
@@ -77,6 +75,9 @@ impl Emitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if let Some(rest) = &fe.params.rest {
|
||||||
|
self.push_rest_param(&rest.rest.argument, &mut param_names)?;
|
||||||
|
}
|
||||||
|
|
||||||
let body_stmts: &[Statement] = if let Some(body) = &fe.body { &body.statements } else { &[] };
|
let body_stmts: &[Statement] = if let Some(body) = &fe.body { &body.statements } else { &[] };
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,9 @@ impl Emitter {
|
|||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if let Some(rest) = &fd.params.rest {
|
||||||
|
self.push_rest_param(&rest.rest.argument, &mut param_names)?;
|
||||||
|
}
|
||||||
let body_stmts: &[Statement] = if let Some(body) = &fd.body { &body.statements } else { &[] };
|
let body_stmts: &[Statement] = if let Some(body) = &fd.body { &body.statements } else { &[] };
|
||||||
let mut sub_module = if fd.generator {
|
let mut sub_module = if fd.generator {
|
||||||
self.compile_generator_body(¶m_names, body_stmts, ctx)?
|
self.compile_generator_body(¶m_names, body_stmts, ctx)?
|
||||||
|
|||||||
@@ -242,6 +242,7 @@ impl Inst {
|
|||||||
| OpCode::NEW_OBJECT
|
| OpCode::NEW_OBJECT
|
||||||
| OpCode::NEW_ARRAY
|
| OpCode::NEW_ARRAY
|
||||||
| OpCode::CREATE_ARGUMENTS
|
| OpCode::CREATE_ARGUMENTS
|
||||||
|
| OpCode::CREATE_REST_ARRAY
|
||||||
| OpCode::NOP => {}
|
| OpCode::NOP => {}
|
||||||
// 变量读写:LOAD_VAR/STORE_VAR/CELL_GET 读 a(None→0)
|
// 变量读写:LOAD_VAR/STORE_VAR/CELL_GET 读 a(None→0)
|
||||||
OpCode::LOAD_VAR | OpCode::STORE_VAR | OpCode::CELL_GET => {
|
OpCode::LOAD_VAR | OpCode::STORE_VAR | OpCode::CELL_GET => {
|
||||||
|
|||||||
@@ -267,6 +267,13 @@ impl Inst {
|
|||||||
Self::new(OpCode::CREATE_ARGUMENTS, dst, Operand::None, Operand::None)
|
Self::new(OpCode::CREATE_ARGUMENTS, dst, Operand::None, Operand::None)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 创建 rest 参数数组:`dst = 当前帧实参区中下标 ≥ fixed_count 的实参组成的数组`。
|
||||||
|
/// `fixed_count` 为 rest 之前的固定形参数,编码进 b 槽(Imm 单字节),运行时
|
||||||
|
/// 与 CREATE_ARGUMENTS 同源读实参区,无寄存器 use。
|
||||||
|
pub fn create_rest_array(dst: Operand, fixed_count: u32) -> Self {
|
||||||
|
Self::new(OpCode::CREATE_REST_ARRAY, dst, Operand::None, Operand::Imm(fixed_count as u16))
|
||||||
|
}
|
||||||
|
|
||||||
/// 生成器让出:`rd` 为被让出的值;恢复时 `next(v)` 的 `v` 经 reg 0 交付
|
/// 生成器让出:`rd` 为被让出的值;恢复时 `next(v)` 的 `v` 经 reg 0 交付
|
||||||
/// (与 CALL 同协议,emit 用 `LOAD_VAR(None)` 读取)。
|
/// (与 CALL 同协议,emit 用 `LOAD_VAR(None)` 读取)。
|
||||||
pub fn yield_value(src: Operand) -> Self {
|
pub fn yield_value(src: Operand) -> Self {
|
||||||
|
|||||||
@@ -196,6 +196,43 @@ impl Vm {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 创建 rest 参数数组:实参区下标 ≥ `fixed_count` 的实参收集为数组存入 `rd`。
|
||||||
|
///
|
||||||
|
/// # 步骤
|
||||||
|
/// 1. 读当前帧(或 inline 同步调用)的实参区基址与个数。
|
||||||
|
/// 2. 实参个数超出固定形参数的部分作为数组元素写入。
|
||||||
|
///
|
||||||
|
/// # 边界与前提
|
||||||
|
/// - 实参来源与 CREATE_ARGUMENTS 相同(push_bytecode_frame / inline 路径写入 spill 栈)。
|
||||||
|
/// - 实参不足 `fixed_count` 时数组为空。
|
||||||
|
pub(crate) fn dispatch_create_rest_array(&mut self, rd: usize, fixed_count: usize) -> Result<(), String> {
|
||||||
|
let (base, count) = match self.frames.last() {
|
||||||
|
Some(frame) => (frame.arguments_base, frame.arguments_count),
|
||||||
|
None => (self.inline_args_base, self.inline_args_count),
|
||||||
|
};
|
||||||
|
let n = count as usize;
|
||||||
|
let rest_len = n.saturating_sub(fixed_count);
|
||||||
|
let proto_ptr = self.session.builtin_world().array_proto.as_ptr() as *mut JsObject;
|
||||||
|
let bump = self.epoch.bump();
|
||||||
|
let obj_ptr = self.alloc_object(JsObject::new_array(
|
||||||
|
EMPTY_SHAPE_ID,
|
||||||
|
JsValue::from_js_object(proto_ptr),
|
||||||
|
rest_len,
|
||||||
|
bump,
|
||||||
|
));
|
||||||
|
let obj = unsafe { &mut *obj_ptr };
|
||||||
|
for i in 0..rest_len {
|
||||||
|
let val = self
|
||||||
|
.spill_stack
|
||||||
|
.get(base as usize + fixed_count + i)
|
||||||
|
.copied()
|
||||||
|
.unwrap_or(JsValue::undefined());
|
||||||
|
obj.set_prop_at(i as u32, val);
|
||||||
|
}
|
||||||
|
self.regs[rd] = JsValue::object(obj_ptr as *mut u8);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub(crate) fn dispatch_new_array(&mut self, rd: usize, instr: u32) {
|
pub(crate) fn dispatch_new_array(&mut self, rd: usize, instr: u32) {
|
||||||
let n = opcode::imm16(instr) as usize;
|
let n = opcode::imm16(instr) as usize;
|
||||||
|
|||||||
@@ -1330,6 +1330,11 @@ impl Vm {
|
|||||||
self.dispatch_create_arguments(rd)?;
|
self.dispatch_create_arguments(rd)?;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
OpCode::CREATE_REST_ARRAY => {
|
||||||
|
let fixed_count = opcode::b(instr) as usize;
|
||||||
|
self.dispatch_create_rest_array(rd, fixed_count)?;
|
||||||
|
}
|
||||||
|
|
||||||
OpCode::NEW_ARRAY => {
|
OpCode::NEW_ARRAY => {
|
||||||
self.dispatch_new_array(rd, instr);
|
self.dispatch_new_array(rd, instr);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,74 @@
|
|||||||
|
//! rest 参数集成测试:实参收集、length 交互、与 spread/箭头/闭包组合。
|
||||||
|
//! 每个用例独立编译执行,断言顶层表达式结果。
|
||||||
|
|
||||||
|
use oxide_compiler::compiler::Compiler;
|
||||||
|
use oxide_parser::Allocator;
|
||||||
|
use oxide_types::value::JsValue;
|
||||||
|
use oxide_vm::vm::Vm;
|
||||||
|
|
||||||
|
fn eval(source: &str) -> JsValue {
|
||||||
|
let allocator = Allocator::default();
|
||||||
|
let program = oxide_parser::parse(&allocator, source).expect("parse failed");
|
||||||
|
let module = Compiler::new().compile(&program).expect("compile failed");
|
||||||
|
let mut vm = Vm::new();
|
||||||
|
vm.run(&module).expect("vm run failed")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn eval_int(source: &str) -> i32 {
|
||||||
|
let v = eval(source);
|
||||||
|
if v.is_int() {
|
||||||
|
v.as_int()
|
||||||
|
} else if v.is_double() {
|
||||||
|
v.as_double() as i32
|
||||||
|
} else {
|
||||||
|
panic!("expected number, got {v}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn eval_str(source: &str) -> String {
|
||||||
|
let v = eval(source);
|
||||||
|
if v.is_string() {
|
||||||
|
unsafe { oxide_runtime_api::string_data(v) }.to_string()
|
||||||
|
} else {
|
||||||
|
format!("{v}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rest_collects_remaining_arguments() {
|
||||||
|
assert_eq!(eval_int("function f(...args){return args.length} f(1,2,3)"), 3);
|
||||||
|
assert_eq!(eval_str("function f(...args){return args.join(',')} f(1,2,3)"), "1,2,3");
|
||||||
|
assert_eq!(eval_int("function f(...args){return args.length} f()"), 0);
|
||||||
|
assert_eq!(eval_int("function f(...args){return args[0]+args[1]} f(7,8)"), 15);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rest_after_fixed_params_starts_at_fixed_count() {
|
||||||
|
assert_eq!(eval_str("function f(a,...rest){return a+':'+rest.join(',')} f(1,2,3)"), "1:2,3");
|
||||||
|
assert_eq!(eval_int("function f(a,b,...rest){return rest.length} f(1,2,3,4,5)"), 3);
|
||||||
|
assert_eq!(eval_int("function f(a,b,...rest){return rest.length} f(1)"), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rest_is_a_mutable_array() {
|
||||||
|
assert_eq!(eval_str("function f(...args){return Array.isArray(args)&&'y'} f(1)"), "y");
|
||||||
|
assert_eq!(eval_str("function f(a,...r){r.push(4); return r.join(',')} f(1,2,3)"), "2,3,4");
|
||||||
|
assert_eq!(eval_str("function f(...a){a[0]=99; return a.join(',')} f(1,2,3)"), "99,2,3");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rest_combines_with_spread_and_arrow() {
|
||||||
|
assert_eq!(eval_int("function f(...rest){return rest.length} f(...[1,2,3])"), 3);
|
||||||
|
assert_eq!(eval_int("var f=(...args)=>args.length; f(1,2,3)"), 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rest_captured_by_closure() {
|
||||||
|
assert_eq!(eval_int("function f(...a){return ()=>a.length} f(1,2,3)()"), 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rest_does_not_count_toward_function_length() {
|
||||||
|
assert_eq!(eval("function f(...r){}; f.length"), JsValue::int(0));
|
||||||
|
assert_eq!(eval("function f(a,b=1,...r){}; f.length"), JsValue::int(1));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user