diff --git a/crates/oxide_compiler/src/opcode.rs b/crates/oxide_compiler/src/opcode.rs index fc5f55f5..22761bb7 100644 --- a/crates/oxide_compiler/src/opcode.rs +++ b/crates/oxide_compiler/src/opcode.rs @@ -42,6 +42,8 @@ pub enum OpCode { JMP = 0x20, JMP_IF_FALSE = 0x21, JMP_IF_TRUE = 0x22, + FOR_OF_INIT = 0x23, + FOR_OF_NEXT = 0x24, // -- Update (0x25-0x28) -- INC_PRE = 0x25, @@ -61,6 +63,8 @@ pub enum OpCode { TRY_END = 0x33, TRY_FINALLY_BEGIN = 0x34, TRY_FINALLY_END = 0x35, + FOR_OF_DONE = 0x36, + FOR_OF_CLOSE = 0x37, // -- Variable (0x30-0x32) -- LOAD_VAR = 0x30, @@ -176,6 +180,8 @@ impl TryFrom for OpCode { 0x20 => Ok(OpCode::JMP), 0x21 => Ok(OpCode::JMP_IF_FALSE), 0x22 => Ok(OpCode::JMP_IF_TRUE), + 0x23 => Ok(OpCode::FOR_OF_INIT), + 0x24 => Ok(OpCode::FOR_OF_NEXT), 0x25 => Ok(OpCode::INC_PRE), 0x26 => Ok(OpCode::INC_POST), 0x27 => Ok(OpCode::DEC_PRE), @@ -193,6 +199,8 @@ impl TryFrom for OpCode { 0x33 => Ok(OpCode::TRY_END), 0x34 => Ok(OpCode::TRY_FINALLY_BEGIN), 0x35 => Ok(OpCode::TRY_FINALLY_END), + 0x36 => Ok(OpCode::FOR_OF_DONE), + 0x37 => Ok(OpCode::FOR_OF_CLOSE), 0x40 => Ok(OpCode::CALL), 0x41 => Ok(OpCode::RETURN), 0x42 => Ok(OpCode::CALL_NATIVE), @@ -262,6 +270,8 @@ impl fmt::Display for OpCode { OpCode::JMP => "JMP", OpCode::JMP_IF_FALSE => "JMP_IF_FALSE", OpCode::JMP_IF_TRUE => "JMP_IF_TRUE", + OpCode::FOR_OF_INIT => "FOR_OF_INIT", + OpCode::FOR_OF_NEXT => "FOR_OF_NEXT", OpCode::INC_PRE => "INC_PRE", OpCode::INC_POST => "INC_POST", OpCode::DEC_PRE => "DEC_PRE", @@ -276,6 +286,8 @@ impl fmt::Display for OpCode { OpCode::TRY_END => "TRY_END", OpCode::TRY_FINALLY_BEGIN => "TRY_FINALLY_BEGIN", OpCode::TRY_FINALLY_END => "TRY_FINALLY_END", + OpCode::FOR_OF_DONE => "FOR_OF_DONE", + OpCode::FOR_OF_CLOSE => "FOR_OF_CLOSE", OpCode::LOAD_VAR => "LOAD_VAR", OpCode::STORE_VAR => "STORE_VAR", OpCode::LOAD_CONST => "LOAD_CONST", diff --git a/crates/oxide_kernel/src/builtin.rs b/crates/oxide_kernel/src/builtin.rs index 3e900183..e6ac9b76 100644 --- a/crates/oxide_kernel/src/builtin.rs +++ b/crates/oxide_kernel/src/builtin.rs @@ -168,6 +168,7 @@ pub struct BuiltinWorld { pub sym_replace: P, pub sym_search: P, pub sym_split: P, + pub sym_iterator: P, } fn intern_label(string_forge: &StringForge, label: &str) -> u32 { @@ -337,6 +338,7 @@ impl BuiltinWorld { let sym_replace = P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())); let sym_search = P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())); let sym_split = P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())); + let sym_iterator = P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())); /// Overwrite placeholder slots (set up by make_pair) with real values. /// ctor.vec[0] = constructor.prototype -> proto @@ -406,6 +408,7 @@ impl BuiltinWorld { sym_replace, sym_search, sym_split, + sym_iterator, } } @@ -483,6 +486,8 @@ impl BuiltinWorld { let _ = self.bind_method(ctor, shape_forge, string_forge, "fromEntries", methods.from_entries, 1); let _ = self.bind_method(ctor, shape_forge, string_forge, "getPrototypeOf", methods.get_prototype_of, 1); let _ = self.bind_method(ctor, shape_forge, string_forge, "hasOwn", methods.has_own, 2); + let _ = self.bind_method(ctor, shape_forge, string_forge, "entries", methods.entries, 1); + let _ = self.bind_method(ctor, shape_forge, string_forge, "values", methods.values, 1); } pub fn bind_array_methods( @@ -791,6 +796,8 @@ impl BuiltinWorld { let _ = self.bind_method(proto, shape_forge, string_forge, "trimEnd", methods.trim_end, 0); let _ = self.bind_method(proto, shape_forge, string_forge, "codePointAt", methods.code_point_at, 1); let _ = self.bind_method(proto, shape_forge, string_forge, "normalize", methods.normalize, 0); + let _ = self.bind_method(proto, shape_forge, string_forge, "matchAll", methods.match_all, 1); + let _ = self.bind_method(proto, shape_forge, string_forge, "replaceAll", methods.replace_all, 2); } pub fn bind_number_methods( diff --git a/crates/oxide_vm/src/bindings/bind_object.rs b/crates/oxide_vm/src/bindings/bind_object.rs index a70cc184..2b453ec4 100644 --- a/crates/oxide_vm/src/bindings/bind_object.rs +++ b/crates/oxide_vm/src/bindings/bind_object.rs @@ -24,8 +24,8 @@ pub fn bind_object(kernel: &Arc, global: &mut JsObject) { from_entries: crate::builtins::object::object_from_entries as *const (), get_prototype_of: crate::builtins::object::object_get_prototype_of as *const (), has_own: crate::builtins::object::object_has_own as *const (), - entries: std::ptr::null(), - values: std::ptr::null(), + entries: crate::builtins::object::object_entries as *const (), + values: crate::builtins::object::object_values as *const (), }; kernel.builtin_world().bind_object_methods( &methods, diff --git a/crates/oxide_vm/src/bindings/bind_string.rs b/crates/oxide_vm/src/bindings/bind_string.rs index 6970a447..5704bd90 100644 --- a/crates/oxide_vm/src/bindings/bind_string.rs +++ b/crates/oxide_vm/src/bindings/bind_string.rs @@ -30,8 +30,8 @@ pub fn bind_string(kernel: &Arc, global: &mut JsObject) { trim_end: crate::builtins::string::string_trim_end as *const (), code_point_at: crate::builtins::string::string_code_point_at as *const (), normalize: crate::builtins::string::string_normalize as *const (), - match_all: std::ptr::null(), - replace_all: std::ptr::null(), + match_all: crate::builtins::string::string_match_all as *const (), + replace_all: crate::builtins::string::string_replace_all as *const (), }; kernel.builtin_world().bind_string_methods( &string_methods, diff --git a/crates/oxide_vm/src/bindings/bind_symbol.rs b/crates/oxide_vm/src/bindings/bind_symbol.rs index 941ffd65..0d5b260e 100644 --- a/crates/oxide_vm/src/bindings/bind_symbol.rs +++ b/crates/oxide_vm/src/bindings/bind_symbol.rs @@ -45,6 +45,11 @@ pub fn bind_symbol(kernel: &Arc, global: &mut JsObject) { ); bind_well_known_symbol(kernel, ctor, sf, sh, "split", sym_split_val); + let sym_iterator_val = JsValue::from_js_object( + kernel.builtin_world().sym_iterator.as_ptr() as *mut JsObject, + ); + bind_well_known_symbol(kernel, ctor, sf, sh, "iterator", sym_iterator_val); + bind_constructor_hash!( kernel, global, diff --git a/crates/oxide_vm/src/builtins/object.rs b/crates/oxide_vm/src/builtins/object.rs index f27ef582..d13852d3 100644 --- a/crates/oxide_vm/src/builtins/object.rs +++ b/crates/oxide_vm/src/builtins/object.rs @@ -379,3 +379,40 @@ pub fn object_has_own(vm: &mut Vm, args: &[u8]) -> NativeResult { if !key_val.is_string() { return Ok(JsValue::bool(false)); } Ok(JsValue::bool(false)) } + +pub fn object_entries(vm: &mut Vm, args: &[u8]) -> NativeResult { + let obj_ptr = require_obj_arg(vm, args, "entries")?; + let obj = unsafe { &*obj_ptr }; + let keys = walk_own_keys(vm, obj); + let n = keys.len().min(255); + let array_proto = vm.kernel().builtin_world().array_proto.as_ptr() as *mut JsObject; + let arr = vm.epoch().alloc(JsObject::new_array(EMPTY_SHAPE_ID, JsValue::from_js_object(array_proto), n, vm.epoch().bump())); + for (_i, (si, offset)) in keys.iter().enumerate().take(n) { + let key_str = vm.kernel().string_forge().lookup(*si).unwrap_or_default(); + let key_val = vm.intern(&key_str); + let val = obj.get_prop_at(*offset); + let pair = vm.epoch().alloc(JsObject::new_array(EMPTY_SHAPE_ID, JsValue::from_js_object(array_proto), 2, vm.epoch().bump())); + unsafe { + (*pair).set_prop_at(0, key_val); + (*pair).set_prop_at(1, val); + (*pair).set_prop_count(2); + (*arr).ensure_hash_props().push(Box::new(JsValue::from_js_object(pair))); + } + } + Ok(JsValue::from_js_object(arr)) +} + +pub fn object_values(vm: &mut Vm, args: &[u8]) -> NativeResult { + let obj_ptr = require_obj_arg(vm, args, "values")?; + let obj = unsafe { &*obj_ptr }; + let keys = walk_own_keys(vm, obj); + let n = keys.len().min(255); + let array_proto = vm.kernel().builtin_world().array_proto.as_ptr() as *mut JsObject; + let arr = vm.epoch().alloc(JsObject::new_array(EMPTY_SHAPE_ID, JsValue::from_js_object(array_proto), n, vm.epoch().bump())); + for (i, (_si, offset)) in keys.iter().enumerate().take(n) { + let val = obj.get_prop_at(*offset); + unsafe { (*arr).set_prop_at(i as u8, val); } + } + unsafe { (*arr).set_prop_count(n as u8); } + Ok(JsValue::from_js_object(arr)) +} diff --git a/crates/oxide_vm/src/builtins/string.rs b/crates/oxide_vm/src/builtins/string.rs index 5c77661e..697452c8 100644 --- a/crates/oxide_vm/src/builtins/string.rs +++ b/crates/oxide_vm/src/builtins/string.rs @@ -469,3 +469,36 @@ pub fn string_normalize(vm: &mut Vm, args: &[u8]) -> NativeResult { }; Ok(vm.intern(&result)) } + +pub fn string_match_all(vm: &mut Vm, args: &[u8]) -> NativeResult { + let s = this_string(vm, args); + if args.len() < 2 { return Err(JsValue::undefined()); } + let pattern_val = vm.reg(args[1]); + if is_regexp_obj(pattern_val, vm) { + let re_ptr = pattern_val.as_js_object_ptr(); + let re = unsafe { &*re_ptr }; + let fn_ptr = match re.native_fn() { Some(p) => p, None => return Ok(JsValue::undefined()) }; + let regex = unsafe { &*(fn_ptr as *const regex::Regex) }; + let matches: Vec = regex.find_iter(&s).map(|m| m.as_str().to_string()).collect(); + return Ok(make_string_array(vm, &matches)); + } + Ok(JsValue::undefined()) +} + +pub fn string_replace_all(vm: &mut Vm, args: &[u8]) -> NativeResult { + let s = this_string(vm, args); + if args.len() < 2 { return Ok(vm.intern(&s)); } + let pattern_val = vm.reg(args[1]); + let replacement = if args.len() > 2 { as_string(vm, vm.reg(args[2])) } else { String::new() }; + if is_regexp_obj(pattern_val, vm) { + let re_ptr = pattern_val.as_js_object_ptr(); + let re = unsafe { &*re_ptr }; + let fn_ptr = match re.native_fn() { Some(p) => p, None => return Ok(vm.intern(&s)) }; + let regex = unsafe { &*(fn_ptr as *const regex::Regex) }; + let result = regex.replace_all(&s, replacement.as_str()).to_string(); + return Ok(vm.intern(&result)); + } + let pattern = as_string(vm, pattern_val); + let result = s.replace(&pattern, &replacement); + Ok(vm.intern(&result)) +} diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index 1e4f0986..a8ac8322 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -174,6 +174,8 @@ pub struct Vm { pub(crate) pending_error_kind: Option<&'static str>, pub(crate) symbol_counter: u32, pub(crate) symbol_descriptions: Vec, + #[allow(dead_code)] + pub(crate) for_of_iters: Vec<*mut u8>, } impl Vm { @@ -203,6 +205,7 @@ impl Vm { pending_error_kind: None, symbol_counter: 0, symbol_descriptions: Vec::new(), + for_of_iters: Vec::new(), } } @@ -230,6 +233,7 @@ impl Vm { pending_error_kind: None, symbol_counter: 0, symbol_descriptions: Vec::new(), + for_of_iters: Vec::new(), } } @@ -1573,6 +1577,14 @@ impl Vm { self.for_in_iters.pop(); } + OpCode::FOR_OF_INIT => {} + + OpCode::FOR_OF_NEXT => {}, + + OpCode::FOR_OF_DONE => {}, + + OpCode::FOR_OF_CLOSE => {}, + OpCode::THROW => { let exc_value = self.regs[rd]; self.exception_value = Some(exc_value); diff --git a/crates/oxide_vm/tests/builtins_array_tests.rs b/crates/oxide_vm/tests/builtins_array_tests.rs index 051608d7..88249b9d 100644 --- a/crates/oxide_vm/tests/builtins_array_tests.rs +++ b/crates/oxide_vm/tests/builtins_array_tests.rs @@ -160,3 +160,66 @@ fn array_empty_literal() { assert!(obj.is_array()); assert_eq!(obj.prop_count(), 0); } + +#[test] +fn array_shift_removes_first() { + let result = eval("[1,2,3].shift()").unwrap(); + assert_eq!(result.as_int(), 1); +} + +#[test] +fn array_unshift_adds_front() { + let result = eval("[3].unshift(1,2)").unwrap(); + assert_eq!(result.as_int(), 3); +} + +#[test] +fn array_fill_replaces() { + let result = eval("[1,2,3,4].fill(0,1,3)").unwrap(); + let obj = unsafe { &*result.as_js_object_ptr() }; + assert_eq!(obj.prop_count(), 4); +} + +#[test] +fn array_at_positive() { + let result = eval("[10,20,30].at(1)").unwrap(); + assert_eq!(result.as_int(), 20); +} + +#[test] +fn array_at_negative() { + let result = eval("[10,20,30].at(-1)").unwrap(); + assert_eq!(result.as_int(), 30); +} + +#[test] +fn array_last_index_of_found() { + let result = eval("[1,2,3,2].lastIndexOf(2)").unwrap(); + assert_eq!(result.as_int(), 3); +} + +#[test] +fn array_last_index_of_not_found() { + let result = eval("[1,2,3].lastIndexOf(99)").unwrap(); + assert_eq!(result.as_int(), -1); +} + +#[test] +fn array_sort_default() { + let result = eval("[3,1,2].sort()").unwrap(); + let obj = unsafe { &*result.as_js_object_ptr() }; + assert_eq!(obj.prop_count(), 3); +} + +#[test] +fn array_copy_within_copies() { + let result = eval("[1,2,3,4,5].copyWithin(0,3)").unwrap(); + assert!(result.is_object()); +} + +#[test] +fn array_fill_single() { + let result = eval("[1,2,3].fill(0)").unwrap(); + let obj = unsafe { &*result.as_js_object_ptr() }; + assert_eq!(obj.get_prop_at(0).as_int(), 0); +} diff --git a/crates/oxide_vm/tests/builtins_object_tests.rs b/crates/oxide_vm/tests/builtins_object_tests.rs index 974c1b4e..8fd632d8 100644 --- a/crates/oxide_vm/tests/builtins_object_tests.rs +++ b/crates/oxide_vm/tests/builtins_object_tests.rs @@ -51,3 +51,27 @@ fn object_define_property_sets_value() { let result = eval("Object.defineProperty({},'x',{value:42})").unwrap(); assert!(result.is_object()); } + +#[test] +fn object_get_prototype_of() { + let result = eval("Object.getPrototypeOf({})").unwrap(); + assert!(result.is_object() || result.is_null()); +} + +#[test] +fn object_has_own_true() { + let result = eval("Object.hasOwn({a:1}, 'a')").unwrap(); + assert!(result.is_bool()); +} + +#[test] +fn object_entries_returns_array() { + let result = eval("Object.entries({a:1,b:2})").unwrap(); + assert!(result.is_object()); +} + +#[test] +fn object_values_returns_array() { + let result = eval("Object.values({a:1,b:2})").unwrap(); + assert!(result.is_object()); +} diff --git a/crates/oxide_vm/tests/builtins_string_tests.rs b/crates/oxide_vm/tests/builtins_string_tests.rs index f9948805..7cc47422 100644 --- a/crates/oxide_vm/tests/builtins_string_tests.rs +++ b/crates/oxide_vm/tests/builtins_string_tests.rs @@ -165,3 +165,45 @@ fn string_search_not_found() { let result = eval(&mut vm, "'hello'.search('x')").unwrap(); assert_eq!(result.as_int(), -1); } + +#[test] +fn string_trim_start() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "' hi '.trimStart()").unwrap(); + assert_eq!(to_str(&vm, result), "hi "); +} + +#[test] +fn string_trim_end() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "' hi '.trimEnd()").unwrap(); + assert_eq!(to_str(&vm, result), " hi"); +} + +#[test] +fn string_code_point_at_ascii() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "'ABC'.codePointAt(1)").unwrap(); + assert_eq!(result.as_int(), 66); +} + +#[test] +fn string_normalize_nfc() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "'hello'.normalize('NFC')").unwrap(); + assert_eq!(to_str(&vm, result), "hello"); +} + +#[test] +fn string_match_all() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "'aba'.matchAll(/a/g)").unwrap(); + assert!(result.is_object()); +} + +#[test] +fn string_replace_all() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "'aba'.replaceAll('a', 'c')").unwrap(); + assert_eq!(to_str(&vm, result), "cbc"); +} diff --git a/crates/oxide_vm/tests/builtins_symbol_tests.rs b/crates/oxide_vm/tests/builtins_symbol_tests.rs index 3e43f4e1..05503e1b 100644 --- a/crates/oxide_vm/tests/builtins_symbol_tests.rs +++ b/crates/oxide_vm/tests/builtins_symbol_tests.rs @@ -119,3 +119,10 @@ fn symbol_not_equals_string() { let result = eval(&mut vm, "Symbol() === 'symbol'").unwrap(); assert!(!result.as_bool()); } + +#[test] +fn symbol_iterator_exists() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "typeof Symbol.iterator").unwrap(); + assert_eq!(to_str(&vm, result), "object"); +}