diff --git a/crates/oxide_compiler/src/compiler.rs b/crates/oxide_compiler/src/compiler.rs index cce85591..288b6800 100644 --- a/crates/oxide_compiler/src/compiler.rs +++ b/crates/oxide_compiler/src/compiler.rs @@ -22,6 +22,7 @@ pub(crate) fn is_side_effect_free(expr: &Expression) -> bool { | Expression::BooleanLiteral(_) | Expression::NullLiteral(_) | Expression::Identifier(_) + | Expression::RegExpLiteral(_) ) } @@ -198,6 +199,8 @@ impl CompileCtx { "Date", "Set", "Map", + "RegExp", + "Symbol", "parseInt", "parseFloat", "isNaN", diff --git a/crates/oxide_compiler/src/counter.rs b/crates/oxide_compiler/src/counter.rs index 4f275922..addac076 100644 --- a/crates/oxide_compiler/src/counter.rs +++ b/crates/oxide_compiler/src/counter.rs @@ -465,8 +465,13 @@ impl Compiler { } _ => { ctx.alloc_reg(); + ctx.projected_pc += 1; } }, + Expression::RegExpLiteral(_) => { + ctx.alloc_reg(); + ctx.projected_pc += 1; + } _ => { ctx.alloc_reg(); ctx.projected_pc += 1; // LOAD_CONST or LOAD_VAR diff --git a/crates/oxide_compiler/src/emitter.rs b/crates/oxide_compiler/src/emitter.rs index 38c2009b..acadf527 100644 --- a/crates/oxide_compiler/src/emitter.rs +++ b/crates/oxide_compiler/src/emitter.rs @@ -1041,6 +1041,38 @@ impl Compiler { ctx.emit(opcode::encode(OpCode::LOAD_VAR, r, 254, 0)); Ok(r) } + Expression::RegExpLiteral(lit) => { + if let Some(raw) = &lit.raw { + let raw_str = raw.to_string(); + if raw_str.len() >= 2 && raw_str.starts_with('/') { + let last_slash = raw_str.rfind('/').unwrap_or(raw_str.len() - 1); + let pattern = &raw_str[1..last_slash]; + let flags = &raw_str[last_slash + 1..]; + let ci = ctx.add_constant(Constant::RegExp( + pattern.to_string(), + flags.to_string(), + )); + let r = ctx.alloc_reg(); + ctx.emit(opcode::encode( + OpCode::LOAD_CONST, + r, + (ci & 0xFF) as u8, + ((ci >> 8) & 0xFF) as u8, + )); + Ok(r) + } else { + Err(format!( + "unsupported expression type: {:?}", + expr + )) + } + } else { + Err(format!( + "unsupported expression type: {:?}", + expr + )) + } + } _ => Err(format!("unsupported expression type: {:?}", expr)), } } diff --git a/crates/oxide_compiler/src/hash.rs b/crates/oxide_compiler/src/hash.rs index a0884645..3b403711 100644 --- a/crates/oxide_compiler/src/hash.rs +++ b/crates/oxide_compiler/src/hash.rs @@ -243,6 +243,12 @@ fn hash_expression(expr: &Expression, h: &mut rustc_hash::FxHasher) { } } } + Expression::RegExpLiteral(lit) => { + if let Some(raw) = &lit.raw { + raw.to_string().hash(h); + } + std::mem::discriminant(expr).hash(h); + } _ => { std::mem::discriminant(expr).hash(h); } diff --git a/crates/oxide_compiler/src/module.rs b/crates/oxide_compiler/src/module.rs index f3be48b9..fcbf7786 100644 --- a/crates/oxide_compiler/src/module.rs +++ b/crates/oxide_compiler/src/module.rs @@ -11,6 +11,7 @@ pub enum Constant { Null, Undefined, BytecodeFunc(u32), + RegExp(String, String), } pub struct CompiledModule { diff --git a/crates/oxide_kernel/src/builtin.rs b/crates/oxide_kernel/src/builtin.rs index 9d609319..ad759aef 100644 --- a/crates/oxide_kernel/src/builtin.rs +++ b/crates/oxide_kernel/src/builtin.rs @@ -88,6 +88,12 @@ pub struct StringMethods { pub search: *const (), } +pub struct RegExpMethods { + pub exec: *const (), + pub test: *const (), + pub to_string: *const (), +} + pub struct NumberMethods { pub is_nan: *const (), pub is_finite: *const (), @@ -134,6 +140,12 @@ pub struct BuiltinWorld { pub set_proto: P, pub map_constructor: P, pub map_proto: P, + pub regexp_constructor: P, + pub regexp_proto: P, + pub sym_match: P, + pub sym_replace: P, + pub sym_search: P, + pub sym_split: P, } fn intern_label(string_forge: &StringForge, label: &str) -> u32 { @@ -290,6 +302,19 @@ impl BuiltinWorld { si_constructor, si_name, ); + let (regexp_proto, regexp_constructor) = make_pair( + string_forge, + shape_forge, + "RegExp", + si_prototype, + si_constructor, + si_name, + ); + + let sym_match = P::new(JsObject::new_empty(EMPTY_SHAPE_ID, JsValue::null())); + 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())); /// Overwrite placeholder slots (set up by make_pair) with real values. /// ctor.vec[0] = constructor.prototype -> proto @@ -320,6 +345,7 @@ impl BuiltinWorld { wire_ctor_proto(&date_constructor, &date_proto); wire_ctor_proto(&set_constructor, &set_proto); wire_ctor_proto(&map_constructor, &map_proto); + wire_ctor_proto(®exp_constructor, ®exp_proto); Self { object_proto, @@ -352,6 +378,12 @@ impl BuiltinWorld { set_proto, map_constructor, map_proto, + regexp_constructor, + regexp_proto, + sym_match, + sym_replace, + sym_search, + sym_split, } } diff --git a/crates/oxide_types/src/value.rs b/crates/oxide_types/src/value.rs index 2565f919..fe6fbc8c 100644 --- a/crates/oxide_types/src/value.rs +++ b/crates/oxide_types/src/value.rs @@ -20,6 +20,7 @@ const TAG_NULL: u64 = 2; const TAG_UNDEFINED: u64 = 3; const TAG_OBJECT: u64 = 4; const TAG_STRING: u64 = 5; +const TAG_SYMBOL: u64 = 6; /// 48-bit pointer mask (x86-64 canonical VA) const PTR_MASK: u64 = 0x0000_FFFF_FFFF_FFFF; @@ -77,6 +78,9 @@ impl PartialEq for JsValue { } return self.as_string_index() == other.as_string_index(); } + if self.is_symbol() && other.is_symbol() { + return self.as_symbol_index() == other.as_symbol_index(); + } false } } @@ -206,6 +210,19 @@ impl JsValue { debug_assert!(self.is_string(), "JsValue is not a string"); ((self.0 & STRING_HASH_MASK) >> STRING_HASH_SHIFT) as u16 } + + pub fn symbol(index: u32) -> Self { + Self(make_tag(TAG_SYMBOL) | (index as u64)) + } + + pub fn is_symbol(&self) -> bool { + is_nan_boxed(self.0) && get_tag(self.0) == TAG_SYMBOL + } + + pub fn as_symbol_index(&self) -> u32 { + debug_assert!(self.is_symbol()); + (self.0 & STRING_INDEX_MASK) as u32 + } } fn is_nan_bits(bits: u64) -> bool { @@ -213,7 +230,7 @@ fn is_nan_bits(bits: u64) -> bool { } fn is_nan_boxed(bits: u64) -> bool { - (0xFFF8..=0xFFFD).contains(&((bits >> 48) as u16)) + (0xFFF8..=0xFFFE).contains(&((bits >> 48) as u16)) } impl fmt::Display for JsValue { @@ -243,6 +260,8 @@ impl fmt::Display for JsValue { write!(f, "{{object}}") } else if self.is_string() { write!(f, "{{string}}") + } else if self.is_symbol() { + write!(f, "Symbol(idx={})", self.as_symbol_index()) } else { write!(f, "{{unknown}}") } @@ -275,6 +294,8 @@ impl fmt::Debug for JsValue { self.as_string_index(), self.as_string_hash() ) + } else if self.is_symbol() { + write!(f, "JsValue(Symbol(idx={}))", self.as_symbol_index()) } else { write!(f, "JsValue(Unknown)") } @@ -342,6 +363,7 @@ mod tests { val.is_undefined(), val.is_object(), val.is_string(), + val.is_symbol(), ]; let count = matched.iter().filter(|&&x| x).count(); assert_eq!(count, 1, "bits={bits:#018x} matched {count} types"); diff --git a/crates/oxide_vm/src/bindings/bind_regexp.rs b/crates/oxide_vm/src/bindings/bind_regexp.rs new file mode 100644 index 00000000..c25e2711 --- /dev/null +++ b/crates/oxide_vm/src/bindings/bind_regexp.rs @@ -0,0 +1,56 @@ +use std::sync::Arc; + +use oxide_kernel::bind_method; +use oxide_kernel::kernel::OxideKernel; +use oxide_types::object::JsObject; + +use crate::bind_constructor_hash; + +pub fn bind_regexp(kernel: &Arc, global: &mut JsObject) { + let ctor_ptr = kernel.builtin_world().regexp_constructor.as_ptr() as *mut JsObject; + let ctor = unsafe { &mut *ctor_ptr }; + let proto_ptr = kernel.builtin_world().regexp_proto.as_ptr() as *mut JsObject; + let proto = unsafe { &mut *proto_ptr }; + let sf = kernel.string_forge().as_ref(); + let sh = kernel.shape_forge().as_ref(); + + ctor.set_native_fn(Some(crate::builtins::regexp::regexp_constructor as *const ())); + ctor.set_native_arg_count(2); + + bind_method!( + kernel.builtin_world(), + proto, + sf, + sh, + "exec", + crate::builtins::regexp::regexp_exec, + 1 + ); + bind_method!( + kernel.builtin_world(), + proto, + sf, + sh, + "test", + crate::builtins::regexp::regexp_test, + 1 + ); + bind_method!( + kernel.builtin_world(), + proto, + sf, + sh, + "toString", + crate::builtins::regexp::regexp_to_string, + 0 + ); + + bind_constructor_hash!( + kernel, + global, + "RegExp", + ctor_ptr, + crate::builtins::regexp::regexp_constructor, + 2 + ); +} diff --git a/crates/oxide_vm/src/bindings/bind_symbol.rs b/crates/oxide_vm/src/bindings/bind_symbol.rs new file mode 100644 index 00000000..941ffd65 --- /dev/null +++ b/crates/oxide_vm/src/bindings/bind_symbol.rs @@ -0,0 +1,70 @@ +use std::sync::Arc; + +use oxide_kernel::bind_method; +use oxide_kernel::kernel::OxideKernel; +use oxide_types::object::JsObject; +use oxide_types::value::JsValue; + +use crate::bind_constructor_hash; + +pub fn bind_symbol(kernel: &Arc, global: &mut JsObject) { + let ctor_ptr = kernel.builtin_world().symbol_constructor.as_ptr() as *mut JsObject; + let ctor = unsafe { &mut *ctor_ptr }; + let proto_ptr = kernel.builtin_world().symbol_proto.as_ptr() as *mut JsObject; + let proto = unsafe { &mut *proto_ptr }; + let sf = kernel.string_forge().as_ref(); + let sh = kernel.shape_forge().as_ref(); + + bind_method!( + kernel.builtin_world(), + proto, + sf, + sh, + "toString", + crate::builtins::symbol::symbol_to_string, + 0 + ); + + let sym_match_val = JsValue::from_js_object( + kernel.builtin_world().sym_match.as_ptr() as *mut JsObject, + ); + bind_well_known_symbol(kernel, ctor, sf, sh, "match", sym_match_val); + + let sym_replace_val = JsValue::from_js_object( + kernel.builtin_world().sym_replace.as_ptr() as *mut JsObject, + ); + bind_well_known_symbol(kernel, ctor, sf, sh, "replace", sym_replace_val); + + let sym_search_val = JsValue::from_js_object( + kernel.builtin_world().sym_search.as_ptr() as *mut JsObject, + ); + bind_well_known_symbol(kernel, ctor, sf, sh, "search", sym_search_val); + + let sym_split_val = JsValue::from_js_object( + kernel.builtin_world().sym_split.as_ptr() as *mut JsObject, + ); + bind_well_known_symbol(kernel, ctor, sf, sh, "split", sym_split_val); + + bind_constructor_hash!( + kernel, + global, + "Symbol", + ctor_ptr, + crate::builtins::symbol::symbol_constructor, + 1 + ); +} + +fn bind_well_known_symbol( + kernel: &Arc, + ctor: &mut JsObject, + _sf: &oxide_kernel::string_forge::StringForge, + _sh: &oxide_kernel::shape_forge::ShapeForge, + name: &str, + val: JsValue, +) { + let si = kernel.string_forge().intern(name).0; + let shape_id = kernel.shape_forge().make_shape(ctor.shape_id(), si); + ctor.set_shape_id(shape_id); + ctor.ensure_hash_props().push(Box::new(val)); +} diff --git a/crates/oxide_vm/src/bindings/mod.rs b/crates/oxide_vm/src/bindings/mod.rs index 23d7e7de..94036fbd 100644 --- a/crates/oxide_vm/src/bindings/mod.rs +++ b/crates/oxide_vm/src/bindings/mod.rs @@ -8,8 +8,10 @@ pub mod bind_map; pub mod bind_math; pub mod bind_number; pub mod bind_object; +pub mod bind_regexp; pub mod bind_set; pub mod bind_string; +pub mod bind_symbol; use std::sync::Arc; @@ -60,4 +62,6 @@ pub fn init_kernel_builtins(kernel: &Arc) { bind_map::bind_map(kernel, global); bind_boolean::bind_boolean(kernel, global); bind_function::bind_function(kernel); + bind_regexp::bind_regexp(kernel, global); + bind_symbol::bind_symbol(kernel, global); } diff --git a/crates/oxide_vm/src/builtins/mod.rs b/crates/oxide_vm/src/builtins/mod.rs index ca79ee76..36d4e3cc 100644 --- a/crates/oxide_vm/src/builtins/mod.rs +++ b/crates/oxide_vm/src/builtins/mod.rs @@ -9,5 +9,7 @@ pub mod map; pub mod math; pub mod number; pub mod object; +pub mod regexp; pub mod set; pub mod string; +pub mod symbol; diff --git a/crates/oxide_vm/src/builtins/regexp.rs b/crates/oxide_vm/src/builtins/regexp.rs new file mode 100644 index 00000000..f92afc33 --- /dev/null +++ b/crates/oxide_vm/src/builtins/regexp.rs @@ -0,0 +1,261 @@ +use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; +use oxide_types::object::JsObject; +use oxide_types::value::JsValue; + +use crate::coercion; +use crate::native::NativeResult; +use crate::vm::Vm; + +fn get_regexp_ptr(vm: &Vm, args: &[u8]) -> Result<*mut JsObject, JsValue> { + let this_val = vm.reg(args[0]); + if !this_val.is_object() { + return Err(JsValue::string( + vm.kernel() + .string_forge() + .intern("TypeError: RegExp.prototype method called on non-object") + .0, + 0, + )); + } + let ptr = this_val.as_js_object_ptr(); + if ptr.is_null() { + return Err(JsValue::string( + vm.kernel() + .string_forge() + .intern("TypeError: null object") + .0, + 0, + )); + } + Ok(ptr) +} + +fn parse_flags(flags: &str) -> (bool, bool, bool) { + let mut global = false; + let mut ignore_case = false; + let mut multi_line = false; + for c in flags.chars() { + match c { + 'g' => global = true, + 'i' => ignore_case = true, + 'm' => multi_line = true, + _ => {} + } + } + (global, ignore_case, multi_line) +} + +fn set_prop(obj: &mut JsObject, name: &str, val: JsValue, vm: &Vm) { + let si = vm.kernel().string_forge().intern(name).0; + let shape_id = vm.kernel().shape_forge().make_shape(obj.shape_id(), si); + obj.set_shape_id(shape_id); + obj.ensure_hash_props().push(Box::new(val)); +} + +fn get_prop(obj: &JsObject, idx: usize) -> JsValue { + obj.hash_props_vec() + .and_then(|v| v.get(idx)) + .map(|b| **b) + .unwrap_or(JsValue::undefined()) +} + +fn set_prop_at(obj: *mut JsObject, idx: usize, val: JsValue) { + unsafe { + let re = &mut *obj; + if let Some(vec) = re.hash_props_vec() { + let ptr = vec.as_ptr() as *mut Box; + let v = &mut *ptr.add(idx); + **v = val; + } + } +} + +pub fn regexp_constructor(vm: &mut Vm, args: &[u8]) -> NativeResult { + let (pattern, flags) = if args.len() < 2 { + (String::new(), String::new()) + } else if args.len() < 3 { + let pat = coercion::to_string(vm.kernel().string_forge().as_ref(), vm.reg(args[1])); + (pat, String::new()) + } else { + let pat = coercion::to_string(vm.kernel().string_forge().as_ref(), vm.reg(args[1])); + let fl = coercion::to_string(vm.kernel().string_forge().as_ref(), vm.reg(args[2])); + (pat, fl) + }; + + let (global, ignore_case, multi_line) = parse_flags(&flags); + + let mut obj = JsObject::new_empty( + EMPTY_SHAPE_ID, + JsValue::from_js_object( + vm.kernel().builtin_world().regexp_proto.as_ptr() as *mut JsObject, + ), + ); + + let compiled = regex::RegexBuilder::new(&pattern) + .case_insensitive(ignore_case) + .multi_line(multi_line) + .build(); + + match compiled { + Ok(re) => { + let re_ptr = Box::into_raw(Box::new(re)); + obj.set_native_fn(Some(re_ptr as *const ())); + } + Err(e) => { + let msg = format!("SyntaxError: Invalid regular expression: {}", e); + return Err(JsValue::string( + vm.kernel().string_forge().intern(&msg).0, + 0, + )); + } + } + + set_prop(&mut obj, "lastIndex", JsValue::int(0), vm); + set_prop(&mut obj, "source", vm.intern(&pattern), vm); + set_prop(&mut obj, "flags", vm.intern(&flags), vm); + set_prop(&mut obj, "global", JsValue::bool(global), vm); + set_prop(&mut obj, "ignoreCase", JsValue::bool(ignore_case), vm); + set_prop(&mut obj, "multiline", JsValue::bool(multi_line), vm); + set_prop(&mut obj, "dotAll", JsValue::bool(false), vm); + set_prop(&mut obj, "sticky", JsValue::bool(false), vm); + set_prop(&mut obj, "unicode", JsValue::bool(false), vm); + + let obj_ptr = vm.epoch().alloc(obj); + Ok(JsValue::from_js_object(obj_ptr)) +} + +pub fn regexp_test(vm: &mut Vm, args: &[u8]) -> NativeResult { + let re_ptr = get_regexp_ptr(vm, args)?; + let re = unsafe { &*re_ptr }; + + let fn_ptr = match re.native_fn() { + None => { + return Err(JsValue::string( + vm.kernel() + .string_forge() + .intern("TypeError: invalid RegExp") + .0, + 0, + )); + } + Some(p) => p, + }; + + let regex = unsafe { &*(fn_ptr as *const regex::Regex) }; + let haystack = coercion::to_string( + vm.kernel().string_forge().as_ref(), + vm.reg(if args.len() > 1 { args[1] } else { args[0] }), + ); + let last_index = + coercion::to_number(get_prop(re, 0), vm.kernel().string_forge().as_ref()) as usize; + let is_global = get_prop(re, 3).as_bool(); + + if is_global { + let result = regex.find_at(&haystack, last_index).is_some(); + Ok(JsValue::bool(result)) + } else { + Ok(JsValue::bool(regex.is_match(&haystack))) + } +} + +pub fn regexp_exec(vm: &mut Vm, args: &[u8]) -> NativeResult { + let re_ptr = get_regexp_ptr(vm, args)?; + + let fn_ptr = { + let re = unsafe { &*re_ptr }; + match re.native_fn() { + None => { + return Err(JsValue::string( + vm.kernel() + .string_forge() + .intern("TypeError: invalid RegExp") + .0, + 0, + )); + } + Some(p) => p, + } + }; + + let regex = unsafe { &*(fn_ptr as *const regex::Regex) }; + let haystack = coercion::to_string( + vm.kernel().string_forge().as_ref(), + vm.reg(if args.len() > 1 { args[1] } else { args[0] }), + ); + + let (last_index, is_global) = { + let re = unsafe { &*re_ptr }; + let li = + coercion::to_number(get_prop(re, 0), vm.kernel().string_forge().as_ref()) as usize; + let g = get_prop(re, 3).as_bool(); + (li, g) + }; + + let match_result = if is_global { + if last_index > haystack.len() { + return Ok(JsValue::null()); + } + regex.find_at(&haystack, last_index) + } else { + regex.find(&haystack) + }; + + if let Some(m) = match_result { + let mut match_obj = JsObject::new_empty( + EMPTY_SHAPE_ID, + JsValue::from_js_object( + vm.kernel().builtin_world().array_proto.as_ptr() as *mut JsObject, + ), + ); + + let full_match = &haystack[m.start()..m.end()]; + let full_si = vm.kernel().string_forge().intern(full_match).0; + match_obj + .ensure_hash_props() + .push(Box::new(JsValue::string(full_si, 0))); + + let captures = regex.captures(&haystack[m.start()..m.end()]); + if let Some(caps) = &captures { + for i in 1..caps.len() { + let cap_str = caps.get(i).map(|cm| cm.as_str()).unwrap_or(""); + let si = vm.kernel().string_forge().intern(cap_str).0; + match_obj + .ensure_hash_props() + .push(Box::new(JsValue::string(si, 0))); + } + } + + set_prop(&mut match_obj, "index", JsValue::int(m.start() as i32), vm); + let haystack_val = vm.intern(&haystack); + set_prop(&mut match_obj, "input", haystack_val, vm); + set_prop(&mut match_obj, "groups", JsValue::undefined(), vm); + + if is_global { + set_prop_at(re_ptr, 0, JsValue::int(m.end() as i32)); + } + + let obj_ptr = vm.epoch().alloc(match_obj); + Ok(JsValue::from_js_object(obj_ptr)) + } else { + if is_global { + set_prop_at(re_ptr, 0, JsValue::int(0)); + } + Ok(JsValue::null()) + } +} + +pub fn regexp_to_string(vm: &mut Vm, args: &[u8]) -> NativeResult { + let re_ptr = get_regexp_ptr(vm, args)?; + let re = unsafe { &*re_ptr }; + let source = { + let val = get_prop(re, 1); + vm.lookup_str(val).unwrap_or_default() + }; + let flags = { + let val = get_prop(re, 2); + vm.lookup_str(val).unwrap_or_default() + }; + let result = format!("/{}/{}", source, flags); + let si = vm.kernel().string_forge().intern(&result).0; + Ok(JsValue::string(si, 0)) +} diff --git a/crates/oxide_vm/src/builtins/string.rs b/crates/oxide_vm/src/builtins/string.rs index a1b9d681..c744e4f3 100644 --- a/crates/oxide_vm/src/builtins/string.rs +++ b/crates/oxide_vm/src/builtins/string.rs @@ -6,7 +6,6 @@ use crate::coercion; use crate::native::NativeResult; use crate::vm::Vm; use memchr::memchr; -use regex::Regex; fn this_string(vm: &Vm, args: &[u8]) -> String { coercion::to_string(vm.kernel().string_forge().as_ref(), vm.reg(args[0])) @@ -35,6 +34,27 @@ fn as_string(vm: &Vm, val: JsValue) -> String { coercion::to_string(vm.kernel().string_forge().as_ref(), val) } +fn is_regexp_obj(val: JsValue, vm: &Vm) -> bool { + if !val.is_object() { + return false; + } + let ptr = val.as_js_object_ptr(); + if ptr.is_null() { + return false; + } + let obj = unsafe { &*ptr }; + let proto = obj.proto(); + if !proto.is_object() { + return false; + } + let proto_ptr = proto.as_js_object_ptr(); + if proto_ptr.is_null() { + return false; + } + let rp = vm.kernel().builtin_world().regexp_proto.as_ptr() as *mut JsObject; + std::ptr::eq(proto_ptr, rp) +} + pub fn string_index_of(vm: &mut Vm, args: &[u8]) -> NativeResult { let s = this_string(vm, args); if args.len() < 2 { @@ -291,27 +311,35 @@ pub fn string_split(vm: &mut Vm, args: &[u8]) -> NativeResult { let parts = vec![s.clone()]; return Ok(make_string_array(vm, &parts)); } - let sep = as_string(vm, vm.reg(args[1])); + let sep_val = vm.reg(args[1]); let limit = if args.len() > 2 { coercion::to_number(vm.reg(args[2]), vm.kernel().string_forge().as_ref()) as usize } else { usize::MAX }; - let parts: Vec; - if sep.starts_with('/') && sep.len() > 2 && sep.ends_with('/') { - let pattern = &sep[1..sep.len() - 1]; - if let Ok(re) = Regex::new(pattern) { - let raw: Vec<&str> = re.split(&s).collect(); - parts = raw.iter().map(|p| p.to_string()).take(limit).collect(); - } else { - parts = vec![s.clone()]; - } - } else if sep.is_empty() { - parts = s.chars().map(|c| c.to_string()).take(limit).collect(); - } else { - parts = s.split(&sep).map(|p| p.to_string()).take(limit).collect(); + if is_regexp_obj(sep_val, vm) { + let re_ptr = sep_val.as_js_object_ptr(); + let re = unsafe { &*re_ptr }; + let fn_ptr = match re.native_fn() { + Some(p) => p, + None => { + let parts = vec![s.clone()]; + return Ok(make_string_array(vm, &parts)); + } + }; + let regex = unsafe { &*(fn_ptr as *const regex::Regex) }; + let raw: Vec<&str> = regex.split(&s).collect(); + let parts: Vec = raw.iter().map(|p| p.to_string()).take(limit).collect(); + return Ok(make_string_array(vm, &parts)); } + + let sep = as_string(vm, sep_val); + let parts: Vec = if sep.is_empty() { + s.chars().map(|c| c.to_string()).take(limit).collect() + } else { + s.split(&sep).map(|p| p.to_string()).take(limit).collect() + }; Ok(make_string_array(vm, &parts)) } @@ -320,23 +348,27 @@ pub fn string_replace(vm: &mut Vm, args: &[u8]) -> NativeResult { if args.len() < 2 { return Ok(vm.intern(&s)); } - let pattern = as_string(vm, vm.reg(args[1])); + let pattern_val = vm.reg(args[1]); let replacement = if args.len() > 2 { as_string(vm, vm.reg(args[2])) } else { String::new() }; - let result = if pattern.starts_with('/') && pattern.len() > 2 && pattern.ends_with('/') { - let pat = &pattern[1..pattern.len() - 1]; - if let Ok(re) = Regex::new(pat) { - re.replace_all(&s, replacement.as_str()).to_string() - } else { - s.clone() - } - } else { - s.replacen(&pattern, &replacement, 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(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.replacen(&pattern, &replacement, 1); Ok(vm.intern(&result)) } @@ -345,14 +377,22 @@ pub fn string_match_fn(vm: &mut Vm, args: &[u8]) -> NativeResult { if args.len() < 2 { return Ok(JsValue::undefined()); } - let pattern = as_string(vm, vm.reg(args[1])); - if pattern.starts_with('/') && pattern.len() > 2 && pattern.ends_with('/') { - let pat = &pattern[1..pattern.len() - 1]; - if let Ok(re) = Regex::new(pat) { - let matches: Vec = re.find_iter(&s).map(|m| m.as_str().to_string()).collect(); - return Ok(make_string_array(vm, &matches)); - } - } else if let Some(pos) = s.find(&pattern) { + 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)); + } + + let pattern = as_string(vm, pattern_val); + if let Some(pos) = s.find(&pattern) { return Ok(make_string_array( vm, &[s[pos..pos + pattern.len()].to_string()], @@ -366,15 +406,24 @@ pub fn string_search(vm: &mut Vm, args: &[u8]) -> NativeResult { if args.len() < 2 { return Ok(JsValue::int(-1)); } - let pattern = as_string(vm, vm.reg(args[1])); - if pattern.starts_with('/') && pattern.len() > 2 && pattern.ends_with('/') { - let pat = &pattern[1..pattern.len() - 1]; - if let Ok(re) = Regex::new(pat) { - if let Some(m) = re.find(&s) { - return Ok(JsValue::int(m.start() as i32)); - } + 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::int(-1)), + }; + let regex = unsafe { &*(fn_ptr as *const regex::Regex) }; + if let Some(m) = regex.find(&s) { + return Ok(JsValue::int(m.start() as i32)); } - } else if let Some(pos) = s.find(&pattern) { + return Ok(JsValue::int(-1)); + } + + let pattern = as_string(vm, pattern_val); + if let Some(pos) = s.find(&pattern) { return Ok(JsValue::int(pos as i32)); } Ok(JsValue::int(-1)) diff --git a/crates/oxide_vm/src/builtins/symbol.rs b/crates/oxide_vm/src/builtins/symbol.rs new file mode 100644 index 00000000..4fd30e94 --- /dev/null +++ b/crates/oxide_vm/src/builtins/symbol.rs @@ -0,0 +1,66 @@ +use oxide_types::object::JsObject; +use oxide_types::value::JsValue; + +use crate::coercion; +use crate::native::NativeResult; +use crate::vm::Vm; + +pub fn symbol_constructor(vm: &mut Vm, args: &[u8]) -> NativeResult { + let this_val = vm.reg(if args.is_empty() { 0 } else { args[0] }); + if this_val.is_object() { + let ptr = this_val.as_js_object_ptr(); + if !ptr.is_null() { + let obj = unsafe { &*ptr }; + let proto = obj.proto(); + if proto.is_object() { + let proto_ptr = proto.as_js_object_ptr(); + if !proto_ptr.is_null() { + let sp = vm.kernel().builtin_world().symbol_proto.as_ptr() as *mut JsObject; + if std::ptr::eq(proto_ptr, sp) { + return Err(JsValue::string( + vm.kernel() + .string_forge() + .intern("TypeError: Symbol is not a constructor") + .0, + 0, + )); + } + } + } + } + } + + let description = if args.len() > 1 { + coercion::to_string(vm.kernel().string_forge().as_ref(), vm.reg(args[1])) + } else { + String::new() + }; + + vm.symbol_counter = vm.symbol_counter.wrapping_add(1); + let idx = vm.symbol_descriptions.len() as u32; + vm.symbol_descriptions.push(description); + Ok(JsValue::symbol(idx)) +} + +pub fn symbol_to_string(vm: &mut Vm, args: &[u8]) -> NativeResult { + let this_val = vm.reg(args[0]); + if !this_val.is_symbol() { + return Err(JsValue::string( + vm.kernel() + .string_forge() + .intern("TypeError: Symbol.prototype.toString requires a Symbol") + .0, + 0, + )); + } + + let idx = this_val.as_symbol_index(); + let desc = vm + .symbol_descriptions + .get(idx as usize) + .cloned() + .unwrap_or_default(); + let result = format!("Symbol({})", desc); + let si = vm.kernel().string_forge().intern(&result).0; + Ok(JsValue::string(si, 0)) +} diff --git a/crates/oxide_vm/src/coercion.rs b/crates/oxide_vm/src/coercion.rs index e3aff0bc..589ee7e6 100644 --- a/crates/oxide_vm/src/coercion.rs +++ b/crates/oxide_vm/src/coercion.rs @@ -101,6 +101,9 @@ pub fn to_boolean(val: JsValue, string_forge: &StringForge) -> bool { if val.is_object() { return true; } + if val.is_symbol() { + return true; + } false } @@ -172,6 +175,9 @@ pub fn strict_eq(lhs: JsValue, rhs: JsValue) -> bool { if lhs.is_object() && rhs.is_object() { return lhs.as_ptr() == rhs.as_ptr(); } + if lhs.is_symbol() && rhs.is_symbol() { + return lhs.as_symbol_index() == rhs.as_symbol_index(); + } false } @@ -260,6 +266,9 @@ pub fn same_value(lhs: JsValue, rhs: JsValue) -> bool { if lhs.is_object() && rhs.is_object() { return lhs.as_ptr() == rhs.as_ptr(); } + if lhs.is_symbol() && rhs.is_symbol() { + return lhs.as_symbol_index() == rhs.as_symbol_index(); + } false } diff --git a/crates/oxide_vm/src/dispatch/misc.rs b/crates/oxide_vm/src/dispatch/misc.rs index 43a86207..d8921393 100644 --- a/crates/oxide_vm/src/dispatch/misc.rs +++ b/crates/oxide_vm/src/dispatch/misc.rs @@ -25,6 +25,8 @@ impl Vm { "number" } else if val.is_string() { "string" + } else if val.is_symbol() { + "symbol" } else if val.is_object() { let obj = unsafe { &*val.as_js_object_ptr() }; if obj.is_function() { diff --git a/crates/oxide_vm/src/vm.rs b/crates/oxide_vm/src/vm.rs index 39c20bd7..1e4f0986 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -172,6 +172,8 @@ pub struct Vm { pub(crate) exception_value: Option, pub(crate) pending_exception: Option, pub(crate) pending_error_kind: Option<&'static str>, + pub(crate) symbol_counter: u32, + pub(crate) symbol_descriptions: Vec, } impl Vm { @@ -199,10 +201,13 @@ impl Vm { exception_value: None, pending_exception: None, pending_error_kind: None, + symbol_counter: 0, + symbol_descriptions: Vec::new(), } } pub fn with_kernel(kernel: Arc) -> Self { + let obj_proto = P::clone(&kernel.builtin_world().object_proto); Self { regs: [JsValue::undefined(); 256], pc: 0, @@ -210,10 +215,10 @@ impl Vm { constants: Vec::new(), frames: Vec::with_capacity(128), for_in_iters: Vec::new(), - kernel: Arc::clone(&kernel), + kernel, interned_strings: Vec::new(), epoch: Epoch::new(), - object_prototype: P::clone(&kernel.builtin_world().object_proto), + object_prototype: obj_proto, math_rng_state: 0, sub_modules: Vec::new(), sub_module_constants: Vec::new(), @@ -223,6 +228,8 @@ impl Vm { exception_value: None, pending_exception: None, pending_error_kind: None, + symbol_counter: 0, + symbol_descriptions: Vec::new(), } } @@ -308,6 +315,34 @@ impl Vm { Constant::Null => JsValue::null(), Constant::Undefined => JsValue::undefined(), Constant::BytecodeFunc(idx) => self.create_function_object(*idx), + 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 ctor = unsafe { &*native_fn }; + let ctor_fn = ctor.native_fn(); + if ctor_fn.is_none() { + return JsValue::undefined(); + } + let saved_0 = self.regs[0]; + let saved_1 = self.regs[1]; + let saved_2 = self.regs[2]; + 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 result = func(self, &[0, 1, 2]); + self.regs[0] = saved_0; + self.regs[1] = saved_1; + self.regs[2] = saved_2; + match result { + Ok(val) => val, + Err(_) => JsValue::undefined(), + } + }, }) .collect() } @@ -706,6 +741,7 @@ impl Vm { Constant::BytecodeFunc(idx) => { self.create_function_object(*idx) } + Constant::RegExp(_, _) => JsValue::undefined(), }) .collect(); diff --git a/crates/oxide_vm/tests/builtins_regexp_tests.rs b/crates/oxide_vm/tests/builtins_regexp_tests.rs new file mode 100644 index 00000000..4a8f3236 --- /dev/null +++ b/crates/oxide_vm/tests/builtins_regexp_tests.rs @@ -0,0 +1,235 @@ +use oxide_compiler::compiler::Compiler; +use oxide_types::value::JsValue; +use oxide_vm::vm::Vm; + +fn eval(vm: &mut Vm, source: &str) -> Result { + let allocator = oxide_parser::Allocator::default(); + let program = + oxide_parser::parse(&allocator, source).map_err(|e| format!("Parse error: {:?}", e))?; + let module = Compiler::new() + .compile(&program) + .map_err(|e| format!("Compile error: {}", e))?; + vm.run(&module) +} + +fn to_str(vm: &Vm, val: JsValue) -> String { + vm.kernel() + .string_forge() + .lookup(val.as_string_index()) + .unwrap_or_default() +} + +// --- RegExp constructor --- + +#[test] +fn regexp_constructor_creates_object() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "typeof new RegExp('abc')").unwrap(); + assert_eq!(to_str(&vm, result), "object"); +} + +#[test] +fn regexp_constructor_with_flags() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "new RegExp('abc', 'gi').ignoreCase").unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn regexp_constructor_invalid_pattern_syntax_error() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "new RegExp('[')"); + assert!(result.is_err() || (result.is_ok() && result.unwrap().is_string())); +} + +#[test] +fn regexp_source_property() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "new RegExp('hello', 'g').source").unwrap(); + assert_eq!(to_str(&vm, result), "hello"); +} + +#[test] +fn regexp_flags_property() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "new RegExp('abc', 'gi').flags").unwrap(); + assert_eq!(to_str(&vm, result), "gi"); +} + +#[test] +fn regexp_global_property() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "new RegExp('abc', 'g').global").unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn regexp_multiline_property() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "new RegExp('abc', 'm').multiline").unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn regexp_last_index_property() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "new RegExp('abc', 'g').lastIndex").unwrap(); + assert_eq!(result.as_int(), 0); +} + +#[test] +fn regexp_dotall_sticky_unicode_default_false() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "new RegExp('abc').dotAll").unwrap(); + assert!(!result.as_bool()); +} + +// --- RegExp.prototype.test --- + +#[test] +fn regexp_test_basic() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "new RegExp('abc').test('abc')").unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn regexp_test_no_match() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "new RegExp('abc').test('xyz')").unwrap(); + assert!(!result.as_bool()); +} + +#[test] +fn regexp_test_case_insensitive() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "new RegExp('abc', 'i').test('ABC')").unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn regexp_test_global_flag_iteration() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "var re = /a/g; re.test('a')").unwrap(); + assert!(result.as_bool()); +} + +// --- RegExp.prototype.exec --- + +#[test] +fn regexp_exec_basic() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "/hello/.exec('hello world') !== null").unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn regexp_exec_no_match_returns_null() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "/xyz/.exec('hello')").unwrap(); + assert!(result.is_null()); +} + +#[test] +fn regexp_exec_returns_index() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "/ell/.exec('hello')").unwrap(); + assert!(result.is_object()); +} + +#[test] +fn regexp_exec_returns_input() { + let mut vm = Vm::new(); + let m = eval(&mut vm, "/ell/.exec('hello')").unwrap(); + assert!(m.is_object()); +} + +#[test] +fn regexp_exec_global_last_index_advances() { + let mut vm = Vm::new(); + let re = eval(&mut vm, "var re = /a/g; re.exec('aba'); re.lastIndex").unwrap(); + assert_eq!(re.as_int(), 1); +} + +#[test] +fn regexp_exec_global_resets_on_exhaustion() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "var re = /a/g; re.exec('a'); re.exec('a') === null").unwrap(); + assert!(result.as_bool()); +} + +// --- RegExp literal compilation --- + +#[test] +fn regexp_literal_basic() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "/hello/").unwrap(); + assert!(result.is_object()); +} + +#[test] +fn regexp_literal_with_flags() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "/abc/i.test('ABC')").unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn regexp_literal_global_exec() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "var re = /a/g; re.exec('aba'); re.lastIndex").unwrap(); + assert_eq!(result.as_int(), 1); +} + +// --- String integration --- + +#[test] +fn string_match_with_regexp() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "'hello world'.match(/ell/)").unwrap(); + assert!(result.is_object()); +} + +#[test] +fn string_replace_with_regexp() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "'hello'.replace(/ell/, 'ipp')").unwrap(); + assert_eq!(to_str(&vm, result), "hippo"); +} + +#[test] +fn string_replace_with_regexp_global() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "'aba'.replace(/a/g, 'c')").unwrap(); + assert_eq!(to_str(&vm, result), "cbc"); +} + +#[test] +fn string_search_with_regexp() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "'hello'.search(/ell/)").unwrap(); + assert_eq!(result.as_int(), 1); +} + +#[test] +fn string_search_not_found() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "'hello'.search(/xyz/)").unwrap(); + assert_eq!(result.as_int(), -1); +} + +#[test] +fn string_split_with_regexp() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "'a,b,c'.split(/,/)").unwrap(); + assert!(result.is_object()); +} + +// --- toString --- + +#[test] +fn regexp_to_string() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "/abc/gi.toString()").unwrap(); + assert_eq!(to_str(&vm, result), "/abc/gi"); +} diff --git a/crates/oxide_vm/tests/builtins_symbol_tests.rs b/crates/oxide_vm/tests/builtins_symbol_tests.rs new file mode 100644 index 00000000..3e43f4e1 --- /dev/null +++ b/crates/oxide_vm/tests/builtins_symbol_tests.rs @@ -0,0 +1,121 @@ +use oxide_compiler::compiler::Compiler; +use oxide_types::value::JsValue; +use oxide_vm::vm::Vm; + +fn eval(vm: &mut Vm, source: &str) -> Result { + let allocator = oxide_parser::Allocator::default(); + let program = + oxide_parser::parse(&allocator, source).map_err(|e| format!("Parse error: {:?}", e))?; + let module = Compiler::new() + .compile(&program) + .map_err(|e| format!("Compile error: {}", e))?; + vm.run(&module) +} + +fn to_str(vm: &Vm, val: JsValue) -> String { + vm.kernel() + .string_forge() + .lookup(val.as_string_index()) + .unwrap_or_default() +} + +// --- Symbol constructor --- + +#[test] +fn symbol_creates_value() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "Symbol()").unwrap(); + assert!(result.is_symbol()); +} + +#[test] +fn symbol_with_description() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "Symbol('hello') !== Symbol('hello')").unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn symbol_unique_identity() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "var a = Symbol('x'); var b = Symbol('x'); a === b").unwrap(); + assert!(!result.as_bool()); +} + +#[test] +fn symbol_same_reference_equals() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "var s = Symbol('x'); s === s").unwrap(); + assert!(result.as_bool()); +} + +// --- typeof Symbol --- + +#[test] +fn typeof_symbol_returns_symbol() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "typeof Symbol()").unwrap(); + assert_eq!(to_str(&vm, result), "symbol"); +} + +#[test] +fn typeof_symbol_variable() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "var s = Symbol('test'); typeof s").unwrap(); + assert_eq!(to_str(&vm, result), "symbol"); +} + +// --- Symbol well-known properties --- + +#[test] +fn symbol_match_exists() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "typeof Symbol.match").unwrap(); + assert_eq!(to_str(&vm, result), "object"); +} + +#[test] +fn symbol_replace_exists() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "typeof Symbol.replace").unwrap(); + assert_eq!(to_str(&vm, result), "object"); +} + +#[test] +fn symbol_search_exists() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "typeof Symbol.search").unwrap(); + assert_eq!(to_str(&vm, result), "object"); +} + +#[test] +fn symbol_split_exists() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "typeof Symbol.split").unwrap(); + assert_eq!(to_str(&vm, result), "object"); +} + +// --- Symbol coercion --- + +#[test] +fn symbol_is_truthy() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "Symbol() ? true : false").unwrap(); + assert!(result.as_bool()); +} + +// --- Symbol types --- + +#[test] +fn symbol_not_equals_object() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "Symbol() === {}").unwrap(); + assert!(!result.as_bool()); +} + +#[test] +fn symbol_not_equals_string() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "Symbol() === 'symbol'").unwrap(); + assert!(!result.as_bool()); +}