feat: 支持 RegExpr 与 Symbol 部分方法

This commit is contained in:
2026-06-08 10:36:39 +08:00
parent 125b91ddae
commit b8fbc93bca
19 changed files with 1057 additions and 45 deletions
+3
View File
@@ -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",
+5
View File
@@ -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
+32
View File
@@ -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)),
}
}
+6
View File
@@ -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);
}
+1
View File
@@ -11,6 +11,7 @@ pub enum Constant {
Null,
Undefined,
BytecodeFunc(u32),
RegExp(String, String),
}
pub struct CompiledModule {
+32
View File
@@ -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<JsObject>,
pub map_constructor: P<JsObject>,
pub map_proto: P<JsObject>,
pub regexp_constructor: P<JsObject>,
pub regexp_proto: P<JsObject>,
pub sym_match: P<JsObject>,
pub sym_replace: P<JsObject>,
pub sym_search: P<JsObject>,
pub sym_split: P<JsObject>,
}
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(&regexp_constructor, &regexp_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,
}
}
+23 -1
View File
@@ -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");
@@ -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<OxideKernel>, 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
);
}
@@ -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<OxideKernel>, 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<OxideKernel>,
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));
}
+4
View File
@@ -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<OxideKernel>) {
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);
}
+2
View File
@@ -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;
+261
View File
@@ -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<JsValue>;
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))
}
+91 -42
View File
@@ -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<String>;
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<String> = 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<String> = 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<String> = 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<String> = 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))
+66
View File
@@ -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))
}
+9
View File
@@ -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
}
+2
View File
@@ -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() {
+38 -2
View File
@@ -172,6 +172,8 @@ pub struct Vm {
pub(crate) exception_value: Option<JsValue>,
pub(crate) pending_exception: Option<JsValue>,
pub(crate) pending_error_kind: Option<&'static str>,
pub(crate) symbol_counter: u32,
pub(crate) symbol_descriptions: Vec<String>,
}
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<OxideKernel>) -> 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();
@@ -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<JsValue, String> {
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");
}
@@ -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<JsValue, String> {
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());
}