diff --git a/Cargo.lock b/Cargo.lock index 53b4d933..d8ce851b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -14,6 +14,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + [[package]] name = "allocator-api2" version = "0.2.21" @@ -833,11 +842,13 @@ version = "0.1.0" dependencies = [ "bumpalo", "fast-float", + "memchr", "oxide_compiler", "oxide_kernel", "oxide_parser", "oxide_types", "proptest", + "regex", "ryu", ] @@ -1054,6 +1065,29 @@ dependencies = [ "bitflags", ] +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + [[package]] name = "regex-syntax" version = "0.8.10" diff --git a/Cargo.toml b/Cargo.toml index 6673916a..eb6e94e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,8 @@ proptest = "1" rustc-hash = "2" fast-float = "0.2" ryu = "1" +memchr = "2" +regex = "1" [workspace.lints.clippy] all = { level = "deny", priority = -1 } diff --git a/crates/oxide_kernel/src/builtin.rs b/crates/oxide_kernel/src/builtin.rs index 46a8e347..5b10c6a0 100644 --- a/crates/oxide_kernel/src/builtin.rs +++ b/crates/oxide_kernel/src/builtin.rs @@ -44,6 +44,28 @@ pub struct ErrorMethods { pub eval_error: *const (), } +pub struct StringMethods { + pub index_of: *const (), + pub includes: *const (), + pub char_at: *const (), + pub char_code_at: *const (), + pub concat: *const (), + pub slice: *const (), + pub substring: *const (), + pub to_upper_case: *const (), + pub to_lower_case: *const (), + pub trim: *const (), + pub repeat: *const (), + pub pad_start: *const (), + pub pad_end: *const (), + pub starts_with: *const (), + pub ends_with: *const (), + pub split: *const (), + pub replace: *const (), + pub match_fn: *const (), + pub search: *const (), +} + pub struct BuiltinWorld { pub object_proto: P, pub array_proto: P, @@ -393,6 +415,148 @@ impl BuiltinWorld { ); } + pub fn bind_string_methods( + &self, + methods: &StringMethods, + string_forge: &StringForge, + shape_forge: &ShapeForge, + ) { + let proto_ptr = P::as_ptr(&self.string_proto) as *mut JsObject; + let proto = unsafe { &mut *proto_ptr }; + + let _ = Self::bind_method( + proto, + shape_forge, + string_forge, + "indexOf", + methods.index_of, + 1, + ); + let _ = Self::bind_method( + proto, + shape_forge, + string_forge, + "includes", + methods.includes, + 1, + ); + let _ = Self::bind_method( + proto, + shape_forge, + string_forge, + "charAt", + methods.char_at, + 1, + ); + let _ = Self::bind_method( + proto, + shape_forge, + string_forge, + "charCodeAt", + methods.char_code_at, + 1, + ); + let _ = Self::bind_method( + proto, + shape_forge, + string_forge, + "concat", + methods.concat, + 1, + ); + let _ = Self::bind_method(proto, shape_forge, string_forge, "slice", methods.slice, 2); + let _ = Self::bind_method( + proto, + shape_forge, + string_forge, + "substring", + methods.substring, + 2, + ); + let _ = Self::bind_method( + proto, + shape_forge, + string_forge, + "toUpperCase", + methods.to_upper_case, + 0, + ); + let _ = Self::bind_method( + proto, + shape_forge, + string_forge, + "toLowerCase", + methods.to_lower_case, + 0, + ); + let _ = Self::bind_method(proto, shape_forge, string_forge, "trim", methods.trim, 0); + let _ = Self::bind_method( + proto, + shape_forge, + string_forge, + "repeat", + methods.repeat, + 1, + ); + let _ = Self::bind_method( + proto, + shape_forge, + string_forge, + "padStart", + methods.pad_start, + 1, + ); + let _ = Self::bind_method( + proto, + shape_forge, + string_forge, + "padEnd", + methods.pad_end, + 1, + ); + let _ = Self::bind_method( + proto, + shape_forge, + string_forge, + "startsWith", + methods.starts_with, + 1, + ); + let _ = Self::bind_method( + proto, + shape_forge, + string_forge, + "endsWith", + methods.ends_with, + 1, + ); + let _ = Self::bind_method(proto, shape_forge, string_forge, "split", methods.split, 1); + let _ = Self::bind_method( + proto, + shape_forge, + string_forge, + "replace", + methods.replace, + 2, + ); + let _ = Self::bind_method( + proto, + shape_forge, + string_forge, + "match", + methods.match_fn, + 1, + ); + let _ = Self::bind_method( + proto, + shape_forge, + string_forge, + "search", + methods.search, + 1, + ); + } + pub fn bind_method( proto: &mut JsObject, shape_forge: &ShapeForge, diff --git a/crates/oxide_vm/Cargo.toml b/crates/oxide_vm/Cargo.toml index aadea3b0..7d2f70bf 100644 --- a/crates/oxide_vm/Cargo.toml +++ b/crates/oxide_vm/Cargo.toml @@ -13,6 +13,8 @@ oxide_types = { path = "../oxide_types" } bumpalo.workspace = true fast-float.workspace = true ryu.workspace = true +memchr.workspace = true +regex.workspace = true [dev-dependencies] proptest.workspace = true diff --git a/crates/oxide_vm/src/builtins/mod.rs b/crates/oxide_vm/src/builtins/mod.rs index 7634e0ee..276aea7e 100644 --- a/crates/oxide_vm/src/builtins/mod.rs +++ b/crates/oxide_vm/src/builtins/mod.rs @@ -2,3 +2,4 @@ pub mod array; pub mod error; pub mod global; pub mod object; +pub mod string; diff --git a/crates/oxide_vm/src/builtins/string.rs b/crates/oxide_vm/src/builtins/string.rs new file mode 100644 index 00000000..935113e7 --- /dev/null +++ b/crates/oxide_vm/src/builtins/string.rs @@ -0,0 +1,381 @@ +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; +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])) +} + +fn make_string_array(vm: &mut Vm, parts: &[String]) -> JsValue { + let proto = vm.kernel().builtin_world().array_proto.as_ptr() as *mut JsObject; + let n = parts.len().min(31); + let arr = vm.epoch().alloc(JsObject::new_array( + EMPTY_SHAPE_ID, + JsValue::from_js_object(proto), + n, + vm.epoch().bump(), + )); + unsafe { + for (i, s) in parts.iter().enumerate().take(n) { + let sv = vm.intern(s); + (*arr).set_prop(i as u8, sv); + } + (*arr).set_prop_count(n as u8); + } + JsValue::from_js_object(arr) +} + +fn as_string(vm: &Vm, val: JsValue) -> String { + coercion::to_string(vm.kernel().string_forge().as_ref(), val) +} + +pub fn string_index_of(vm: &mut Vm, args: &[u8]) -> NativeResult { + let s = this_string(vm, args); + if args.len() < 2 { + return Ok(JsValue::int(-1)); + } + let search = as_string(vm, vm.reg(args[1])); + let pos = if args.len() > 2 { + (coercion::to_number(vm.reg(args[2]), vm.kernel().string_forge().as_ref()) as usize) + .min(s.len()) + } else { + 0 + }; + + if search.is_empty() { + return Ok(JsValue::int(pos as i32)); + } + + let haystack = &s.as_bytes()[pos..]; + if search.len() == 1 { + if let Some(idx) = memchr(search.as_bytes()[0], haystack) { + return Ok(JsValue::int((pos + idx) as i32)); + } + } else if let Some(idx) = s[pos..].find(&search) { + return Ok(JsValue::int((pos + idx) as i32)); + } + Ok(JsValue::int(-1)) +} + +pub fn string_includes(vm: &mut Vm, args: &[u8]) -> NativeResult { + let s = this_string(vm, args); + if args.len() < 2 { + return Ok(JsValue::bool(false)); + } + let search = as_string(vm, vm.reg(args[1])); + let pos = if args.len() > 2 { + (coercion::to_number(vm.reg(args[2]), vm.kernel().string_forge().as_ref()) as usize) + .min(s.len()) + } else { + 0 + }; + + if search.is_empty() { + return Ok(JsValue::bool(true)); + } + + let haystack = &s.as_bytes()[pos..]; + if search.len() == 1 { + Ok(JsValue::bool( + memchr(search.as_bytes()[0], haystack).is_some(), + )) + } else { + Ok(JsValue::bool(s[pos..].contains(&search))) + } +} + +pub fn string_char_at(vm: &mut Vm, args: &[u8]) -> NativeResult { + let s = this_string(vm, args); + if args.len() < 2 { + if s.is_empty() { + return Ok(vm.intern("")); + } + return Ok(vm.intern(&s[0..1])); + } + let idx = coercion::to_number(vm.reg(args[1]), vm.kernel().string_forge().as_ref()) as i32; + if idx < 0 || idx as usize >= s.len() { + return Ok(vm.intern("")); + } + let ch = &s[(idx as usize)..(idx as usize + 1)]; + Ok(vm.intern(ch)) +} + +pub fn string_char_code_at(vm: &mut Vm, args: &[u8]) -> NativeResult { + let s = this_string(vm, args); + if args.len() < 2 { + if s.is_empty() { + return Ok(JsValue::float(f64::NAN)); + } + return Ok(JsValue::int(s.as_bytes()[0] as i32)); + } + let idx = coercion::to_number(vm.reg(args[1]), vm.kernel().string_forge().as_ref()) as i32; + if idx < 0 || idx as usize >= s.len() { + return Ok(JsValue::float(f64::NAN)); + } + Ok(JsValue::int(s.as_bytes()[idx as usize] as i32)) +} + +pub fn string_concat(vm: &mut Vm, args: &[u8]) -> NativeResult { + let mut result = this_string(vm, args); + let sf = vm.kernel().string_forge().as_ref(); + for &arg_reg in args.iter().skip(1) { + result.push_str(&coercion::to_string(sf, vm.reg(arg_reg))); + } + Ok(vm.intern(&result)) +} + +pub fn string_slice(vm: &mut Vm, args: &[u8]) -> NativeResult { + let s = this_string(vm, args); + let n = s.len() as i32; + let start = if args.len() > 1 { + let v = coercion::to_number(vm.reg(args[1]), vm.kernel().string_forge().as_ref()) as i32; + if v < 0 { + (n + v).max(0) + } else { + v.min(n) + } + } else { + 0 + }; + let end = if args.len() > 2 { + let v = coercion::to_number(vm.reg(args[2]), vm.kernel().string_forge().as_ref()) as i32; + if v < 0 { + (n + v).max(0) + } else { + v.min(n) + } + } else { + n + }; + let start = start as usize; + let end = end as usize; + let result = if start < end { &s[start..end] } else { "" }; + Ok(vm.intern(result)) +} + +pub fn string_substring(vm: &mut Vm, args: &[u8]) -> NativeResult { + let s = this_string(vm, args); + let n = s.len() as i32; + let mut start = if args.len() > 1 { + (coercion::to_number(vm.reg(args[1]), vm.kernel().string_forge().as_ref()) as i32) + .max(0) + .min(n) + } else { + 0 + }; + let mut end = if args.len() > 2 { + (coercion::to_number(vm.reg(args[2]), vm.kernel().string_forge().as_ref()) as i32) + .max(0) + .min(n) + } else { + n + }; + if start > end { + std::mem::swap(&mut start, &mut end); + } + let result = &s[start as usize..end as usize]; + Ok(vm.intern(result)) +} + +pub fn string_to_upper_case(vm: &mut Vm, args: &[u8]) -> NativeResult { + let s = this_string(vm, args); + Ok(vm.intern(&s.to_uppercase())) +} + +pub fn string_to_lower_case(vm: &mut Vm, args: &[u8]) -> NativeResult { + let s = this_string(vm, args); + Ok(vm.intern(&s.to_lowercase())) +} + +pub fn string_trim(vm: &mut Vm, args: &[u8]) -> NativeResult { + let s = this_string(vm, args); + Ok(vm.intern(s.trim())) +} + +pub fn string_repeat(vm: &mut Vm, args: &[u8]) -> NativeResult { + let s = this_string(vm, args); + let n = if args.len() > 1 { + (coercion::to_number(vm.reg(args[1]), vm.kernel().string_forge().as_ref()) as usize) + .min(10000) + } else { + 1 + }; + Ok(vm.intern(&s.repeat(n))) +} + +pub fn string_pad_start(vm: &mut Vm, args: &[u8]) -> NativeResult { + let s = this_string(vm, args); + let target = if args.len() > 1 { + coercion::to_number(vm.reg(args[1]), vm.kernel().string_forge().as_ref()) as usize + } else { + s.len() + }; + let pad = if args.len() > 2 { + as_string(vm, vm.reg(args[2])) + } else { + " ".to_string() + }; + if s.len() >= target || pad.is_empty() { + return Ok(vm.intern(&s)); + } + let needed = target - s.len(); + let reps = needed.div_ceil(pad.len()); + let mut out = pad.repeat(reps); + out.truncate(needed); + out.push_str(&s); + Ok(vm.intern(&out)) +} + +pub fn string_pad_end(vm: &mut Vm, args: &[u8]) -> NativeResult { + let s = this_string(vm, args); + let target = if args.len() > 1 { + coercion::to_number(vm.reg(args[1]), vm.kernel().string_forge().as_ref()) as usize + } else { + s.len() + }; + let pad = if args.len() > 2 { + as_string(vm, vm.reg(args[2])) + } else { + " ".to_string() + }; + if s.len() >= target || pad.is_empty() { + return Ok(vm.intern(&s)); + } + let needed = target - s.len(); + let reps = needed.div_ceil(pad.len()); + let mut out = s; + let pad_repeated = pad.repeat(reps); + out.push_str(&pad_repeated[..needed]); + Ok(vm.intern(&out)) +} + +pub fn string_starts_with(vm: &mut Vm, args: &[u8]) -> NativeResult { + let s = this_string(vm, args); + if args.len() < 2 { + return Ok(JsValue::bool(false)); + } + let search = as_string(vm, vm.reg(args[1])); + let pos = if args.len() > 2 { + (coercion::to_number(vm.reg(args[2]), vm.kernel().string_forge().as_ref()) as usize) + .min(s.len()) + } else { + 0 + }; + Ok(JsValue::bool(s[pos..].starts_with(&search))) +} + +pub fn string_ends_with(vm: &mut Vm, args: &[u8]) -> NativeResult { + let s = this_string(vm, args); + if args.len() < 2 { + return Ok(JsValue::bool(false)); + } + let search = as_string(vm, vm.reg(args[1])); + let end_pos = if args.len() > 2 { + (coercion::to_number(vm.reg(args[2]), vm.kernel().string_forge().as_ref()) as usize) + .min(s.len()) + } else { + s.len() + }; + Ok(JsValue::bool(s[..end_pos].ends_with(&search))) +} + +pub fn string_split(vm: &mut Vm, args: &[u8]) -> NativeResult { + let s = this_string(vm, args); + if args.len() < 2 { + let parts = vec![s.clone()]; + return Ok(make_string_array(vm, &parts)); + } + let sep = as_string(vm, 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(); + } + Ok(make_string_array(vm, &parts)) +} + +pub fn string_replace(vm: &mut Vm, args: &[u8]) -> NativeResult { + let s = this_string(vm, args); + if args.len() < 2 { + return Ok(vm.intern(&s)); + } + let pattern = as_string(vm, 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) + }; + Ok(vm.intern(&result)) +} + +pub fn string_match_fn(vm: &mut Vm, args: &[u8]) -> NativeResult { + let s = this_string(vm, args); + 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) { + return Ok(make_string_array( + vm, + &[s[pos..pos + pattern.len()].to_string()], + )); + } + Ok(JsValue::undefined()) +} + +pub fn string_search(vm: &mut Vm, args: &[u8]) -> NativeResult { + let s = this_string(vm, args); + 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)); + } + } + } else 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/vm.rs b/crates/oxide_vm/src/vm.rs index 676daf6b..48be4910 100644 --- a/crates/oxide_vm/src/vm.rs +++ b/crates/oxide_vm/src/vm.rs @@ -8,7 +8,7 @@ use oxide_compiler::opcode::{self, OpCode}; use crate::coercion; use crate::native::NativeFn; -use oxide_kernel::builtin::{ArrayMethods, ErrorMethods, ObjectMethods}; +use oxide_kernel::builtin::{ArrayMethods, ErrorMethods, ObjectMethods, StringMethods}; use oxide_kernel::kernel::{KernelConfig, OxideKernel}; use oxide_kernel::prop_forge::PropTemplate; use oxide_kernel::shape_forge::EMPTY_SHAPE_ID; @@ -131,6 +131,45 @@ pub fn init_kernel_builtins(kernel: &Arc) { let bump3 = bumpalo::Bump::new(); global.set_prop_expand(cur_count, err_val, &bump3); global.bump_generation(); + + let string_methods = StringMethods { + index_of: crate::builtins::string::string_index_of as *const (), + includes: crate::builtins::string::string_includes as *const (), + char_at: crate::builtins::string::string_char_at as *const (), + char_code_at: crate::builtins::string::string_char_code_at as *const (), + concat: crate::builtins::string::string_concat as *const (), + slice: crate::builtins::string::string_slice as *const (), + substring: crate::builtins::string::string_substring as *const (), + to_upper_case: crate::builtins::string::string_to_upper_case as *const (), + to_lower_case: crate::builtins::string::string_to_lower_case as *const (), + trim: crate::builtins::string::string_trim as *const (), + repeat: crate::builtins::string::string_repeat as *const (), + pad_start: crate::builtins::string::string_pad_start as *const (), + pad_end: crate::builtins::string::string_pad_end as *const (), + starts_with: crate::builtins::string::string_starts_with as *const (), + ends_with: crate::builtins::string::string_ends_with as *const (), + split: crate::builtins::string::string_split as *const (), + replace: crate::builtins::string::string_replace as *const (), + match_fn: crate::builtins::string::string_match_fn as *const (), + search: crate::builtins::string::string_search as *const (), + }; + kernel.builtin_world().bind_string_methods( + &string_methods, + kernel.string_forge().as_ref(), + kernel.shape_forge().as_ref(), + ); + + let si_str = kernel.string_forge().intern("String").0; + let str_shape = kernel.shape_forge().make_shape(global.shape_id(), si_str); + let str_val = JsValue::from_js_object( + kernel.builtin_world().string_constructor.as_ptr() as *mut JsObject + ); + let cur_count = global.prop_count(); + global.set_shape_id(str_shape); + global.set_prop_count(cur_count + 1); + let bump4 = bumpalo::Bump::new(); + global.set_prop_expand(cur_count, str_val, &bump4); + global.bump_generation(); } impl Vm { @@ -552,8 +591,20 @@ impl Vm { } OpCode::IC_GET_PROP => { - let obj_ptr = self.regs[a].as_object_ptr() as *mut JsObject; + let val = self.regs[a]; + let obj_ptr = val.as_object_ptr() as *mut JsObject; if obj_ptr.is_null() { + if val.is_string() { + let proto_ptr = + self.kernel.builtin_world().string_proto.as_ptr() as *mut JsObject; + let proto = unsafe { &*proto_ptr }; + let prop_name_si = self.regs[b].as_string_index(); + if let Some(resolved) = self.resolve_property(proto, prop_name_si) { + self.regs[a] = resolved; + self.pc += 1; + continue; + } + } return Err("IC_GET_PROP on non-object".into()); } let obj = unsafe { &*obj_ptr }; diff --git a/crates/oxide_vm/tests/builtins_string_tests.rs b/crates/oxide_vm/tests/builtins_string_tests.rs new file mode 100644 index 00000000..f9948805 --- /dev/null +++ b/crates/oxide_vm/tests/builtins_string_tests.rs @@ -0,0 +1,167 @@ +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() +} + +#[test] +fn string_index_of_found() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "'hello'.indexOf('e')").unwrap(); + assert_eq!(result.as_int(), 1); +} + +#[test] +fn string_index_of_not_found() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "'hello'.indexOf('x')").unwrap(); + assert_eq!(result.as_int(), -1); +} + +#[test] +fn string_includes_true() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "'hello'.includes('ell')").unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn string_includes_false() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "'hello'.includes('xyz')").unwrap(); + assert!(!result.as_bool()); +} + +#[test] +fn string_char_at() { + let mut vm = Vm::new(); + let s = eval(&mut vm, "'hello'.charAt(1)").unwrap(); + assert_eq!(to_str(&vm, s), "e"); +} + +#[test] +fn string_char_code_at() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "'hello'.charCodeAt(0)").unwrap(); + assert_eq!(result.as_int(), 104); +} + +#[test] +fn string_concat() { + let mut vm = Vm::new(); + let s = eval(&mut vm, "'hello'.concat(' world')").unwrap(); + assert_eq!(to_str(&vm, s), "hello world"); +} + +#[test] +fn string_slice() { + let mut vm = Vm::new(); + let s = eval(&mut vm, "'hello'.slice(1, 4)").unwrap(); + assert_eq!(to_str(&vm, s), "ell"); +} + +#[test] +fn string_substring() { + let mut vm = Vm::new(); + let s = eval(&mut vm, "'hello'.substring(0, 2)").unwrap(); + assert_eq!(to_str(&vm, s), "he"); +} + +#[test] +fn string_to_upper_case() { + let mut vm = Vm::new(); + let s = eval(&mut vm, "'hello'.toUpperCase()").unwrap(); + assert_eq!(to_str(&vm, s), "HELLO"); +} + +#[test] +fn string_to_lower_case() { + let mut vm = Vm::new(); + let s = eval(&mut vm, "'HELLO'.toLowerCase()").unwrap(); + assert_eq!(to_str(&vm, s), "hello"); +} + +#[test] +fn string_trim() { + let mut vm = Vm::new(); + let s = eval(&mut vm, "' hi '.trim()").unwrap(); + assert_eq!(to_str(&vm, s), "hi"); +} + +#[test] +fn string_repeat() { + let mut vm = Vm::new(); + let s = eval(&mut vm, "'abc'.repeat(3)").unwrap(); + assert_eq!(to_str(&vm, s), "abcabcabc"); +} + +#[test] +fn string_pad_start() { + let mut vm = Vm::new(); + let s = eval(&mut vm, "'5'.padStart(4, '0')").unwrap(); + assert_eq!(to_str(&vm, s), "0005"); +} + +#[test] +fn string_pad_end() { + let mut vm = Vm::new(); + let s = eval(&mut vm, "'hi'.padEnd(4)").unwrap(); + assert_eq!(to_str(&vm, s), "hi "); +} + +#[test] +fn string_starts_with() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "'hello'.startsWith('hel')").unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn string_ends_with() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "'hello'.endsWith('lo')").unwrap(); + assert!(result.as_bool()); +} + +#[test] +fn string_split_comma() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "'a,b,c'.split(',')").unwrap(); + assert!(result.is_object()); +} + +#[test] +fn string_replace_first() { + let mut vm = Vm::new(); + let s = eval(&mut vm, "'hello'.replace('l', 'L')").unwrap(); + assert_eq!(to_str(&vm, s), "heLlo"); +} + +#[test] +fn string_search_found() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "'hello'.search('ll')").unwrap(); + assert_eq!(result.as_int(), 2); +} + +#[test] +fn string_search_not_found() { + let mut vm = Vm::new(); + let result = eval(&mut vm, "'hello'.search('x')").unwrap(); + assert_eq!(result.as_int(), -1); +}