From 8bd7b097bf828009132a15c0b3fe983be443bf47 Mon Sep 17 00:00:00 2001 From: Pierre Le Fevre Date: Sun, 22 Mar 2026 13:46:18 +0100 Subject: [PATCH] Implement JS built-in String, Number, Boolean, and Symbol ## String - Constructor: String(), String.fromCharCode(), String.fromCodePoint() - Prototype: charAt, charCodeAt, codePointAt, concat, slice, substring, substr, indexOf, lastIndexOf, includes, startsWith, endsWith, trim, trimStart, trimEnd, padStart, padEnd, repeat, split, replace, replaceAll, toLowerCase, toUpperCase, at, toString, valueOf ## Number - Constructor: Number() - Static: isNaN, isFinite, isInteger, isSafeInteger, parseInt, parseFloat - Constants: EPSILON, MAX_SAFE_INTEGER, MIN_SAFE_INTEGER, MAX_VALUE, MIN_VALUE, NaN, POSITIVE_INFINITY, NEGATIVE_INFINITY - Prototype: toString(radix), valueOf, toFixed, toPrecision, toExponential ## Boolean - Constructor: Boolean() with truthiness coercion - Prototype: toString, valueOf ## Symbol - Factory: Symbol(description) producing unique string-based identifiers - Well-known: Symbol.iterator, toPrimitive, toStringTag, hasInstance - Registry: Symbol.for(key), Symbol.keyFor(sym) ## VM Changes - Added string_prototype, number_prototype, boolean_prototype to Vm - Primitive auto-boxing: GetProperty/GetPropertyByName now look up prototype methods for String, Number, and Boolean values - Global constants: NaN, Infinity, undefined 37 new tests (328 total JS tests, all passing) Co-Authored-By: Claude Opus 4.6 (1M context) --- crates/js/src/builtins.rs | 851 +++++++++++++++++++++++++++++++++++++- crates/js/src/vm.rs | 491 +++++++++++++++++++++- 2 files changed, 1339 insertions(+), 3 deletions(-) diff --git a/crates/js/src/builtins.rs b/crates/js/src/builtins.rs index 729abb9..aaf5769 100644 --- a/crates/js/src/builtins.rs +++ b/crates/js/src/builtins.rs @@ -8,6 +8,12 @@ use crate::gc::{Gc, GcRef}; use crate::vm::*; use std::collections::HashMap; +/// Native callback type alias to satisfy clippy::type_complexity. +type NativeMethod = ( + &'static str, + fn(&[Value], &mut NativeContext) -> Result, +); + // ── Helpers ────────────────────────────────────────────────── /// Create a native function GcRef. @@ -175,9 +181,30 @@ pub fn init_builtins(vm: &mut Vm) { let err_proto = vm.gc.alloc(HeapObject::Object(err_proto_data)); init_error_prototype(&mut vm.gc, err_proto); - // Store prototypes in VM for use by CreateArray/CreateObject. + // Create String.prototype (inherits from Object.prototype). + let mut str_proto_data = ObjectData::new(); + str_proto_data.prototype = Some(obj_proto); + let str_proto = vm.gc.alloc(HeapObject::Object(str_proto_data)); + init_string_prototype(&mut vm.gc, str_proto); + + // Create Number.prototype (inherits from Object.prototype). + let mut num_proto_data = ObjectData::new(); + num_proto_data.prototype = Some(obj_proto); + let num_proto = vm.gc.alloc(HeapObject::Object(num_proto_data)); + init_number_prototype(&mut vm.gc, num_proto); + + // Create Boolean.prototype (inherits from Object.prototype). + let mut bool_proto_data = ObjectData::new(); + bool_proto_data.prototype = Some(obj_proto); + let bool_proto = vm.gc.alloc(HeapObject::Object(bool_proto_data)); + init_boolean_prototype(&mut vm.gc, bool_proto); + + // Store prototypes in VM for use by CreateArray/CreateObject and auto-boxing. vm.object_prototype = Some(obj_proto); vm.array_prototype = Some(arr_proto); + vm.string_prototype = Some(str_proto); + vm.number_prototype = Some(num_proto); + vm.boolean_prototype = Some(bool_proto); // Create and register Object constructor. let obj_ctor = init_object_constructor(&mut vm.gc, obj_proto); @@ -190,6 +217,21 @@ pub fn init_builtins(vm: &mut Vm) { // Create and register Error constructors. init_error_constructors(vm, err_proto); + // Create and register String constructor. + let str_ctor = init_string_constructor(&mut vm.gc, str_proto); + vm.set_global("String", Value::Function(str_ctor)); + + // Create and register Number constructor. + let num_ctor = init_number_constructor(&mut vm.gc, num_proto); + vm.set_global("Number", Value::Function(num_ctor)); + + // Create and register Boolean constructor. + let bool_ctor = init_boolean_constructor(&mut vm.gc, bool_proto); + vm.set_global("Boolean", Value::Function(bool_ctor)); + + // Create and register Symbol factory. + init_symbol_builtins(vm); + // Register global utility functions. init_global_functions(vm); @@ -1420,6 +1462,813 @@ fn error_proto_to_string(_args: &[Value], ctx: &mut NativeContext) -> Result, proto: GcRef) { + let methods: &[NativeMethod] = &[ + ("charAt", string_proto_char_at), + ("charCodeAt", string_proto_char_code_at), + ("codePointAt", string_proto_code_point_at), + ("concat", string_proto_concat), + ("slice", string_proto_slice), + ("substring", string_proto_substring), + ("substr", string_proto_substr), + ("indexOf", string_proto_index_of), + ("lastIndexOf", string_proto_last_index_of), + ("includes", string_proto_includes), + ("startsWith", string_proto_starts_with), + ("endsWith", string_proto_ends_with), + ("trim", string_proto_trim), + ("trimStart", string_proto_trim_start), + ("trimEnd", string_proto_trim_end), + ("padStart", string_proto_pad_start), + ("padEnd", string_proto_pad_end), + ("repeat", string_proto_repeat), + ("split", string_proto_split), + ("replace", string_proto_replace), + ("replaceAll", string_proto_replace_all), + ("toLowerCase", string_proto_to_lower_case), + ("toUpperCase", string_proto_to_upper_case), + ("at", string_proto_at), + ("toString", string_proto_to_string), + ("valueOf", string_proto_value_of), + ]; + for &(name, callback) in methods { + let f = make_native(gc, name, callback); + set_builtin_prop(gc, proto, name, Value::Function(f)); + } +} + +fn init_string_constructor(gc: &mut Gc, str_proto: GcRef) -> GcRef { + let ctor = gc.alloc(HeapObject::Function(Box::new(FunctionData { + name: "String".to_string(), + kind: FunctionKind::Native(NativeFunc { + callback: string_constructor, + }), + prototype_obj: Some(str_proto), + properties: HashMap::new(), + upvalues: Vec::new(), + }))); + let from_char_code = make_native(gc, "fromCharCode", string_from_char_code); + set_func_prop(gc, ctor, "fromCharCode", Value::Function(from_char_code)); + let from_code_point = make_native(gc, "fromCodePoint", string_from_code_point); + set_func_prop(gc, ctor, "fromCodePoint", Value::Function(from_code_point)); + ctor +} + +fn string_constructor(args: &[Value], ctx: &mut NativeContext) -> Result { + let s = args + .first() + .map(|v| v.to_js_string(ctx.gc)) + .unwrap_or_default(); + Ok(Value::String(s)) +} + +fn string_from_char_code(args: &[Value], _ctx: &mut NativeContext) -> Result { + let s: String = args + .iter() + .filter_map(|v| { + let code = v.to_number() as u32; + char::from_u32(code) + }) + .collect(); + Ok(Value::String(s)) +} + +fn string_from_code_point(args: &[Value], _ctx: &mut NativeContext) -> Result { + let mut s = String::new(); + for v in args { + let code = v.to_number() as u32; + match char::from_u32(code) { + Some(c) => s.push(c), + None => { + return Err(RuntimeError::range_error(format!( + "Invalid code point {code}" + ))) + } + } + } + Ok(Value::String(s)) +} + +/// Helper: extract the string from `this` for String.prototype methods. +fn this_string(ctx: &NativeContext) -> String { + ctx.this.to_js_string(ctx.gc) +} + +/// Helper: get chars as a Vec for index-based operations. +fn str_chars(s: &str) -> Vec { + s.chars().collect() +} + +fn string_proto_char_at(args: &[Value], ctx: &mut NativeContext) -> Result { + let s = this_string(ctx); + let chars = str_chars(&s); + let idx = args.first().map(|v| v.to_number() as i64).unwrap_or(0); + if idx < 0 || idx as usize >= chars.len() { + Ok(Value::String(String::new())) + } else { + Ok(Value::String(chars[idx as usize].to_string())) + } +} + +fn string_proto_char_code_at( + args: &[Value], + ctx: &mut NativeContext, +) -> Result { + let s = this_string(ctx); + let chars = str_chars(&s); + let idx = args.first().map(|v| v.to_number() as i64).unwrap_or(0); + if idx < 0 || idx as usize >= chars.len() { + Ok(Value::Number(f64::NAN)) + } else { + Ok(Value::Number(chars[idx as usize] as u32 as f64)) + } +} + +fn string_proto_code_point_at( + args: &[Value], + ctx: &mut NativeContext, +) -> Result { + let s = this_string(ctx); + let chars = str_chars(&s); + let idx = args.first().map(|v| v.to_number() as i64).unwrap_or(0); + if idx < 0 || idx as usize >= chars.len() { + Ok(Value::Undefined) + } else { + Ok(Value::Number(chars[idx as usize] as u32 as f64)) + } +} + +fn string_proto_concat(args: &[Value], ctx: &mut NativeContext) -> Result { + let mut s = this_string(ctx); + for arg in args { + s.push_str(&arg.to_js_string(ctx.gc)); + } + Ok(Value::String(s)) +} + +fn string_proto_slice(args: &[Value], ctx: &mut NativeContext) -> Result { + let s = this_string(ctx); + let chars = str_chars(&s); + let len = chars.len() as i64; + let start = args.first().map(|v| v.to_number() as i64).unwrap_or(0); + let end = args.get(1).map(|v| v.to_number() as i64).unwrap_or(len); + let start = if start < 0 { + (len + start).max(0) as usize + } else { + start.min(len) as usize + }; + let end = if end < 0 { + (len + end).max(0) as usize + } else { + end.min(len) as usize + }; + if start >= end { + Ok(Value::String(String::new())) + } else { + Ok(Value::String(chars[start..end].iter().collect())) + } +} + +fn string_proto_substring(args: &[Value], ctx: &mut NativeContext) -> Result { + let s = this_string(ctx); + let chars = str_chars(&s); + let len = chars.len() as i64; + let a = args + .first() + .map(|v| v.to_number() as i64) + .unwrap_or(0) + .clamp(0, len) as usize; + let b = args + .get(1) + .map(|v| v.to_number() as i64) + .unwrap_or(len) + .clamp(0, len) as usize; + let (start, end) = if a <= b { (a, b) } else { (b, a) }; + Ok(Value::String(chars[start..end].iter().collect())) +} + +fn string_proto_substr(args: &[Value], ctx: &mut NativeContext) -> Result { + let s = this_string(ctx); + let chars = str_chars(&s); + let len = chars.len() as i64; + let start = args.first().map(|v| v.to_number() as i64).unwrap_or(0); + let start = if start < 0 { + (len + start).max(0) as usize + } else { + start.min(len) as usize + }; + let count = args.get(1).map(|v| v.to_number() as i64).unwrap_or(len) as usize; + let end = (start + count).min(chars.len()); + Ok(Value::String(chars[start..end].iter().collect())) +} + +fn string_proto_index_of(args: &[Value], ctx: &mut NativeContext) -> Result { + let s = this_string(ctx); + let search = args + .first() + .map(|v| v.to_js_string(ctx.gc)) + .unwrap_or_default(); + let from = args.get(1).map(|v| v.to_number() as usize).unwrap_or(0); + let chars = str_chars(&s); + let search_chars = str_chars(&search); + if search_chars.is_empty() { + return Ok(Value::Number(from.min(chars.len()) as f64)); + } + for i in from..chars.len() { + if i + search_chars.len() <= chars.len() + && chars[i..i + search_chars.len()] == search_chars[..] + { + return Ok(Value::Number(i as f64)); + } + } + Ok(Value::Number(-1.0)) +} + +fn string_proto_last_index_of( + args: &[Value], + ctx: &mut NativeContext, +) -> Result { + let s = this_string(ctx); + let search = args + .first() + .map(|v| v.to_js_string(ctx.gc)) + .unwrap_or_default(); + let chars = str_chars(&s); + let search_chars = str_chars(&search); + let from = args + .get(1) + .map(|v| { + let n = v.to_number(); + if n.is_nan() { + chars.len() + } else { + n as usize + } + }) + .unwrap_or(chars.len()); + if search_chars.is_empty() { + return Ok(Value::Number(from.min(chars.len()) as f64)); + } + let max_start = from.min(chars.len().saturating_sub(search_chars.len())); + for i in (0..=max_start).rev() { + if i + search_chars.len() <= chars.len() + && chars[i..i + search_chars.len()] == search_chars[..] + { + return Ok(Value::Number(i as f64)); + } + } + Ok(Value::Number(-1.0)) +} + +fn string_proto_includes(args: &[Value], ctx: &mut NativeContext) -> Result { + let s = this_string(ctx); + let search = args + .first() + .map(|v| v.to_js_string(ctx.gc)) + .unwrap_or_default(); + let from = args.get(1).map(|v| v.to_number() as usize).unwrap_or(0); + let chars = str_chars(&s); + let search_chars = str_chars(&search); + if search_chars.is_empty() { + return Ok(Value::Boolean(true)); + } + for i in from..chars.len() { + if i + search_chars.len() <= chars.len() + && chars[i..i + search_chars.len()] == search_chars[..] + { + return Ok(Value::Boolean(true)); + } + } + Ok(Value::Boolean(false)) +} + +fn string_proto_starts_with( + args: &[Value], + ctx: &mut NativeContext, +) -> Result { + let s = this_string(ctx); + let search = args + .first() + .map(|v| v.to_js_string(ctx.gc)) + .unwrap_or_default(); + let pos = args.get(1).map(|v| v.to_number() as usize).unwrap_or(0); + let chars = str_chars(&s); + let search_chars = str_chars(&search); + if pos + search_chars.len() > chars.len() { + return Ok(Value::Boolean(false)); + } + Ok(Value::Boolean( + chars[pos..pos + search_chars.len()] == search_chars[..], + )) +} + +fn string_proto_ends_with(args: &[Value], ctx: &mut NativeContext) -> Result { + let s = this_string(ctx); + let search = args + .first() + .map(|v| v.to_js_string(ctx.gc)) + .unwrap_or_default(); + let chars = str_chars(&s); + let search_chars = str_chars(&search); + let end_pos = args + .get(1) + .map(|v| (v.to_number() as usize).min(chars.len())) + .unwrap_or(chars.len()); + if search_chars.len() > end_pos { + return Ok(Value::Boolean(false)); + } + let start = end_pos - search_chars.len(); + Ok(Value::Boolean(chars[start..end_pos] == search_chars[..])) +} + +fn string_proto_trim(args: &[Value], ctx: &mut NativeContext) -> Result { + let _ = args; + Ok(Value::String(this_string(ctx).trim().to_string())) +} + +fn string_proto_trim_start(args: &[Value], ctx: &mut NativeContext) -> Result { + let _ = args; + Ok(Value::String(this_string(ctx).trim_start().to_string())) +} + +fn string_proto_trim_end(args: &[Value], ctx: &mut NativeContext) -> Result { + let _ = args; + Ok(Value::String(this_string(ctx).trim_end().to_string())) +} + +fn string_proto_pad_start(args: &[Value], ctx: &mut NativeContext) -> Result { + let s = this_string(ctx); + let target_len = args.first().map(|v| v.to_number() as usize).unwrap_or(0); + let fill = args + .get(1) + .map(|v| v.to_js_string(ctx.gc)) + .unwrap_or_else(|| " ".to_string()); + let chars = str_chars(&s); + if chars.len() >= target_len || fill.is_empty() { + return Ok(Value::String(s)); + } + let fill_chars = str_chars(&fill); + let needed = target_len - chars.len(); + let mut pad = String::new(); + for i in 0..needed { + pad.push(fill_chars[i % fill_chars.len()]); + } + pad.push_str(&s); + Ok(Value::String(pad)) +} + +fn string_proto_pad_end(args: &[Value], ctx: &mut NativeContext) -> Result { + let s = this_string(ctx); + let target_len = args.first().map(|v| v.to_number() as usize).unwrap_or(0); + let fill = args + .get(1) + .map(|v| v.to_js_string(ctx.gc)) + .unwrap_or_else(|| " ".to_string()); + let chars = str_chars(&s); + if chars.len() >= target_len || fill.is_empty() { + return Ok(Value::String(s)); + } + let fill_chars = str_chars(&fill); + let needed = target_len - chars.len(); + let mut result = s; + for i in 0..needed { + result.push(fill_chars[i % fill_chars.len()]); + } + Ok(Value::String(result)) +} + +fn string_proto_repeat(args: &[Value], ctx: &mut NativeContext) -> Result { + let s = this_string(ctx); + let count = args.first().map(|v| v.to_number()).unwrap_or(0.0); + if count < 0.0 || count.is_infinite() { + return Err(RuntimeError::range_error("Invalid count value")); + } + Ok(Value::String(s.repeat(count as usize))) +} + +fn string_proto_split(args: &[Value], ctx: &mut NativeContext) -> Result { + let s = this_string(ctx); + if args.is_empty() || matches!(args.first(), Some(Value::Undefined)) { + return Ok(make_value_array(ctx.gc, &[Value::String(s)])); + } + let sep = args[0].to_js_string(ctx.gc); + let limit = args + .get(1) + .map(|v| v.to_number() as usize) + .unwrap_or(usize::MAX); + if sep.is_empty() { + let items: Vec = str_chars(&s) + .into_iter() + .take(limit) + .map(|c| Value::String(c.to_string())) + .collect(); + return Ok(make_value_array(ctx.gc, &items)); + } + let mut items = Vec::new(); + let mut start = 0; + let sep_len = sep.len(); + while let Some(pos) = s[start..].find(&sep) { + if items.len() >= limit { + break; + } + items.push(Value::String(s[start..start + pos].to_string())); + start += pos + sep_len; + } + if items.len() < limit { + items.push(Value::String(s[start..].to_string())); + } + Ok(make_value_array(ctx.gc, &items)) +} + +fn string_proto_replace(args: &[Value], ctx: &mut NativeContext) -> Result { + let s = this_string(ctx); + let search = args + .first() + .map(|v| v.to_js_string(ctx.gc)) + .unwrap_or_default(); + let replacement = args + .get(1) + .map(|v| v.to_js_string(ctx.gc)) + .unwrap_or_default(); + // Replace only the first occurrence. + if let Some(pos) = s.find(&search) { + let mut result = String::with_capacity(s.len()); + result.push_str(&s[..pos]); + result.push_str(&replacement); + result.push_str(&s[pos + search.len()..]); + Ok(Value::String(result)) + } else { + Ok(Value::String(s)) + } +} + +fn string_proto_replace_all( + args: &[Value], + ctx: &mut NativeContext, +) -> Result { + let s = this_string(ctx); + let search = args + .first() + .map(|v| v.to_js_string(ctx.gc)) + .unwrap_or_default(); + let replacement = args + .get(1) + .map(|v| v.to_js_string(ctx.gc)) + .unwrap_or_default(); + Ok(Value::String(s.replace(&search, &replacement))) +} + +fn string_proto_to_lower_case( + args: &[Value], + ctx: &mut NativeContext, +) -> Result { + let _ = args; + Ok(Value::String(this_string(ctx).to_lowercase())) +} + +fn string_proto_to_upper_case( + args: &[Value], + ctx: &mut NativeContext, +) -> Result { + let _ = args; + Ok(Value::String(this_string(ctx).to_uppercase())) +} + +fn string_proto_at(args: &[Value], ctx: &mut NativeContext) -> Result { + let s = this_string(ctx); + let chars = str_chars(&s); + let idx = args.first().map(|v| v.to_number() as i64).unwrap_or(0); + let actual = if idx < 0 { + chars.len() as i64 + idx + } else { + idx + }; + if actual < 0 || actual as usize >= chars.len() { + Ok(Value::Undefined) + } else { + Ok(Value::String(chars[actual as usize].to_string())) + } +} + +fn string_proto_to_string(args: &[Value], ctx: &mut NativeContext) -> Result { + let _ = args; + Ok(Value::String(this_string(ctx))) +} + +fn string_proto_value_of(args: &[Value], ctx: &mut NativeContext) -> Result { + let _ = args; + Ok(Value::String(this_string(ctx))) +} + +// ── Number built-in ────────────────────────────────────────── + +fn init_number_prototype(gc: &mut Gc, proto: GcRef) { + let methods: &[NativeMethod] = &[ + ("toString", number_proto_to_string), + ("valueOf", number_proto_value_of), + ("toFixed", number_proto_to_fixed), + ("toPrecision", number_proto_to_precision), + ("toExponential", number_proto_to_exponential), + ]; + for &(name, callback) in methods { + let f = make_native(gc, name, callback); + set_builtin_prop(gc, proto, name, Value::Function(f)); + } +} + +fn init_number_constructor(gc: &mut Gc, num_proto: GcRef) -> GcRef { + let ctor = gc.alloc(HeapObject::Function(Box::new(FunctionData { + name: "Number".to_string(), + kind: FunctionKind::Native(NativeFunc { + callback: number_constructor, + }), + prototype_obj: Some(num_proto), + properties: HashMap::new(), + upvalues: Vec::new(), + }))); + // Static methods. + let is_nan = make_native(gc, "isNaN", number_is_nan); + set_func_prop(gc, ctor, "isNaN", Value::Function(is_nan)); + let is_finite = make_native(gc, "isFinite", number_is_finite); + set_func_prop(gc, ctor, "isFinite", Value::Function(is_finite)); + let is_integer = make_native(gc, "isInteger", number_is_integer); + set_func_prop(gc, ctor, "isInteger", Value::Function(is_integer)); + let is_safe_integer = make_native(gc, "isSafeInteger", number_is_safe_integer); + set_func_prop(gc, ctor, "isSafeInteger", Value::Function(is_safe_integer)); + let parse_int_fn = make_native(gc, "parseInt", crate::builtins::parse_int); + set_func_prop(gc, ctor, "parseInt", Value::Function(parse_int_fn)); + let parse_float_fn = make_native(gc, "parseFloat", crate::builtins::parse_float); + set_func_prop(gc, ctor, "parseFloat", Value::Function(parse_float_fn)); + // Constants. + set_func_prop(gc, ctor, "EPSILON", Value::Number(f64::EPSILON)); + set_func_prop( + gc, + ctor, + "MAX_SAFE_INTEGER", + Value::Number(9007199254740991.0), + ); + set_func_prop( + gc, + ctor, + "MIN_SAFE_INTEGER", + Value::Number(-9007199254740991.0), + ); + set_func_prop(gc, ctor, "MAX_VALUE", Value::Number(f64::MAX)); + set_func_prop(gc, ctor, "MIN_VALUE", Value::Number(f64::MIN_POSITIVE)); + set_func_prop(gc, ctor, "NaN", Value::Number(f64::NAN)); + set_func_prop(gc, ctor, "POSITIVE_INFINITY", Value::Number(f64::INFINITY)); + set_func_prop( + gc, + ctor, + "NEGATIVE_INFINITY", + Value::Number(f64::NEG_INFINITY), + ); + ctor +} + +fn number_constructor(args: &[Value], _ctx: &mut NativeContext) -> Result { + let n = args.first().map(|v| v.to_number()).unwrap_or(0.0); + Ok(Value::Number(n)) +} + +fn number_is_nan(args: &[Value], _ctx: &mut NativeContext) -> Result { + match args.first() { + Some(Value::Number(n)) => Ok(Value::Boolean(n.is_nan())), + _ => Ok(Value::Boolean(false)), + } +} + +fn number_is_finite(args: &[Value], _ctx: &mut NativeContext) -> Result { + match args.first() { + Some(Value::Number(n)) => Ok(Value::Boolean(n.is_finite())), + _ => Ok(Value::Boolean(false)), + } +} + +fn number_is_integer(args: &[Value], _ctx: &mut NativeContext) -> Result { + match args.first() { + Some(Value::Number(n)) => Ok(Value::Boolean(n.is_finite() && n.trunc() == *n)), + _ => Ok(Value::Boolean(false)), + } +} + +fn number_is_safe_integer(args: &[Value], _ctx: &mut NativeContext) -> Result { + match args.first() { + Some(Value::Number(n)) => { + let safe = n.is_finite() && n.trunc() == *n && n.abs() <= 9007199254740991.0; + Ok(Value::Boolean(safe)) + } + _ => Ok(Value::Boolean(false)), + } +} + +/// Helper: extract the number from `this` for Number.prototype methods. +fn this_number(ctx: &NativeContext) -> f64 { + ctx.this.to_number() +} + +fn number_proto_to_string(args: &[Value], ctx: &mut NativeContext) -> Result { + let n = this_number(ctx); + let radix = args.first().map(|v| v.to_number() as u32).unwrap_or(10); + if !(2..=36).contains(&radix) { + return Err(RuntimeError::range_error( + "toString() radix must be between 2 and 36", + )); + } + if radix == 10 { + return Ok(Value::String(Value::Number(n).to_js_string(ctx.gc))); + } + if n.is_nan() { + return Ok(Value::String("NaN".to_string())); + } + if n.is_infinite() { + return Ok(Value::String(if n > 0.0 { + "Infinity".to_string() + } else { + "-Infinity".to_string() + })); + } + // Integer path for non-decimal radix. + let neg = n < 0.0; + let abs = n.abs() as u64; + let mut digits = Vec::new(); + let mut val = abs; + if val == 0 { + digits.push('0'); + } else { + while val > 0 { + let d = (val % radix as u64) as u32; + digits.push(char::from_digit(d, radix).unwrap_or('?')); + val /= radix as u64; + } + } + digits.reverse(); + let mut result = String::new(); + if neg { + result.push('-'); + } + result.extend(digits); + Ok(Value::String(result)) +} + +fn number_proto_value_of(args: &[Value], ctx: &mut NativeContext) -> Result { + let _ = args; + Ok(Value::Number(this_number(ctx))) +} + +fn number_proto_to_fixed(args: &[Value], ctx: &mut NativeContext) -> Result { + let n = this_number(ctx); + let digits = args.first().map(|v| v.to_number() as usize).unwrap_or(0); + if digits > 100 { + return Err(RuntimeError::range_error( + "toFixed() digits argument must be between 0 and 100", + )); + } + Ok(Value::String(format!("{n:.digits$}"))) +} + +fn number_proto_to_precision( + args: &[Value], + ctx: &mut NativeContext, +) -> Result { + let n = this_number(ctx); + if args.is_empty() || matches!(args.first(), Some(Value::Undefined)) { + return Ok(Value::String(Value::Number(n).to_js_string(ctx.gc))); + } + let prec = args[0].to_number() as usize; + if !(1..=100).contains(&prec) { + return Err(RuntimeError::range_error( + "toPrecision() argument must be between 1 and 100", + )); + } + Ok(Value::String(format!("{n:.prec$e}"))) +} + +fn number_proto_to_exponential( + args: &[Value], + ctx: &mut NativeContext, +) -> Result { + let n = this_number(ctx); + let digits = args.first().map(|v| v.to_number() as usize).unwrap_or(6); + if digits > 100 { + return Err(RuntimeError::range_error( + "toExponential() argument must be between 0 and 100", + )); + } + Ok(Value::String(format!("{n:.digits$e}"))) +} + +// ── Boolean built-in ───────────────────────────────────────── + +fn init_boolean_prototype(gc: &mut Gc, proto: GcRef) { + let to_string = make_native(gc, "toString", boolean_proto_to_string); + set_builtin_prop(gc, proto, "toString", Value::Function(to_string)); + let value_of = make_native(gc, "valueOf", boolean_proto_value_of); + set_builtin_prop(gc, proto, "valueOf", Value::Function(value_of)); +} + +fn init_boolean_constructor(gc: &mut Gc, bool_proto: GcRef) -> GcRef { + gc.alloc(HeapObject::Function(Box::new(FunctionData { + name: "Boolean".to_string(), + kind: FunctionKind::Native(NativeFunc { + callback: boolean_constructor, + }), + prototype_obj: Some(bool_proto), + properties: HashMap::new(), + upvalues: Vec::new(), + }))) +} + +fn boolean_constructor(args: &[Value], _ctx: &mut NativeContext) -> Result { + let b = args.first().map(|v| v.to_boolean()).unwrap_or(false); + Ok(Value::Boolean(b)) +} + +fn boolean_proto_to_string(args: &[Value], ctx: &mut NativeContext) -> Result { + let _ = args; + Ok(Value::String( + if ctx.this.to_boolean() { + "true" + } else { + "false" + } + .to_string(), + )) +} + +fn boolean_proto_value_of(args: &[Value], ctx: &mut NativeContext) -> Result { + let _ = args; + Ok(Value::Boolean(ctx.this.to_boolean())) +} + +// ── Symbol built-in ────────────────────────────────────────── + +/// Global symbol ID counter. Each Symbol() call increments this. +static SYMBOL_COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); + +fn init_symbol_builtins(vm: &mut Vm) { + // Create a function object so we can hang static props on it. + let gc_ref = make_native(&mut vm.gc, "Symbol", symbol_factory); + + // Well-known symbols as string constants. + let well_known = [ + ("iterator", "@@iterator"), + ("toPrimitive", "@@toPrimitive"), + ("toStringTag", "@@toStringTag"), + ("hasInstance", "@@hasInstance"), + ]; + for (name, value) in well_known { + set_func_prop(&mut vm.gc, gc_ref, name, Value::String(value.to_string())); + } + + // Symbol.for() and Symbol.keyFor(). + let sym_for = make_native(&mut vm.gc, "for", symbol_for); + set_func_prop(&mut vm.gc, gc_ref, "for", Value::Function(sym_for)); + let sym_key_for = make_native(&mut vm.gc, "keyFor", symbol_key_for); + set_func_prop(&mut vm.gc, gc_ref, "keyFor", Value::Function(sym_key_for)); + + vm.set_global("Symbol", Value::Function(gc_ref)); + + // Register global NaN and Infinity constants. + vm.set_global("NaN", Value::Number(f64::NAN)); + vm.set_global("Infinity", Value::Number(f64::INFINITY)); + vm.set_global("undefined", Value::Undefined); +} + +fn symbol_factory(args: &[Value], ctx: &mut NativeContext) -> Result { + let desc = args + .first() + .map(|v| v.to_js_string(ctx.gc)) + .unwrap_or_default(); + let id = SYMBOL_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed); + // Return a unique string representation. Real Symbol is a distinct type, + // but this pragmatic approach works for property keys and identity checks. + Ok(Value::String(format!("@@sym_{id}_{desc}"))) +} + +fn symbol_for(args: &[Value], ctx: &mut NativeContext) -> Result { + let key = args + .first() + .map(|v| v.to_js_string(ctx.gc)) + .unwrap_or_default(); + // Deterministic: same key always produces same symbol string. + Ok(Value::String(format!("@@global_{key}"))) +} + +fn symbol_key_for(args: &[Value], ctx: &mut NativeContext) -> Result { + let sym = args + .first() + .map(|v| v.to_js_string(ctx.gc)) + .unwrap_or_default(); + if let Some(key) = sym.strip_prefix("@@global_") { + Ok(Value::String(key.to_string())) + } else { + Ok(Value::Undefined) + } +} + // ── Global utility functions ───────────────────────────────── fn init_global_functions(vm: &mut Vm) { diff --git a/crates/js/src/vm.rs b/crates/js/src/vm.rs index 41f6168..4be5521 100644 --- a/crates/js/src/vm.rs +++ b/crates/js/src/vm.rs @@ -682,6 +682,12 @@ pub struct Vm { pub object_prototype: Option, /// Built-in Array.prototype (set on newly created arrays). pub array_prototype: Option, + /// Built-in String.prototype (for primitive auto-boxing). + pub string_prototype: Option, + /// Built-in Number.prototype (for primitive auto-boxing). + pub number_prototype: Option, + /// Built-in Boolean.prototype (for primitive auto-boxing). + pub boolean_prototype: Option, } /// Maximum register file size. @@ -700,6 +706,9 @@ impl Vm { instructions_executed: 0, object_prototype: None, array_prototype: None, + string_prototype: None, + number_prototype: None, + boolean_prototype: None, }; crate::builtins::init_builtins(&mut vm); vm @@ -793,6 +802,15 @@ impl Vm { if let Some(r) = self.array_prototype { roots.push(r); } + if let Some(r) = self.string_prototype { + roots.push(r); + } + if let Some(r) = self.number_prototype { + roots.push(r); + } + if let Some(r) = self.boolean_prototype { + roots.push(r); + } roots } @@ -1351,7 +1369,24 @@ impl Vm { Value::Object(gc_ref) | Value::Function(gc_ref) => { gc_get_property(&self.gc, gc_ref, &key) } - Value::String(ref s) => string_get_property(s, &key), + Value::String(ref s) => { + let v = string_get_property(s, &key); + if matches!(v, Value::Undefined) { + self.string_prototype + .map(|p| gc_get_property(&self.gc, p, &key)) + .unwrap_or(Value::Undefined) + } else { + v + } + } + Value::Number(_) => self + .number_prototype + .map(|p| gc_get_property(&self.gc, p, &key)) + .unwrap_or(Value::Undefined), + Value::Boolean(_) => self + .boolean_prototype + .map(|p| gc_get_property(&self.gc, p, &key)) + .unwrap_or(Value::Undefined), _ => Value::Undefined, }; self.registers[base + dst as usize] = val; @@ -1434,7 +1469,24 @@ impl Vm { Value::Object(gc_ref) | Value::Function(gc_ref) => { gc_get_property(&self.gc, gc_ref, &key) } - Value::String(ref s) => string_get_property(s, &key), + Value::String(ref s) => { + let v = string_get_property(s, &key); + if matches!(v, Value::Undefined) { + self.string_prototype + .map(|p| gc_get_property(&self.gc, p, &key)) + .unwrap_or(Value::Undefined) + } else { + v + } + } + Value::Number(_) => self + .number_prototype + .map(|p| gc_get_property(&self.gc, p, &key)) + .unwrap_or(Value::Undefined), + Value::Boolean(_) => self + .boolean_prototype + .map(|p| gc_get_property(&self.gc, p, &key)) + .unwrap_or(Value::Undefined), _ => Value::Undefined, }; self.registers[base + dst as usize] = val; @@ -3255,4 +3307,439 @@ mod tests { v => panic!("expected true, got {v:?}"), } } + + // ── String built-in tests ───────────────────────────────── + + #[test] + fn test_string_constructor() { + match eval("String(42)").unwrap() { + Value::String(s) => assert_eq!(s, "42"), + v => panic!("expected '42', got {v:?}"), + } + match eval("String(true)").unwrap() { + Value::String(s) => assert_eq!(s, "true"), + v => panic!("expected 'true', got {v:?}"), + } + match eval("String()").unwrap() { + Value::String(s) => assert_eq!(s, ""), + v => panic!("expected '', got {v:?}"), + } + } + + #[test] + fn test_string_length() { + match eval("'hello'.length").unwrap() { + Value::Number(n) => assert_eq!(n, 5.0), + v => panic!("expected 5, got {v:?}"), + } + } + + #[test] + fn test_string_char_at() { + match eval("'hello'.charAt(1)").unwrap() { + Value::String(s) => assert_eq!(s, "e"), + v => panic!("expected 'e', got {v:?}"), + } + match eval("'hello'.charAt(10)").unwrap() { + Value::String(s) => assert_eq!(s, ""), + v => panic!("expected '', got {v:?}"), + } + } + + #[test] + fn test_string_char_code_at() { + match eval("'A'.charCodeAt(0)").unwrap() { + Value::Number(n) => assert_eq!(n, 65.0), + v => panic!("expected 65, got {v:?}"), + } + } + + #[test] + fn test_string_proto_concat() { + match eval("'hello'.concat(' ', 'world')").unwrap() { + Value::String(s) => assert_eq!(s, "hello world"), + v => panic!("expected 'hello world', got {v:?}"), + } + } + + #[test] + fn test_string_slice() { + match eval("'hello world'.slice(6)").unwrap() { + Value::String(s) => assert_eq!(s, "world"), + v => panic!("expected 'world', got {v:?}"), + } + match eval("'hello'.slice(1, 3)").unwrap() { + Value::String(s) => assert_eq!(s, "el"), + v => panic!("expected 'el', got {v:?}"), + } + match eval("'hello'.slice(-3)").unwrap() { + Value::String(s) => assert_eq!(s, "llo"), + v => panic!("expected 'llo', got {v:?}"), + } + } + + #[test] + fn test_string_substring() { + match eval("'hello'.substring(1, 3)").unwrap() { + Value::String(s) => assert_eq!(s, "el"), + v => panic!("expected 'el', got {v:?}"), + } + // substring swaps args if start > end + match eval("'hello'.substring(3, 1)").unwrap() { + Value::String(s) => assert_eq!(s, "el"), + v => panic!("expected 'el', got {v:?}"), + } + } + + #[test] + fn test_string_index_of() { + match eval("'hello world'.indexOf('world')").unwrap() { + Value::Number(n) => assert_eq!(n, 6.0), + v => panic!("expected 6, got {v:?}"), + } + match eval("'hello'.indexOf('xyz')").unwrap() { + Value::Number(n) => assert_eq!(n, -1.0), + v => panic!("expected -1, got {v:?}"), + } + } + + #[test] + fn test_string_last_index_of() { + match eval("'abcabc'.lastIndexOf('abc')").unwrap() { + Value::Number(n) => assert_eq!(n, 3.0), + v => panic!("expected 3, got {v:?}"), + } + } + + #[test] + fn test_string_includes() { + match eval("'hello world'.includes('world')").unwrap() { + Value::Boolean(b) => assert!(b), + v => panic!("expected true, got {v:?}"), + } + match eval("'hello'.includes('xyz')").unwrap() { + Value::Boolean(b) => assert!(!b), + v => panic!("expected false, got {v:?}"), + } + } + + #[test] + fn test_string_starts_ends_with() { + match eval("'hello'.startsWith('hel')").unwrap() { + Value::Boolean(b) => assert!(b), + v => panic!("expected true, got {v:?}"), + } + match eval("'hello'.endsWith('llo')").unwrap() { + Value::Boolean(b) => assert!(b), + v => panic!("expected true, got {v:?}"), + } + } + + #[test] + fn test_string_trim() { + match eval("' hello '.trim()").unwrap() { + Value::String(s) => assert_eq!(s, "hello"), + v => panic!("expected 'hello', got {v:?}"), + } + match eval("' hello '.trimStart()").unwrap() { + Value::String(s) => assert_eq!(s, "hello "), + v => panic!("expected 'hello ', got {v:?}"), + } + match eval("' hello '.trimEnd()").unwrap() { + Value::String(s) => assert_eq!(s, " hello"), + v => panic!("expected ' hello', got {v:?}"), + } + } + + #[test] + fn test_string_pad() { + match eval("'5'.padStart(3, '0')").unwrap() { + Value::String(s) => assert_eq!(s, "005"), + v => panic!("expected '005', got {v:?}"), + } + match eval("'5'.padEnd(3, '0')").unwrap() { + Value::String(s) => assert_eq!(s, "500"), + v => panic!("expected '500', got {v:?}"), + } + } + + #[test] + fn test_string_repeat() { + match eval("'ab'.repeat(3)").unwrap() { + Value::String(s) => assert_eq!(s, "ababab"), + v => panic!("expected 'ababab', got {v:?}"), + } + } + + #[test] + fn test_string_split() { + // split returns an array; verify length and elements. + match eval("'a,b,c'.split(',').length").unwrap() { + Value::Number(n) => assert_eq!(n, 3.0), + v => panic!("expected 3, got {v:?}"), + } + match eval("'a,b,c'.split(',')[0]").unwrap() { + Value::String(s) => assert_eq!(s, "a"), + v => panic!("expected 'a', got {v:?}"), + } + match eval("'a,b,c'.split(',')[2]").unwrap() { + Value::String(s) => assert_eq!(s, "c"), + v => panic!("expected 'c', got {v:?}"), + } + } + + #[test] + fn test_string_replace() { + match eval("'hello world'.replace('world', 'there')").unwrap() { + Value::String(s) => assert_eq!(s, "hello there"), + v => panic!("expected 'hello there', got {v:?}"), + } + } + + #[test] + fn test_string_replace_all() { + match eval("'aabbcc'.replaceAll('b', 'x')").unwrap() { + Value::String(s) => assert_eq!(s, "aaxxcc"), + v => panic!("expected 'aaxxcc', got {v:?}"), + } + } + + #[test] + fn test_string_case() { + match eval("'Hello'.toLowerCase()").unwrap() { + Value::String(s) => assert_eq!(s, "hello"), + v => panic!("expected 'hello', got {v:?}"), + } + match eval("'Hello'.toUpperCase()").unwrap() { + Value::String(s) => assert_eq!(s, "HELLO"), + v => panic!("expected 'HELLO', got {v:?}"), + } + } + + #[test] + fn test_string_at() { + match eval("'hello'.at(0)").unwrap() { + Value::String(s) => assert_eq!(s, "h"), + v => panic!("expected 'h', got {v:?}"), + } + match eval("'hello'.at(-1)").unwrap() { + Value::String(s) => assert_eq!(s, "o"), + v => panic!("expected 'o', got {v:?}"), + } + } + + #[test] + fn test_string_from_char_code() { + match eval("String.fromCharCode(72, 101, 108)").unwrap() { + Value::String(s) => assert_eq!(s, "Hel"), + v => panic!("expected 'Hel', got {v:?}"), + } + } + + #[test] + fn test_string_from_code_point() { + match eval("String.fromCodePoint(65, 66, 67)").unwrap() { + Value::String(s) => assert_eq!(s, "ABC"), + v => panic!("expected 'ABC', got {v:?}"), + } + } + + // ── Number built-in tests ───────────────────────────────── + + #[test] + fn test_number_constructor() { + match eval("Number('42')").unwrap() { + Value::Number(n) => assert_eq!(n, 42.0), + v => panic!("expected 42, got {v:?}"), + } + match eval("Number(true)").unwrap() { + Value::Number(n) => assert_eq!(n, 1.0), + v => panic!("expected 1, got {v:?}"), + } + match eval("Number()").unwrap() { + Value::Number(n) => assert_eq!(n, 0.0), + v => panic!("expected 0, got {v:?}"), + } + } + + #[test] + fn test_number_is_nan() { + match eval("Number.isNaN(NaN)").unwrap() { + Value::Boolean(b) => assert!(b), + v => panic!("expected true, got {v:?}"), + } + match eval("Number.isNaN(42)").unwrap() { + Value::Boolean(b) => assert!(!b), + v => panic!("expected false, got {v:?}"), + } + // Number.isNaN doesn't coerce — string "NaN" is not NaN. + match eval("Number.isNaN('NaN')").unwrap() { + Value::Boolean(b) => assert!(!b), + v => panic!("expected false, got {v:?}"), + } + } + + #[test] + fn test_number_is_finite() { + match eval("Number.isFinite(42)").unwrap() { + Value::Boolean(b) => assert!(b), + v => panic!("expected true, got {v:?}"), + } + match eval("Number.isFinite(Infinity)").unwrap() { + Value::Boolean(b) => assert!(!b), + v => panic!("expected false, got {v:?}"), + } + } + + #[test] + fn test_number_is_integer() { + match eval("Number.isInteger(42)").unwrap() { + Value::Boolean(b) => assert!(b), + v => panic!("expected true, got {v:?}"), + } + match eval("Number.isInteger(42.5)").unwrap() { + Value::Boolean(b) => assert!(!b), + v => panic!("expected false, got {v:?}"), + } + } + + #[test] + fn test_number_is_safe_integer() { + match eval("Number.isSafeInteger(42)").unwrap() { + Value::Boolean(b) => assert!(b), + v => panic!("expected true, got {v:?}"), + } + match eval("Number.isSafeInteger(9007199254740992)").unwrap() { + Value::Boolean(b) => assert!(!b), + v => panic!("expected false, got {v:?}"), + } + } + + #[test] + fn test_number_constants() { + match eval("Number.MAX_SAFE_INTEGER").unwrap() { + Value::Number(n) => assert_eq!(n, 9007199254740991.0), + v => panic!("expected MAX_SAFE_INTEGER, got {v:?}"), + } + match eval("Number.EPSILON").unwrap() { + Value::Number(n) => assert_eq!(n, f64::EPSILON), + v => panic!("expected EPSILON, got {v:?}"), + } + } + + #[test] + fn test_number_to_fixed() { + match eval("var n = 3.14159; n.toFixed(2)").unwrap() { + Value::String(s) => assert_eq!(s, "3.14"), + v => panic!("expected '3.14', got {v:?}"), + } + } + + #[test] + fn test_number_to_string_radix() { + match eval("var n = 255; n.toString(16)").unwrap() { + Value::String(s) => assert_eq!(s, "ff"), + v => panic!("expected 'ff', got {v:?}"), + } + match eval("var n = 10; n.toString(2)").unwrap() { + Value::String(s) => assert_eq!(s, "1010"), + v => panic!("expected '1010', got {v:?}"), + } + } + + #[test] + fn test_number_parse_int() { + match eval("Number.parseInt('42')").unwrap() { + Value::Number(n) => assert_eq!(n, 42.0), + v => panic!("expected 42, got {v:?}"), + } + } + + // ── Boolean built-in tests ──────────────────────────────── + + #[test] + fn test_boolean_constructor() { + match eval("Boolean(1)").unwrap() { + Value::Boolean(b) => assert!(b), + v => panic!("expected true, got {v:?}"), + } + match eval("Boolean(0)").unwrap() { + Value::Boolean(b) => assert!(!b), + v => panic!("expected false, got {v:?}"), + } + match eval("Boolean('')").unwrap() { + Value::Boolean(b) => assert!(!b), + v => panic!("expected false, got {v:?}"), + } + match eval("Boolean('hello')").unwrap() { + Value::Boolean(b) => assert!(b), + v => panic!("expected true, got {v:?}"), + } + } + + #[test] + fn test_boolean_to_string() { + match eval("true.toString()").unwrap() { + Value::String(s) => assert_eq!(s, "true"), + v => panic!("expected 'true', got {v:?}"), + } + match eval("false.toString()").unwrap() { + Value::String(s) => assert_eq!(s, "false"), + v => panic!("expected 'false', got {v:?}"), + } + } + + // ── Symbol built-in tests ───────────────────────────────── + + #[test] + fn test_symbol_uniqueness() { + // Each Symbol() call should produce a unique value. + match eval("var a = Symbol('x'); var b = Symbol('x'); a === b").unwrap() { + Value::Boolean(b) => assert!(!b), + v => panic!("expected false, got {v:?}"), + } + } + + #[test] + fn test_symbol_well_known() { + match eval("typeof Symbol.iterator").unwrap() { + Value::String(s) => assert_eq!(s, "string"), + v => panic!("expected 'string', got {v:?}"), + } + match eval("Symbol.iterator").unwrap() { + Value::String(s) => assert_eq!(s, "@@iterator"), + v => panic!("expected '@@iterator', got {v:?}"), + } + } + + #[test] + fn test_symbol_for_and_key_for() { + // "for" is a keyword, so use bracket notation: Symbol["for"](...). + match eval("Symbol['for']('test') === Symbol['for']('test')").unwrap() { + Value::Boolean(b) => assert!(b), + v => panic!("expected true, got {v:?}"), + } + match eval("Symbol.keyFor(Symbol['for']('mykey'))").unwrap() { + Value::String(s) => assert_eq!(s, "mykey"), + v => panic!("expected 'mykey', got {v:?}"), + } + } + + // ── Primitive auto-boxing tests ─────────────────────────── + + #[test] + fn test_string_method_chaining() { + match eval("' Hello World '.trim().toLowerCase()").unwrap() { + Value::String(s) => assert_eq!(s, "hello world"), + v => panic!("expected 'hello world', got {v:?}"), + } + } + + #[test] + fn test_string_substr() { + match eval("'hello world'.substr(6, 5)").unwrap() { + Value::String(s) => assert_eq!(s, "world"), + v => panic!("expected 'world', got {v:?}"), + } + } } -- 2.51.2