feat: 函数 length 属性(字节码函数/bind 包装器)+ getter/setter name 前缀
This commit is contained in:
@@ -121,6 +121,7 @@ impl Vm {
|
||||
let needs_home_object = sub.needs_home_object;
|
||||
let upvalue_captures = sub.upvalue_captures.clone();
|
||||
let function_name = sub.function_name.clone();
|
||||
let function_length = sub.function_length;
|
||||
let result = self.create_function_object(
|
||||
sub_idx,
|
||||
is_arrow,
|
||||
@@ -130,13 +131,18 @@ impl Vm {
|
||||
);
|
||||
// 函数名推断:emit 端在变量声明/对象属性赋值点设置 function_name。
|
||||
let func_obj = unsafe { &mut *result.as_js_object_ptr() };
|
||||
let length_si = self.kernel_core.perm_interner().intern("length").0;
|
||||
let name_si = self.kernel_core.perm_interner().intern("name").0;
|
||||
// length/name 描述符均为不可写、不可枚举、可配置(SetFunctionLength /
|
||||
// SetFunctionName 语义);length 先于 name 定义保证属性序 [prototype, length, name]。
|
||||
let attrs = PropAttributes::new(false, false, true);
|
||||
let length_val = JsValue::int(function_length as i32);
|
||||
self.define_data_property(func_obj, length_si, length_val, attrs)?;
|
||||
let name_val = function_name
|
||||
.as_deref()
|
||||
.map(|n| self.new_string(n))
|
||||
.unwrap_or_else(|| self.new_string(""));
|
||||
// name 属性描述符:不可写、不可枚举、可配置(SetFunctionName 语义)。
|
||||
self.define_data_property(func_obj, name_si, name_val, PropAttributes::new(false, false, true))?;
|
||||
self.define_data_property(func_obj, name_si, name_val, attrs)?;
|
||||
if !upvalue_captures.is_empty() {
|
||||
// 链式捕获(parent_uv_idx):从父闭包(创建者)的 upvalues 取 cell。
|
||||
let parent_upvalues: Vec<*mut Cell> = match self.current_callee() {
|
||||
|
||||
@@ -237,9 +237,16 @@ impl Vm {
|
||||
&mut self, setter: JsValue, receiver: JsValue, val: JsValue, use_frame_push: bool,
|
||||
) -> Result<(), String> {
|
||||
vm_trace!("call_or_push_setter: frame_push={}", use_frame_push);
|
||||
// native setter 抛错经 call_function_sync 以 String 返回(未 unwind)——
|
||||
// 在此经 raise_call_error 恢复为可捕获的 JS 异常(与 getter 路径对称)。
|
||||
let call_native = |vm: &mut Self| -> Result<(), String> {
|
||||
if let Err(err) = vm.call_function_sync(setter, receiver, &[val]) {
|
||||
vm.raise_call_error(&err)?;
|
||||
}
|
||||
Ok(())
|
||||
};
|
||||
if !use_frame_push {
|
||||
self.call_function_sync(setter, receiver, &[val])?;
|
||||
return Ok(());
|
||||
return call_native(self);
|
||||
}
|
||||
if !setter.is_object() {
|
||||
return self.raise_type_error("setter is not callable");
|
||||
@@ -249,8 +256,7 @@ impl Vm {
|
||||
return self.raise_type_error("setter is not callable");
|
||||
}
|
||||
if setter_obj.native_fn().is_some() {
|
||||
self.call_function_sync(setter, receiver, &[val])?;
|
||||
return Ok(());
|
||||
return call_native(self);
|
||||
}
|
||||
self.push_bytecode_frame(
|
||||
setter,
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
//! 函数 length/name 属性集成测试:普通字节码函数、箭头函数、bind 包装器、
|
||||
//! getter/setter 前缀。每个用例独立编译执行,断言顶层表达式结果。
|
||||
|
||||
use oxide_bytecode::module::CompiledModule;
|
||||
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");
|
||||
run(&module)
|
||||
}
|
||||
|
||||
fn run(module: &CompiledModule) -> JsValue {
|
||||
let mut vm = Vm::new();
|
||||
vm.run(module).expect("vm run failed")
|
||||
}
|
||||
|
||||
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 bytecode_function_length_counts_formals_before_first_default() {
|
||||
assert_eq!(eval("function f(a,b,c){}; f.length"), JsValue::int(3));
|
||||
assert_eq!(eval("function f(a,b=1,c){}; f.length"), JsValue::int(1));
|
||||
assert_eq!(eval("function f(...r){}; f.length"), JsValue::int(0));
|
||||
assert_eq!(eval("var f = function named(a,b,c){}; f.length"), JsValue::int(3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn arrow_function_length_matches_formals() {
|
||||
assert_eq!(eval("var f=(a,b)=>a+b; f.length"), JsValue::int(2));
|
||||
assert_eq!(eval("var f=(a,b,c)=>a+b+c; f.length"), JsValue::int(3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn class_method_and_accessor_names() {
|
||||
assert_eq!(eval("class C { m(a,b){} }; C.prototype.m.length"), JsValue::int(2));
|
||||
assert_eq!(
|
||||
eval_str("class C { get x(){} set x(v){} }; Object.getOwnPropertyDescriptor(C.prototype,'x').get.name"),
|
||||
"get x"
|
||||
);
|
||||
assert_eq!(
|
||||
eval_str("class C { get x(){} set x(v){} }; Object.getOwnPropertyDescriptor(C.prototype,'x').set.name"),
|
||||
"set x"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn object_literal_accessor_names() {
|
||||
assert_eq!(
|
||||
eval_str("var o={ get x(){}, set x(v){} }; Object.getOwnPropertyDescriptor(o,'x').get.name"),
|
||||
"get x"
|
||||
);
|
||||
assert_eq!(
|
||||
eval_str("var o={ get x(){}, set x(v){} }; Object.getOwnPropertyDescriptor(o,'x').set.name"),
|
||||
"set x"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bind_wrapper_length_is_target_length_minus_bound_args() {
|
||||
assert_eq!(eval("function f(a,b){}; f.bind(null,1).length"), JsValue::int(1));
|
||||
assert_eq!(eval("function f(){}; f.bind(null,1,2,3).length"), JsValue::int(0));
|
||||
assert_eq!(eval("function f(a,b,c){}; f.bind(null).length"), JsValue::int(3));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bind_wrapper_name_has_bound_prefix() {
|
||||
assert_eq!(eval_str("function f(a){}; f.bind(null).name"), "bound f");
|
||||
assert_eq!(eval_str("(function(){}).bind(null).name"), "bound ");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bind_wrapper_length_supports_infinity_and_large_values() {
|
||||
assert_eq!(
|
||||
eval("function f(){}; Object.defineProperty(f,'length',{value:Infinity}); f.bind().length === Infinity"),
|
||||
JsValue::bool(true)
|
||||
);
|
||||
assert_eq!(
|
||||
eval("function f(){}; Object.defineProperty(f,'length',{value:2147483648}); f.bind().length"),
|
||||
JsValue::float(2147483648.0)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bind_wrapper_caller_and_arguments_are_poisoned() {
|
||||
assert_eq!(
|
||||
eval_str("var b=(function(){}).bind({}); try { b.caller; 'no' } catch(e) { e instanceof TypeError ? 'throws' : 'wrong' }"),
|
||||
"throws"
|
||||
);
|
||||
assert_eq!(
|
||||
eval_str("var b=(function(){}).bind({}); try { b.caller = 1; 'no' } catch(e) { e instanceof TypeError ? 'throws' : 'wrong' }"),
|
||||
"throws"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn length_and_name_descriptors_are_non_writable_non_enumerable_configurable() {
|
||||
assert_eq!(
|
||||
eval_str("function f(){}; var d=Object.getOwnPropertyDescriptor(f,'length'); d.writable===false && d.enumerable===false && d.configurable===true"),
|
||||
"true"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn length_comes_before_name_in_property_order() {
|
||||
assert_eq!(
|
||||
eval_str("function f(a,b,c){}; var p=Object.getOwnPropertyNames(f); p[p.indexOf('length')] < p[p.indexOf('name')]"),
|
||||
"true"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn anonymous_function_name_is_empty_string() {
|
||||
assert_eq!(eval_str("(function(){}).name"), "");
|
||||
}
|
||||
Reference in New Issue
Block a user