feat: 支持箭头函数/模板字符串/let/const/delete/instanceof 语法

This commit is contained in:
2026-06-08 19:55:05 +08:00
parent b0c6532e2c
commit e1ae9387c2
15 changed files with 1379 additions and 91 deletions
+198 -11
View File
@@ -297,12 +297,19 @@ impl Vm {
}
/// Create a function JsObject for a BytecodeFunc constant.
fn create_function_object(&mut self, sub_idx: u32) -> JsValue {
/// When `is_arrow` is true, captures the current `this` (regs[254])
/// for lexical this binding at call time (D-01).
fn create_function_object(&mut self, sub_idx: u32, is_arrow: bool) -> JsValue {
let func_proto_ptr = self.kernel.builtin_world().function_proto.as_ptr() as *mut JsObject;
let proto_val = JsValue::from_js_object(func_proto_ptr);
let mut obj = JsObject::new_empty(EMPTY_SHAPE_ID, proto_val);
obj.set_function(true);
obj.set_sub_module_index(sub_idx);
if is_arrow {
obj.set_arrow(true);
// Capture lexical `this` from the enclosing scope (regs[254]).
obj.set_captured_this(self.regs[254]);
}
let obj_ptr = self.epoch.alloc(obj);
JsValue::object(obj_ptr as *mut u8)
}
@@ -319,14 +326,24 @@ impl Vm {
Constant::Boolean(b) => JsValue::bool(*b),
Constant::Null => JsValue::null(),
Constant::Undefined => JsValue::undefined(),
Constant::BytecodeFunc(idx) => self.create_function_object(*idx),
Constant::BytecodeFunc(idx) => {
// 1-indexed: idx 1 = sub_modules[0], idx 0 = sentinel
let sub_idx = *idx as usize;
let is_arrow = if sub_idx > 0 && sub_idx <= module.sub_modules.len() {
module.sub_modules[sub_idx - 1].is_arrow
} else {
false
};
self.create_function_object(*idx, is_arrow)
}
Constant::RegExp(pattern, flags) => {
let pat_si = self.kernel.string_forge().intern(pattern).0;
let flags_si = self.kernel.string_forge().intern(flags).0;
let pat_val = JsValue::string(pat_si, 0);
let flags_val = JsValue::string(flags_si, 0);
let native_fn = self.kernel.builtin_world().regexp_constructor.as_ptr() as *mut JsObject;
let native_fn =
self.kernel.builtin_world().regexp_constructor.as_ptr() as *mut JsObject;
let ctor = unsafe { &*native_fn };
let ctor_fn = ctor.native_fn();
if ctor_fn.is_none() {
@@ -338,7 +355,8 @@ impl Vm {
self.regs[0] = JsValue::undefined();
self.regs[1] = pat_val;
self.regs[2] = flags_val;
let func: crate::native::NativeFn = unsafe { std::mem::transmute(ctor_fn.unwrap()) };
let func: crate::native::NativeFn =
unsafe { std::mem::transmute(ctor_fn.unwrap()) };
let result = func(self, &[0, 1, 2]);
self.regs[0] = saved_0;
self.regs[1] = saved_1;
@@ -347,7 +365,7 @@ impl Vm {
Ok(val) => val,
Err(_) => JsValue::undefined(),
}
},
}
})
.collect()
}
@@ -654,6 +672,16 @@ impl Vm {
}
OpCode::STORE_VAR => {
if b != 0 {
// const guard: check if already initialized
if !self.regs[rd].is_undefined() {
throw_err!(
self,
TypeError,
"Assignment to constant variable"
);
}
}
self.regs[rd] = self.regs[a];
}
@@ -707,6 +735,7 @@ impl Vm {
let sub_constants = self.sub_modules[sub_idx].constants.clone();
let sub_builtin_count =
self.sub_modules[sub_idx].builtin_reg_map.len();
let sub_is_arrow = self.sub_modules[sub_idx].is_arrow;
// Copy args to regs[sub_builtin_count..sub_builtin_count+n_args]
// (builtins occupy regs[0..sub_builtin_count-1])
@@ -714,8 +743,13 @@ impl Vm {
let src_reg = first_arg_reg.wrapping_add(i as u8) as usize;
self.regs[sub_builtin_count + i] = self.regs[src_reg];
}
// Set this (reg 254 convention)
self.regs[254] = callee;
// Set this (reg 254 convention).
// Arrow functions use captured lexical `this` (D-01).
self.regs[254] = if sub_is_arrow {
obj.captured_this()
} else {
callee
};
// Save current bytecode/constants
self.saved_bytecode_stack
@@ -745,7 +779,15 @@ impl Vm {
Constant::Null => JsValue::null(),
Constant::Undefined => JsValue::undefined(),
Constant::BytecodeFunc(idx) => {
self.create_function_object(*idx)
let idx_usize = *idx as usize;
let nested_is_arrow = if idx_usize > 0
&& idx_usize <= self.sub_modules.len()
{
self.sub_modules[idx_usize - 1].is_arrow
} else {
false
};
self.create_function_object(*idx, nested_is_arrow)
}
Constant::RegExp(_, _) => JsValue::undefined(),
})
@@ -829,6 +871,14 @@ impl Vm {
"NEW_EXPRESSION: constructor is not a function"
);
}
// Arrow functions cannot be used as constructors (D-03)
if ctor_obj.is_arrow() {
throw_err!(
self,
TypeError,
"arrow functions cannot be used as constructors"
);
}
// Read extension word for arg_count
let ext = self.bytecode[self.pc];
@@ -1196,6 +1246,143 @@ impl Vm {
self.regs[rd] = JsValue::undefined();
}
OpCode::TEMPLATE_STR => {
// Read header ext word: (segment_count << 16) | (total_len_hint & 0xFFFF)
let header = self.bytecode[self.pc];
self.pc += 1;
let segment_count = (header >> 16) as usize;
let len_hint = (header & 0xFFFF) as usize;
// Build result string
let mut result = String::with_capacity(len_hint.max(16));
for _ in 0..segment_count {
let seg = self.bytecode[self.pc];
self.pc += 1;
if (seg >> 31) == 1 {
// Expression: register value
let reg = (seg & 0x7F) as u8;
let val = self.regs[reg as usize];
let s = if val.is_string() {
self.kernel()
.string_forge()
.lookup(val.as_string_index())
.unwrap_or_default()
} else {
format!("{}", val)
};
result.push_str(&s);
} else {
// Quasi: constant string
let const_idx = (seg & 0x7FFF_FFFF) as usize;
if const_idx < self.constants.len() {
let val = self.constants[const_idx];
if val.is_string() {
let s = self
.kernel()
.string_forge()
.lookup(val.as_string_index())
.unwrap_or_default();
result.push_str(&s);
}
}
}
}
let si = self.kernel.string_forge().intern(&result).0;
self.regs[rd] = JsValue::string(si, 0);
}
OpCode::DELETE_PROP_STATIC => {
let obj_val = self.regs[a];
if !obj_val.is_object() {
self.regs[rd] = JsValue::bool(true);
} else {
let obj_ptr = obj_val.as_js_object_ptr();
let obj = unsafe { &mut *obj_ptr };
let ext = self.bytecode[self.pc];
self.pc += 1;
let const_idx = ext as usize;
let prop_name_val = self.constants[const_idx];
if !prop_name_val.is_string() {
self.regs[rd] = JsValue::bool(true);
continue;
}
let prop_name_si = prop_name_val.as_string_index();
if let Some(pos) = self.kernel.shape_forge().lookup_position(
obj.shape_id(),
prop_name_si,
) {
obj.set_prop_at(pos, JsValue::undefined());
}
self.regs[rd] = JsValue::bool(true);
}
}
OpCode::DELETE_PROP_DYNAMIC => {
let obj_val = self.regs[a];
if !obj_val.is_object() {
self.regs[rd] = JsValue::bool(true);
} else {
let key_val = self.regs[b];
let prop_name_si = if key_val.is_string() {
key_val.as_string_index()
} else {
let s = crate::coercion::to_string(
self.kernel.string_forge().as_ref(),
key_val,
);
self.kernel.string_forge().intern(&s).0
};
let obj_ptr = obj_val.as_js_object_ptr();
let obj = unsafe { &mut *obj_ptr };
if let Some(pos) = self.kernel.shape_forge().lookup_position(
obj.shape_id(),
prop_name_si,
) {
obj.set_prop_at(pos, JsValue::undefined());
}
self.regs[rd] = JsValue::bool(true);
}
}
OpCode::INSTANCEOF => {
let lhs_val = self.regs[a];
let rhs_val = self.regs[b];
if !rhs_val.is_object() {
throw_err!(self, TypeError, "INSTANCEOF right-hand side is not callable");
}
if !lhs_val.is_object() {
self.regs[rd] = JsValue::bool(false);
continue;
}
let rhs_obj = unsafe { &*rhs_val.as_js_object_ptr() };
let proto_si = self.kernel.string_forge().intern("prototype").0;
let ctor_proto = self.resolve_property(rhs_obj, proto_si);
let ctor_proto_ptr = match ctor_proto {
Some(v) if v.is_object() => v.as_js_object_ptr(),
_ => {
self.regs[rd] = JsValue::bool(false);
continue;
}
};
let mut proto = unsafe { &*lhs_val.as_js_object_ptr() }.proto();
loop {
if !proto.is_object() {
self.regs[rd] = JsValue::bool(false);
break;
}
let proto_ptr = proto.as_js_object_ptr();
if proto_ptr == ctor_proto_ptr {
self.regs[rd] = JsValue::bool(true);
break;
}
proto = unsafe { &*proto_ptr }.proto();
}
}
OpCode::IN => {
let key_val = self.regs[a];
let obj_ptr = self.regs[b].as_object_ptr() as *mut JsObject;
@@ -1581,11 +1768,11 @@ impl Vm {
OpCode::FOR_OF_INIT => {}
OpCode::FOR_OF_NEXT => {},
OpCode::FOR_OF_NEXT => {}
OpCode::FOR_OF_DONE => {},
OpCode::FOR_OF_DONE => {}
OpCode::FOR_OF_CLOSE => {},
OpCode::FOR_OF_CLOSE => {}
OpCode::THROW => {
let exc_value = self.regs[rd];
+125
View File
@@ -0,0 +1,125 @@
use oxide_compiler::compiler::Compiler;
use oxide_parser::Allocator;
use oxide_types::value::JsValue;
use oxide_vm::vm::Vm;
fn eval(source: &str) -> String {
let allocator = Allocator::default();
let program = match oxide_parser::parse(&allocator, source) {
Ok(p) => p,
Err(e) => return format!("parse error: {}", e[0].message),
};
let module = match Compiler::new().compile(&program) {
Ok(m) => m,
Err(e) => return format!("compile error: {}", e),
};
let mut vm = Vm::new();
match vm.run(&module) {
Ok(result) => format!("{}", result),
Err(e) => format!("vm error: {}", e),
}
}
fn eval_val(source: &str) -> (Vm, Result<JsValue, String>) {
let allocator = Allocator::default();
let program = match oxide_parser::parse(&allocator, source) {
Ok(p) => p,
Err(e) => return (Vm::new(), Err(format!("parse error: {}", e[0].message))),
};
let module = match Compiler::new().compile(&program) {
Ok(m) => m,
Err(e) => return (Vm::new(), Err(format!("compile error: {}", e))),
};
let mut vm = Vm::new();
let result = vm.run(&module);
(vm, result)
}
fn to_str(vm: &Vm, val: JsValue) -> String {
if val.is_string() {
vm.kernel()
.string_forge()
.lookup(val.as_string_index())
.unwrap_or_default()
} else {
format!("{}", val)
}
}
// -- Arrow function integration tests --
#[test]
fn arrow_expression_body_returns_value() {
assert_eq!(eval("(() => 42)()"), "42");
}
#[test]
fn arrow_block_body_returns_value() {
assert_eq!(eval("((a,b) => { return a+b; })(3,4)"), "7");
}
#[test]
fn arrow_single_param_no_parens() {
assert_eq!(eval("(x => x * 2)(5)"), "10");
}
#[test]
fn arrow_multiple_params_with_parens() {
assert_eq!(eval("((x, y) => x + y)(3, 7)"), "10");
}
#[test]
fn arrow_stored_in_variable_callable() {
assert_eq!(eval("var f = () => 99; f()"), "99");
}
#[test]
fn arrow_stored_in_variable_with_params() {
assert_eq!(eval("var add = (a,b) => a+b; add(10,20)"), "30");
}
#[test]
fn arrow_not_constructable_throws() {
let result = eval("new (() => {})");
assert!(
result.contains("TypeError") || result.contains("arrow"),
"Expected arrow constructor TypeError, got: {}",
result
);
}
#[test]
fn arrow_no_params_string_result() {
let (vm, result) = eval_val("(() => 'hello')()");
let s = to_str(&vm, result.unwrap());
assert_eq!(s, "hello");
}
#[test]
fn arrow_expression_body_implicit_return() {
assert_eq!(eval("(() => 1 + 2)()"), "3");
}
#[test]
fn arrow_block_body_needs_explicit_return() {
assert_eq!(eval("((a,b) => { a+b; })(3,4)"), "undefined");
}
#[test]
fn arrow_at_global_scope_this_is_undefined() {
assert_eq!(eval("(() => this)()"), "undefined");
}
#[test]
fn multiple_arrows_in_same_scope() {
assert_eq!(eval("var a = () => 1, b = () => 2; a() + b()"), "3");
}
// NOTE: Arrow functions capturing lexical `this` from enclosing functions
// require nested function call support (sub_module flattening), which is
// a pre-existing limitation. These tests will be enabled in a future phase:
//
// fn arrow_captures_enclosing_this() {
// // const o = {x:10, f:function(){ return (() => this.x)(); }}; o.f()
// // Expected: 10
// }
@@ -9,7 +9,7 @@ fn make_vm() -> Vm {
fn eval(source: &str) -> Result<JsValue, String> {
let allocator = oxide_parser::Allocator::default();
let program = oxide_parser::parse(&allocator, source).map_err(|e| format!("Parse: {}", e))?;
let program = oxide_parser::parse(&allocator, source).map_err(|e| format!("Parse: {:?}", e))?;
let module = Compiler::new().compile(&program).map_err(|e| format!("Compile: {}", e))?;
let mut vm = make_vm();
vm.run(&module)
+62
View File
@@ -0,0 +1,62 @@
use oxide_compiler::compiler::Compiler;
use oxide_types::value::JsValue;
use oxide_vm::vm::Vm;
fn eval(source: &str) -> Result<JsValue, String> {
let allocator = oxide_parser::Allocator::default();
let program = oxide_parser::parse(&allocator, source).map_err(|e| format!("Parse: {:?}", e))?;
let module = Compiler::new().compile(&program).map_err(|e| format!("Compile: {}", e))?;
let mut vm = Vm::new();
vm.run(&module)
}
#[test]
fn let_block_scoping_outer_unchanged() {
let result = eval("let x = 1; { let x = 2; } x").unwrap();
assert_eq!(result.as_int(), 1);
}
#[test]
fn const_reassignment_throws() {
let result = eval("const x = 1; x = 2");
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.contains("TypeError") || err.contains("constant"), "got: {}", err);
}
#[test]
fn var_block_not_isolated() {
let result = eval("var x = 5; { x = 10; } x").unwrap();
assert_eq!(result.as_int(), 10);
}
#[test]
fn delete_existing_property() {
let result = eval("var o = {a: 1}; delete o.a; o.a").unwrap();
assert!(result.is_undefined());
}
#[test]
fn delete_returns_true() {
let result = eval("var o = {a: 1}; delete o.a").unwrap();
assert!(result.as_bool());
assert!(result.as_bool());
}
#[test]
fn instanceof_array() {
let result = eval("[] instanceof Array").unwrap();
assert!(result.as_bool());
}
#[test]
fn instance_not_array() {
let result = eval("({}) instanceof Array").unwrap();
assert!(!result.as_bool());
}
#[test]
fn void_returns_undefined() {
let result = eval("void 0").unwrap();
assert!(result.is_undefined());
}
+118
View File
@@ -0,0 +1,118 @@
use oxide_compiler::compiler::Compiler;
use oxide_parser::Allocator;
use oxide_types::value::JsValue;
use oxide_vm::vm::Vm;
fn eval(source: &str) -> String {
let allocator = Allocator::default();
let program = match oxide_parser::parse(&allocator, source) {
Ok(p) => p,
Err(e) => return format!("parse error: {}", e[0].message),
};
let module = match Compiler::new().compile(&program) {
Ok(m) => m,
Err(e) => return format!("compile error: {}", e),
};
let mut vm = Vm::new();
match vm.run(&module) {
Ok(result) => format!("{}", result),
Err(e) => format!("vm error: {}", e),
}
}
fn eval_val(source: &str) -> (Vm, Result<JsValue, String>) {
let allocator = Allocator::default();
let program = match oxide_parser::parse(&allocator, source) {
Ok(p) => p,
Err(e) => return (Vm::new(), Err(format!("parse error: {}", e[0].message))),
};
let module = match Compiler::new().compile(&program) {
Ok(m) => m,
Err(e) => return (Vm::new(), Err(format!("compile error: {}", e))),
};
let mut vm = Vm::new();
let result = vm.run(&module);
(vm, result)
}
fn to_str(vm: &Vm, val: JsValue) -> String {
if val.is_string() {
vm.kernel()
.string_forge()
.lookup(val.as_string_index())
.unwrap_or_default()
} else {
format!("{}", val)
}
}
// -- Template literal tests --
#[test]
fn template_no_expressions() {
let (vm, result) = eval_val("`hello`");
assert_eq!(to_str(&vm, result.unwrap()), "hello");
}
#[test]
fn template_single_expression() {
let (vm, result) = eval_val("const name = 'world'; `hello ${name}`");
assert_eq!(to_str(&vm, result.unwrap()), "hello world");
}
#[test]
fn template_multiple_expressions() {
let (vm, result) = eval_val("`a${1}b${2}c`");
assert_eq!(to_str(&vm, result.unwrap()), "a1b2c");
}
#[test]
fn template_expression_only() {
let (vm, result) = eval_val("const x = 'foo', y = 'bar'; `${x}${y}`");
assert_eq!(to_str(&vm, result.unwrap()), "foobar");
}
#[test]
fn template_empty() {
let (vm, result) = eval_val("``");
assert_eq!(to_str(&vm, result.unwrap()), "");
}
#[test]
fn template_with_numbers() {
let (vm, result) = eval_val("`value is ${42}`");
assert_eq!(to_str(&vm, result.unwrap()), "value is 42");
}
#[test]
fn template_numeric_expression() {
let (vm, result) = eval_val("`${1 + 1}`");
assert_eq!(to_str(&vm, result.unwrap()), "2");
}
// -- Template tagging tests (basic) --
#[test]
fn tagged_template_basic() {
// Use Math.max as a simple native tag function to verify the CALL dispatch works.
// Math.max(cooked_array, raw_array, 42) should return 42 (the max of the args).
// This avoids bytecode function call complexity.
let result = eval("Math.max`hello ${42} world`");
// Math.max on the args should work - just verify it doesn't crash
assert!(
!result.starts_with("vm error:"),
"Tagged template should not crash, got: {}",
result
);
}
#[test]
fn tagged_template_compiles_no_error() {
// Verify that tagged templates compile without error (even if the tag
// function behavior isn't fully tested)
let result = eval("function t(s,v){ return s[0]+v; } t`x${1}`");
assert!(
!result.starts_with("compile error:"),
"Tagged template should compile"
);
}