diff --git a/crates/js/src/builtins.rs b/crates/js/src/builtins.rs index aaf5769..8161a58 100644 --- a/crates/js/src/builtins.rs +++ b/crates/js/src/builtins.rs @@ -1,4 +1,5 @@ -//! Built-in JavaScript objects and functions: Object, Array, Function, Error. +//! Built-in JavaScript objects and functions: Object, Array, Function, Error, +//! String, Number, Boolean, Symbol, Math, Date, JSON. //! //! Registers constructors, static methods, and prototype methods as globals //! in the VM. Callback-based array methods (map, filter, etc.) are defined @@ -7,6 +8,7 @@ use crate::gc::{Gc, GcRef}; use crate::vm::*; use std::collections::HashMap; +use std::time::{SystemTime, UNIX_EPOCH}; /// Native callback type alias to satisfy clippy::type_complexity. type NativeMethod = ( @@ -232,6 +234,15 @@ pub fn init_builtins(vm: &mut Vm) { // Create and register Symbol factory. init_symbol_builtins(vm); + // Create and register Math object (static methods only, not a constructor). + init_math_object(vm); + + // Create and register Date constructor. + init_date_builtins(vm); + + // Create and register JSON object (static methods only). + init_json_object(vm); + // Register global utility functions. init_global_functions(vm); @@ -2208,6 +2219,13 @@ fn boolean_proto_value_of(args: &[Value], ctx: &mut NativeContext) -> Result> = const { std::cell::Cell::new(None) }; +} + 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); @@ -2269,6 +2287,1385 @@ fn symbol_key_for(args: &[Value], ctx: &mut NativeContext) -> Result f64 { + let mut s = PRNG_STATE.load(std::sync::atomic::Ordering::Relaxed); + if s == 0 { + // Seed from system time. + s = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_nanos() as u64) + .unwrap_or(123456789); + } + s ^= s << 13; + s ^= s >> 7; + s ^= s << 17; + PRNG_STATE.store(s, std::sync::atomic::Ordering::Relaxed); + // Map to [0, 1). + (s >> 11) as f64 / ((1u64 << 53) as f64) +} + +fn init_math_object(vm: &mut Vm) { + let mut data = ObjectData::new(); + if let Some(proto) = vm.object_prototype { + data.prototype = Some(proto); + } + let math_ref = vm.gc.alloc(HeapObject::Object(data)); + + // Constants. + let constants: &[(&str, f64)] = &[ + ("E", std::f64::consts::E), + ("LN2", std::f64::consts::LN_2), + ("LN10", std::f64::consts::LN_10), + ("LOG2E", std::f64::consts::LOG2_E), + ("LOG10E", std::f64::consts::LOG10_E), + ("PI", std::f64::consts::PI), + ("SQRT1_2", std::f64::consts::FRAC_1_SQRT_2), + ("SQRT2", std::f64::consts::SQRT_2), + ]; + for &(name, val) in constants { + set_builtin_prop(&mut vm.gc, math_ref, name, Value::Number(val)); + } + + // Methods. + let methods: &[NativeMethod] = &[ + ("abs", math_abs), + ("ceil", math_ceil), + ("floor", math_floor), + ("round", math_round), + ("trunc", math_trunc), + ("max", math_max), + ("min", math_min), + ("pow", math_pow), + ("sqrt", math_sqrt), + ("cbrt", math_cbrt), + ("hypot", math_hypot), + ("sin", math_sin), + ("cos", math_cos), + ("tan", math_tan), + ("asin", math_asin), + ("acos", math_acos), + ("atan", math_atan), + ("atan2", math_atan2), + ("exp", math_exp), + ("log", math_log), + ("log2", math_log2), + ("log10", math_log10), + ("expm1", math_expm1), + ("log1p", math_log1p), + ("sign", math_sign), + ("clz32", math_clz32), + ("fround", math_fround), + ("random", math_random), + ("imul", math_imul), + ]; + for &(name, cb) in methods { + let f = make_native(&mut vm.gc, name, cb); + set_builtin_prop(&mut vm.gc, math_ref, name, Value::Function(f)); + } + + vm.set_global("Math", Value::Object(math_ref)); +} + +fn math_abs(args: &[Value], _ctx: &mut NativeContext) -> Result { + let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN); + Ok(Value::Number(n.abs())) +} + +fn math_ceil(args: &[Value], _ctx: &mut NativeContext) -> Result { + let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN); + Ok(Value::Number(n.ceil())) +} + +fn math_floor(args: &[Value], _ctx: &mut NativeContext) -> Result { + let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN); + Ok(Value::Number(n.floor())) +} + +fn math_round(args: &[Value], _ctx: &mut NativeContext) -> Result { + let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN); + // JS Math.round rounds .5 up (toward +Infinity). + Ok(Value::Number(n.round())) +} + +fn math_trunc(args: &[Value], _ctx: &mut NativeContext) -> Result { + let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN); + Ok(Value::Number(n.trunc())) +} + +fn math_max(args: &[Value], _ctx: &mut NativeContext) -> Result { + if args.is_empty() { + return Ok(Value::Number(f64::NEG_INFINITY)); + } + let mut result = f64::NEG_INFINITY; + for arg in args { + let n = arg.to_number(); + if n.is_nan() { + return Ok(Value::Number(f64::NAN)); + } + if n > result { + result = n; + } + } + Ok(Value::Number(result)) +} + +fn math_min(args: &[Value], _ctx: &mut NativeContext) -> Result { + if args.is_empty() { + return Ok(Value::Number(f64::INFINITY)); + } + let mut result = f64::INFINITY; + for arg in args { + let n = arg.to_number(); + if n.is_nan() { + return Ok(Value::Number(f64::NAN)); + } + if n < result { + result = n; + } + } + Ok(Value::Number(result)) +} + +fn math_pow(args: &[Value], _ctx: &mut NativeContext) -> Result { + let base = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN); + let exp = args.get(1).map(|v| v.to_number()).unwrap_or(f64::NAN); + Ok(Value::Number(base.powf(exp))) +} + +fn math_sqrt(args: &[Value], _ctx: &mut NativeContext) -> Result { + let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN); + Ok(Value::Number(n.sqrt())) +} + +fn math_cbrt(args: &[Value], _ctx: &mut NativeContext) -> Result { + let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN); + Ok(Value::Number(n.cbrt())) +} + +fn math_hypot(args: &[Value], _ctx: &mut NativeContext) -> Result { + if args.is_empty() { + return Ok(Value::Number(0.0)); + } + let mut sum = 0.0f64; + for arg in args { + let n = arg.to_number(); + if n.is_infinite() { + return Ok(Value::Number(f64::INFINITY)); + } + if n.is_nan() { + return Ok(Value::Number(f64::NAN)); + } + sum += n * n; + } + Ok(Value::Number(sum.sqrt())) +} + +fn math_sin(args: &[Value], _ctx: &mut NativeContext) -> Result { + let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN); + Ok(Value::Number(n.sin())) +} + +fn math_cos(args: &[Value], _ctx: &mut NativeContext) -> Result { + let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN); + Ok(Value::Number(n.cos())) +} + +fn math_tan(args: &[Value], _ctx: &mut NativeContext) -> Result { + let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN); + Ok(Value::Number(n.tan())) +} + +fn math_asin(args: &[Value], _ctx: &mut NativeContext) -> Result { + let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN); + Ok(Value::Number(n.asin())) +} + +fn math_acos(args: &[Value], _ctx: &mut NativeContext) -> Result { + let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN); + Ok(Value::Number(n.acos())) +} + +fn math_atan(args: &[Value], _ctx: &mut NativeContext) -> Result { + let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN); + Ok(Value::Number(n.atan())) +} + +fn math_atan2(args: &[Value], _ctx: &mut NativeContext) -> Result { + let y = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN); + let x = args.get(1).map(|v| v.to_number()).unwrap_or(f64::NAN); + Ok(Value::Number(y.atan2(x))) +} + +fn math_exp(args: &[Value], _ctx: &mut NativeContext) -> Result { + let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN); + Ok(Value::Number(n.exp())) +} + +fn math_log(args: &[Value], _ctx: &mut NativeContext) -> Result { + let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN); + Ok(Value::Number(n.ln())) +} + +fn math_log2(args: &[Value], _ctx: &mut NativeContext) -> Result { + let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN); + Ok(Value::Number(n.log2())) +} + +fn math_log10(args: &[Value], _ctx: &mut NativeContext) -> Result { + let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN); + Ok(Value::Number(n.log10())) +} + +fn math_expm1(args: &[Value], _ctx: &mut NativeContext) -> Result { + let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN); + Ok(Value::Number(n.exp_m1())) +} + +fn math_log1p(args: &[Value], _ctx: &mut NativeContext) -> Result { + let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN); + Ok(Value::Number(n.ln_1p())) +} + +fn math_sign(args: &[Value], _ctx: &mut NativeContext) -> Result { + let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN); + if n.is_nan() { + Ok(Value::Number(f64::NAN)) + } else if n == 0.0 { + // Preserve -0 and +0. + Ok(Value::Number(n)) + } else if n > 0.0 { + Ok(Value::Number(1.0)) + } else { + Ok(Value::Number(-1.0)) + } +} + +fn math_clz32(args: &[Value], _ctx: &mut NativeContext) -> Result { + let n = args.first().map(|v| v.to_number()).unwrap_or(0.0); + let i = n as u32; + Ok(Value::Number(i.leading_zeros() as f64)) +} + +fn math_fround(args: &[Value], _ctx: &mut NativeContext) -> Result { + let n = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN); + Ok(Value::Number((n as f32) as f64)) +} + +fn math_random(_args: &[Value], _ctx: &mut NativeContext) -> Result { + Ok(Value::Number(prng_next())) +} + +fn math_imul(args: &[Value], _ctx: &mut NativeContext) -> Result { + // ToUint32 then reinterpret as i32 per spec. + let a = args.first().map(|v| v.to_number()).unwrap_or(0.0) as u32 as i32; + let b = args.get(1).map(|v| v.to_number()).unwrap_or(0.0) as u32 as i32; + Ok(Value::Number(a.wrapping_mul(b) as f64)) +} + +// ── Date built-in ──────────────────────────────────────────── + +/// Milliseconds since Unix epoch from SystemTime. +fn now_ms() -> f64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_millis() as f64) + .unwrap_or(0.0) +} + +/// Calendar components from a timestamp (milliseconds since epoch). +/// Returns (year, month0, day, hours, minutes, seconds, ms, weekday) in UTC. +fn ms_to_utc_components(ms: f64) -> (i64, i64, i64, i64, i64, i64, i64, i64) { + let total_ms = ms as i64; + let ms_part = ((total_ms % 1000) + 1000) % 1000; + let mut days = total_ms.div_euclid(86_400_000); + // Weekday: Jan 1 1970 was Thursday (4). + let weekday = ((days % 7) + 4 + 7) % 7; + + let secs_in_day = (total_ms.rem_euclid(86_400_000)) / 1000; + let hours = secs_in_day / 3600; + let minutes = (secs_in_day % 3600) / 60; + let seconds = secs_in_day % 60; + + // Convert days since epoch to year/month/day using a civil calendar algorithm. + // Shift epoch to March 1, 2000. + days += 719_468; + let era = if days >= 0 { + days / 146_097 + } else { + (days - 146_096) / 146_097 + }; + let doe = days - era * 146_097; // day of era [0, 146096] + let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146_096) / 365; + let y = yoe + era * 400; + let doy = doe - (365 * yoe + yoe / 4 - yoe / 100); + let mp = (5 * doy + 2) / 153; + let d = doy - (153 * mp + 2) / 5 + 1; + let m = if mp < 10 { mp + 3 } else { mp - 9 }; + let y = if m <= 2 { y + 1 } else { y }; + + (y, m - 1, d, hours, minutes, seconds, ms_part, weekday) +} + +/// Convert UTC components to milliseconds since epoch. +fn utc_components_to_ms(year: i64, month: i64, day: i64, h: i64, min: i64, s: i64, ms: i64) -> f64 { + // Normalize month overflow. + let y = year + month.div_euclid(12); + let m = month.rem_euclid(12) + 1; // 1-based + + // Civil calendar to days (inverse of above). + let (y2, m2) = if m <= 2 { (y - 1, m + 9) } else { (y, m - 3) }; + let era = if y2 >= 0 { y2 / 400 } else { (y2 - 399) / 400 }; + let yoe = y2 - era * 400; + let doy = (153 * m2 + 2) / 5 + day - 1; + let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy; + let days = era * 146_097 + doe - 719_468; + + (days * 86_400_000 + h * 3_600_000 + min * 60_000 + s * 1000 + ms) as f64 +} + +/// Parse a subset of ISO 8601 date strings (YYYY-MM-DDTHH:MM:SS.sssZ). +fn parse_date_string(s: &str) -> Option { + let s = s.trim(); + // Try YYYY-MM-DDTHH:MM:SS.sssZ or YYYY-MM-DD or YYYY + let parts: Vec<&str> = s.splitn(2, 'T').collect(); + let date_part = parts[0]; + let time_part = parts.get(1).copied().unwrap_or(""); + + let date_fields: Vec<&str> = date_part.split('-').collect(); + if date_fields.is_empty() { + return None; + } + + let year: i64 = date_fields[0].parse().ok()?; + let month: i64 = date_fields.get(1).and_then(|s| s.parse().ok()).unwrap_or(1); + let day: i64 = date_fields.get(2).and_then(|s| s.parse().ok()).unwrap_or(1); + + if !(1..=12).contains(&month) || !(1..=31).contains(&day) { + return None; + } + + let (h, min, sec, ms) = if time_part.is_empty() { + (0, 0, 0, 0) + } else { + // Strip trailing Z. + let tp = time_part.strip_suffix('Z').unwrap_or(time_part); + // Split seconds from milliseconds. + let (time_str, ms_val) = if let Some((t, m)) = tp.split_once('.') { + let ms_str = &m[..m.len().min(3)]; + let mut ms_val: i64 = ms_str.parse().unwrap_or(0); + // Pad to 3 digits if needed. + for _ in ms_str.len()..3 { + ms_val *= 10; + } + (t, ms_val) + } else { + (tp, 0i64) + }; + let time_fields: Vec<&str> = time_str.split(':').collect(); + let h: i64 = time_fields + .first() + .and_then(|s| s.parse().ok()) + .unwrap_or(0); + let min: i64 = time_fields.get(1).and_then(|s| s.parse().ok()).unwrap_or(0); + let sec: i64 = time_fields.get(2).and_then(|s| s.parse().ok()).unwrap_or(0); + (h, min, sec, ms_val) + }; + + Some(utc_components_to_ms(year, month - 1, day, h, min, sec, ms)) +} + +static DAY_NAMES: [&str; 7] = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"]; +static MONTH_NAMES: [&str; 12] = [ + "Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec", +]; + +fn init_date_builtins(vm: &mut Vm) { + // Date.prototype. + let mut date_proto_data = ObjectData::new(); + if let Some(proto) = vm.object_prototype { + date_proto_data.prototype = Some(proto); + } + let date_proto = vm.gc.alloc(HeapObject::Object(date_proto_data)); + init_date_prototype(&mut vm.gc, date_proto); + + // Store prototype for constructor access. + vm.date_prototype = Some(date_proto); + DATE_PROTO.with(|cell| cell.set(Some(date_proto))); + + // Date constructor function. + let ctor = vm.gc.alloc(HeapObject::Function(Box::new(FunctionData { + name: "Date".to_string(), + kind: FunctionKind::Native(NativeFunc { + callback: date_constructor, + }), + prototype_obj: Some(date_proto), + properties: HashMap::new(), + upvalues: Vec::new(), + }))); + + // Static methods. + let now_fn = make_native(&mut vm.gc, "now", date_now); + set_func_prop(&mut vm.gc, ctor, "now", Value::Function(now_fn)); + + let parse_fn = make_native(&mut vm.gc, "parse", date_parse); + set_func_prop(&mut vm.gc, ctor, "parse", Value::Function(parse_fn)); + + let utc_fn = make_native(&mut vm.gc, "UTC", date_utc); + set_func_prop(&mut vm.gc, ctor, "UTC", Value::Function(utc_fn)); + + vm.set_global("Date", Value::Function(ctor)); +} + +/// Internal: create a Date object (plain object with __date_ms__ property). +fn make_date_obj(gc: &mut Gc, ms: f64, proto: Option) -> Value { + let mut data = ObjectData::new(); + if let Some(p) = proto { + data.prototype = Some(p); + } + data.properties.insert( + "__date_ms__".to_string(), + Property::builtin(Value::Number(ms)), + ); + Value::Object(gc.alloc(HeapObject::Object(data))) +} + +/// Extract timestamp from a Date object. +fn date_get_ms(gc: &Gc, this: &Value) -> f64 { + match this { + Value::Object(r) => match gc.get(*r) { + Some(HeapObject::Object(data)) => data + .properties + .get("__date_ms__") + .map(|p| p.value.to_number()) + .unwrap_or(f64::NAN), + _ => f64::NAN, + }, + _ => f64::NAN, + } +} + +/// Set timestamp on a Date object and return the new value. +fn date_set_ms(gc: &mut Gc, this: &Value, ms: f64) -> f64 { + if let Value::Object(r) = this { + if let Some(HeapObject::Object(data)) = gc.get_mut(*r) { + if let Some(prop) = data.properties.get_mut("__date_ms__") { + prop.value = Value::Number(ms); + } + } + } + ms +} + +fn date_constructor(args: &[Value], ctx: &mut NativeContext) -> Result { + let proto = DATE_PROTO.with(|cell| cell.get()); + let ms = match args.len() { + 0 => now_ms(), + 1 => match &args[0] { + Value::String(s) => parse_date_string(s).unwrap_or(f64::NAN), + Value::Number(n) => *n, + v => v.to_number(), + }, + _ => { + // new Date(year, month, day?, hours?, min?, sec?, ms?) + let year = args[0].to_number() as i64; + let month = args[1].to_number() as i64; + let day = args.get(2).map(|v| v.to_number() as i64).unwrap_or(1); + let h = args.get(3).map(|v| v.to_number() as i64).unwrap_or(0); + let min = args.get(4).map(|v| v.to_number() as i64).unwrap_or(0); + let sec = args.get(5).map(|v| v.to_number() as i64).unwrap_or(0); + let ms = args.get(6).map(|v| v.to_number() as i64).unwrap_or(0); + // Years 0-99 map to 1900-1999 per spec. + let year = if (0..100).contains(&year) { + year + 1900 + } else { + year + }; + utc_components_to_ms(year, month, day, h, min, sec, ms) + } + }; + Ok(make_date_obj(ctx.gc, ms, proto)) +} + +fn date_now(_args: &[Value], _ctx: &mut NativeContext) -> Result { + Ok(Value::Number(now_ms())) +} + +fn date_parse(args: &[Value], ctx: &mut NativeContext) -> Result { + let s = args + .first() + .map(|v| v.to_js_string(ctx.gc)) + .unwrap_or_default(); + Ok(Value::Number(parse_date_string(&s).unwrap_or(f64::NAN))) +} + +fn date_utc(args: &[Value], _ctx: &mut NativeContext) -> Result { + let year = args.first().map(|v| v.to_number() as i64).unwrap_or(1970); + let month = args.get(1).map(|v| v.to_number() as i64).unwrap_or(0); + let day = args.get(2).map(|v| v.to_number() as i64).unwrap_or(1); + let h = args.get(3).map(|v| v.to_number() as i64).unwrap_or(0); + let min = args.get(4).map(|v| v.to_number() as i64).unwrap_or(0); + let sec = args.get(5).map(|v| v.to_number() as i64).unwrap_or(0); + let ms = args.get(6).map(|v| v.to_number() as i64).unwrap_or(0); + let year = if (0..100).contains(&year) { + year + 1900 + } else { + year + }; + Ok(Value::Number(utc_components_to_ms( + year, month, day, h, min, sec, ms, + ))) +} + +fn init_date_prototype(gc: &mut Gc, proto: GcRef) { + let methods: &[NativeMethod] = &[ + ("getTime", date_get_time), + ("valueOf", date_get_time), // valueOf === getTime + ("getFullYear", date_get_full_year), + ("getMonth", date_get_month), + ("getDate", date_get_date), + ("getDay", date_get_day), + ("getHours", date_get_hours), + ("getMinutes", date_get_minutes), + ("getSeconds", date_get_seconds), + ("getMilliseconds", date_get_milliseconds), + ("getTimezoneOffset", date_get_timezone_offset), + ("getUTCFullYear", date_get_full_year), + ("getUTCMonth", date_get_month), + ("getUTCDate", date_get_date), + ("getUTCDay", date_get_day), + ("getUTCHours", date_get_hours), + ("getUTCMinutes", date_get_minutes), + ("getUTCSeconds", date_get_seconds), + ("getUTCMilliseconds", date_get_milliseconds), + ("setTime", date_set_time), + ("setFullYear", date_set_full_year), + ("setMonth", date_set_month), + ("setDate", date_set_date), + ("setHours", date_set_hours), + ("setMinutes", date_set_minutes), + ("setSeconds", date_set_seconds), + ("setMilliseconds", date_set_milliseconds), + ("toString", date_to_string), + ("toISOString", date_to_iso_string), + ("toUTCString", date_to_utc_string), + ("toLocaleDateString", date_to_locale_date_string), + ("toJSON", date_to_json), + ]; + for &(name, cb) in methods { + let f = make_native(gc, name, cb); + set_builtin_prop(gc, proto, name, Value::Function(f)); + } +} + +fn date_get_time(args: &[Value], ctx: &mut NativeContext) -> Result { + let _ = args; + Ok(Value::Number(date_get_ms(ctx.gc, &ctx.this))) +} + +fn date_get_full_year(_args: &[Value], ctx: &mut NativeContext) -> Result { + let ms = date_get_ms(ctx.gc, &ctx.this); + if ms.is_nan() { + return Ok(Value::Number(f64::NAN)); + } + let (y, ..) = ms_to_utc_components(ms); + Ok(Value::Number(y as f64)) +} + +fn date_get_month(_args: &[Value], ctx: &mut NativeContext) -> Result { + let ms = date_get_ms(ctx.gc, &ctx.this); + if ms.is_nan() { + return Ok(Value::Number(f64::NAN)); + } + let (_, m, ..) = ms_to_utc_components(ms); + Ok(Value::Number(m as f64)) +} + +fn date_get_date(_args: &[Value], ctx: &mut NativeContext) -> Result { + let ms = date_get_ms(ctx.gc, &ctx.this); + if ms.is_nan() { + return Ok(Value::Number(f64::NAN)); + } + let (_, _, d, ..) = ms_to_utc_components(ms); + Ok(Value::Number(d as f64)) +} + +fn date_get_day(_args: &[Value], ctx: &mut NativeContext) -> Result { + let ms = date_get_ms(ctx.gc, &ctx.this); + if ms.is_nan() { + return Ok(Value::Number(f64::NAN)); + } + let (.., wd) = ms_to_utc_components(ms); + Ok(Value::Number(wd as f64)) +} + +fn date_get_hours(_args: &[Value], ctx: &mut NativeContext) -> Result { + let ms = date_get_ms(ctx.gc, &ctx.this); + if ms.is_nan() { + return Ok(Value::Number(f64::NAN)); + } + let (_, _, _, h, ..) = ms_to_utc_components(ms); + Ok(Value::Number(h as f64)) +} + +fn date_get_minutes(_args: &[Value], ctx: &mut NativeContext) -> Result { + let ms = date_get_ms(ctx.gc, &ctx.this); + if ms.is_nan() { + return Ok(Value::Number(f64::NAN)); + } + let (_, _, _, _, min, ..) = ms_to_utc_components(ms); + Ok(Value::Number(min as f64)) +} + +fn date_get_seconds(_args: &[Value], ctx: &mut NativeContext) -> Result { + let ms = date_get_ms(ctx.gc, &ctx.this); + if ms.is_nan() { + return Ok(Value::Number(f64::NAN)); + } + let (_, _, _, _, _, s, ..) = ms_to_utc_components(ms); + Ok(Value::Number(s as f64)) +} + +fn date_get_milliseconds(_args: &[Value], ctx: &mut NativeContext) -> Result { + let ms = date_get_ms(ctx.gc, &ctx.this); + if ms.is_nan() { + return Ok(Value::Number(f64::NAN)); + } + let (_, _, _, _, _, _, ms_part, _) = ms_to_utc_components(ms); + Ok(Value::Number(ms_part as f64)) +} + +fn date_get_timezone_offset( + _args: &[Value], + _ctx: &mut NativeContext, +) -> Result { + // We operate in UTC; return 0. + Ok(Value::Number(0.0)) +} + +fn date_set_time(args: &[Value], ctx: &mut NativeContext) -> Result { + let ms = args.first().map(|v| v.to_number()).unwrap_or(f64::NAN); + date_set_ms(ctx.gc, &ctx.this, ms); + Ok(Value::Number(ms)) +} + +fn date_set_full_year(args: &[Value], ctx: &mut NativeContext) -> Result { + let old = date_get_ms(ctx.gc, &ctx.this); + let (_, m, d, h, min, s, ms, _) = ms_to_utc_components(if old.is_nan() { 0.0 } else { old }); + let year = args.first().map(|v| v.to_number() as i64).unwrap_or(0); + let month = args.get(1).map(|v| v.to_number() as i64).unwrap_or(m); + let day = args.get(2).map(|v| v.to_number() as i64).unwrap_or(d); + let new_ms = utc_components_to_ms(year, month, day, h, min, s, ms); + date_set_ms(ctx.gc, &ctx.this, new_ms); + Ok(Value::Number(new_ms)) +} + +fn date_set_month(args: &[Value], ctx: &mut NativeContext) -> Result { + let old = date_get_ms(ctx.gc, &ctx.this); + let (y, _, d, h, min, s, ms, _) = ms_to_utc_components(if old.is_nan() { 0.0 } else { old }); + let month = args.first().map(|v| v.to_number() as i64).unwrap_or(0); + let day = args.get(1).map(|v| v.to_number() as i64).unwrap_or(d); + let new_ms = utc_components_to_ms(y, month, day, h, min, s, ms); + date_set_ms(ctx.gc, &ctx.this, new_ms); + Ok(Value::Number(new_ms)) +} + +fn date_set_date(args: &[Value], ctx: &mut NativeContext) -> Result { + let old = date_get_ms(ctx.gc, &ctx.this); + let (y, m, _, h, min, s, ms, _) = ms_to_utc_components(if old.is_nan() { 0.0 } else { old }); + let day = args.first().map(|v| v.to_number() as i64).unwrap_or(1); + let new_ms = utc_components_to_ms(y, m, day, h, min, s, ms); + date_set_ms(ctx.gc, &ctx.this, new_ms); + Ok(Value::Number(new_ms)) +} + +fn date_set_hours(args: &[Value], ctx: &mut NativeContext) -> Result { + let old = date_get_ms(ctx.gc, &ctx.this); + let (y, m, d, _, min, s, ms, _) = ms_to_utc_components(if old.is_nan() { 0.0 } else { old }); + let h = args.first().map(|v| v.to_number() as i64).unwrap_or(0); + let min_v = args.get(1).map(|v| v.to_number() as i64).unwrap_or(min); + let sec = args.get(2).map(|v| v.to_number() as i64).unwrap_or(s); + let ms_v = args.get(3).map(|v| v.to_number() as i64).unwrap_or(ms); + let new_ms = utc_components_to_ms(y, m, d, h, min_v, sec, ms_v); + date_set_ms(ctx.gc, &ctx.this, new_ms); + Ok(Value::Number(new_ms)) +} + +fn date_set_minutes(args: &[Value], ctx: &mut NativeContext) -> Result { + let old = date_get_ms(ctx.gc, &ctx.this); + let (y, m, d, h, _, s, ms, _) = ms_to_utc_components(if old.is_nan() { 0.0 } else { old }); + let min = args.first().map(|v| v.to_number() as i64).unwrap_or(0); + let sec = args.get(1).map(|v| v.to_number() as i64).unwrap_or(s); + let ms_v = args.get(2).map(|v| v.to_number() as i64).unwrap_or(ms); + let new_ms = utc_components_to_ms(y, m, d, h, min, sec, ms_v); + date_set_ms(ctx.gc, &ctx.this, new_ms); + Ok(Value::Number(new_ms)) +} + +fn date_set_seconds(args: &[Value], ctx: &mut NativeContext) -> Result { + let old = date_get_ms(ctx.gc, &ctx.this); + let (y, m, d, h, min, _, ms, _) = ms_to_utc_components(if old.is_nan() { 0.0 } else { old }); + let sec = args.first().map(|v| v.to_number() as i64).unwrap_or(0); + let ms_v = args.get(1).map(|v| v.to_number() as i64).unwrap_or(ms); + let new_ms = utc_components_to_ms(y, m, d, h, min, sec, ms_v); + date_set_ms(ctx.gc, &ctx.this, new_ms); + Ok(Value::Number(new_ms)) +} + +fn date_set_milliseconds(args: &[Value], ctx: &mut NativeContext) -> Result { + let old = date_get_ms(ctx.gc, &ctx.this); + let (y, m, d, h, min, s, _, _) = ms_to_utc_components(if old.is_nan() { 0.0 } else { old }); + let ms_v = args.first().map(|v| v.to_number() as i64).unwrap_or(0); + let new_ms = utc_components_to_ms(y, m, d, h, min, s, ms_v); + date_set_ms(ctx.gc, &ctx.this, new_ms); + Ok(Value::Number(new_ms)) +} + +fn date_to_string(_args: &[Value], ctx: &mut NativeContext) -> Result { + let ms = date_get_ms(ctx.gc, &ctx.this); + if ms.is_nan() { + return Ok(Value::String("Invalid Date".to_string())); + } + let (y, m, d, h, min, s, _, wd) = ms_to_utc_components(ms); + Ok(Value::String(format!( + "{} {} {:02} {} {:02}:{:02}:{:02} GMT", + DAY_NAMES[wd as usize], MONTH_NAMES[m as usize], d, y, h, min, s + ))) +} + +fn date_to_iso_string(_args: &[Value], ctx: &mut NativeContext) -> Result { + let ms = date_get_ms(ctx.gc, &ctx.this); + if ms.is_nan() { + return Err(RuntimeError::range_error("Invalid time value")); + } + let (y, m, d, h, min, s, ms_part, _) = ms_to_utc_components(ms); + Ok(Value::String(format!( + "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:03}Z", + y, + m + 1, + d, + h, + min, + s, + ms_part + ))) +} + +fn date_to_utc_string(_args: &[Value], ctx: &mut NativeContext) -> Result { + let ms = date_get_ms(ctx.gc, &ctx.this); + if ms.is_nan() { + return Ok(Value::String("Invalid Date".to_string())); + } + let (y, m, d, h, min, s, _, wd) = ms_to_utc_components(ms); + Ok(Value::String(format!( + "{}, {:02} {} {} {:02}:{:02}:{:02} GMT", + DAY_NAMES[wd as usize], d, MONTH_NAMES[m as usize], y, h, min, s + ))) +} + +fn date_to_locale_date_string( + _args: &[Value], + ctx: &mut NativeContext, +) -> Result { + // Simplified: same as toISOString date portion. + let ms = date_get_ms(ctx.gc, &ctx.this); + if ms.is_nan() { + return Ok(Value::String("Invalid Date".to_string())); + } + let (y, m, d, ..) = ms_to_utc_components(ms); + Ok(Value::String(format!("{:04}-{:02}-{:02}", y, m + 1, d))) +} + +fn date_to_json(_args: &[Value], ctx: &mut NativeContext) -> Result { + let ms = date_get_ms(ctx.gc, &ctx.this); + if ms.is_nan() { + return Ok(Value::Null); + } + date_to_iso_string(_args, ctx) +} + +// ── JSON object ────────────────────────────────────────────── + +fn init_json_object(vm: &mut Vm) { + let mut data = ObjectData::new(); + if let Some(proto) = vm.object_prototype { + data.prototype = Some(proto); + } + let json_ref = vm.gc.alloc(HeapObject::Object(data)); + + let parse_fn = make_native(&mut vm.gc, "parse", json_parse); + set_builtin_prop(&mut vm.gc, json_ref, "parse", Value::Function(parse_fn)); + + let stringify_fn = make_native(&mut vm.gc, "stringify", json_stringify); + set_builtin_prop( + &mut vm.gc, + json_ref, + "stringify", + Value::Function(stringify_fn), + ); + + vm.set_global("JSON", Value::Object(json_ref)); +} + +// ── JSON.parse ─────────────────────────────────────────────── + +/// Minimal JSON tokenizer. +#[derive(Debug, Clone, PartialEq)] +enum JsonToken { + LBrace, + RBrace, + LBracket, + RBracket, + Colon, + Comma, + String(String), + Number(f64), + True, + False, + Null, +} + +struct JsonLexer<'a> { + chars: &'a [u8], + pos: usize, +} + +impl<'a> JsonLexer<'a> { + fn new(input: &'a str) -> Self { + Self { + chars: input.as_bytes(), + pos: 0, + } + } + + fn skip_ws(&mut self) { + while self.pos < self.chars.len() { + match self.chars[self.pos] { + b' ' | b'\t' | b'\n' | b'\r' => self.pos += 1, + _ => break, + } + } + } + + fn next_token(&mut self) -> Result, RuntimeError> { + self.skip_ws(); + if self.pos >= self.chars.len() { + return Ok(None); + } + let ch = self.chars[self.pos]; + match ch { + b'{' => { + self.pos += 1; + Ok(Some(JsonToken::LBrace)) + } + b'}' => { + self.pos += 1; + Ok(Some(JsonToken::RBrace)) + } + b'[' => { + self.pos += 1; + Ok(Some(JsonToken::LBracket)) + } + b']' => { + self.pos += 1; + Ok(Some(JsonToken::RBracket)) + } + b':' => { + self.pos += 1; + Ok(Some(JsonToken::Colon)) + } + b',' => { + self.pos += 1; + Ok(Some(JsonToken::Comma)) + } + b'"' => self.read_string(), + b't' => self.read_keyword("true", JsonToken::True), + b'f' => self.read_keyword("false", JsonToken::False), + b'n' => self.read_keyword("null", JsonToken::Null), + b'-' | b'0'..=b'9' => self.read_number(), + _ => Err(RuntimeError::syntax_error(format!( + "Unexpected character '{}' in JSON", + ch as char + ))), + } + } + + fn read_string(&mut self) -> Result, RuntimeError> { + self.pos += 1; // skip opening quote + let mut s = String::new(); + while self.pos < self.chars.len() { + let ch = self.chars[self.pos]; + self.pos += 1; + if ch == b'"' { + return Ok(Some(JsonToken::String(s))); + } + if ch == b'\\' { + if self.pos >= self.chars.len() { + return Err(RuntimeError::syntax_error("Unterminated string in JSON")); + } + let esc = self.chars[self.pos]; + self.pos += 1; + match esc { + b'"' => s.push('"'), + b'\\' => s.push('\\'), + b'/' => s.push('/'), + b'b' => s.push('\u{0008}'), + b'f' => s.push('\u{000C}'), + b'n' => s.push('\n'), + b'r' => s.push('\r'), + b't' => s.push('\t'), + b'u' => { + if self.pos + 4 > self.chars.len() { + return Err(RuntimeError::syntax_error("Invalid unicode escape")); + } + let hex = std::str::from_utf8(&self.chars[self.pos..self.pos + 4]) + .map_err(|_| RuntimeError::syntax_error("Invalid unicode escape"))?; + self.pos += 4; + let code = u32::from_str_radix(hex, 16) + .map_err(|_| RuntimeError::syntax_error("Invalid unicode escape"))?; + if let Some(c) = char::from_u32(code) { + s.push(c); + } else { + return Err(RuntimeError::syntax_error("Invalid unicode code point")); + } + } + _ => { + return Err(RuntimeError::syntax_error(format!( + "Invalid escape character '\\{}'", + esc as char + ))); + } + } + } else { + s.push(ch as char); + } + } + Err(RuntimeError::syntax_error("Unterminated string in JSON")) + } + + fn read_number(&mut self) -> Result, RuntimeError> { + let start = self.pos; + if self.pos < self.chars.len() && self.chars[self.pos] == b'-' { + self.pos += 1; + } + while self.pos < self.chars.len() && self.chars[self.pos].is_ascii_digit() { + self.pos += 1; + } + if self.pos < self.chars.len() && self.chars[self.pos] == b'.' { + self.pos += 1; + while self.pos < self.chars.len() && self.chars[self.pos].is_ascii_digit() { + self.pos += 1; + } + } + if self.pos < self.chars.len() + && (self.chars[self.pos] == b'e' || self.chars[self.pos] == b'E') + { + self.pos += 1; + if self.pos < self.chars.len() + && (self.chars[self.pos] == b'+' || self.chars[self.pos] == b'-') + { + self.pos += 1; + } + while self.pos < self.chars.len() && self.chars[self.pos].is_ascii_digit() { + self.pos += 1; + } + } + let num_str = std::str::from_utf8(&self.chars[start..self.pos]) + .map_err(|_| RuntimeError::syntax_error("Invalid number in JSON"))?; + let n: f64 = num_str + .parse() + .map_err(|_| RuntimeError::syntax_error("Invalid number in JSON"))?; + Ok(Some(JsonToken::Number(n))) + } + + fn read_keyword( + &mut self, + keyword: &str, + token: JsonToken, + ) -> Result, RuntimeError> { + let end = self.pos + keyword.len(); + if end <= self.chars.len() && &self.chars[self.pos..end] == keyword.as_bytes() { + self.pos = end; + Ok(Some(token)) + } else { + Err(RuntimeError::syntax_error(format!( + "Unexpected token in JSON at position {}", + self.pos + ))) + } + } +} + +struct JsonParser<'a> { + lexer: JsonLexer<'a>, + current: Option, +} + +impl<'a> JsonParser<'a> { + fn new(input: &'a str) -> Result { + let mut lexer = JsonLexer::new(input); + let current = lexer.next_token()?; + Ok(Self { lexer, current }) + } + + fn advance(&mut self) -> Result<(), RuntimeError> { + self.current = self.lexer.next_token()?; + Ok(()) + } + + fn parse_value(&mut self, gc: &mut Gc) -> Result { + match self.current.take() { + Some(JsonToken::Null) => { + self.advance()?; + Ok(Value::Null) + } + Some(JsonToken::True) => { + self.advance()?; + Ok(Value::Boolean(true)) + } + Some(JsonToken::False) => { + self.advance()?; + Ok(Value::Boolean(false)) + } + Some(JsonToken::Number(n)) => { + self.advance()?; + Ok(Value::Number(n)) + } + Some(JsonToken::String(s)) => { + self.advance()?; + Ok(Value::String(s)) + } + Some(JsonToken::LBracket) => self.parse_array(gc), + Some(JsonToken::LBrace) => self.parse_object(gc), + Some(other) => { + // Put it back for error context. + self.current = Some(other); + Err(RuntimeError::syntax_error("Unexpected token in JSON")) + } + None => Err(RuntimeError::syntax_error("Unexpected end of JSON input")), + } + } + + fn parse_array(&mut self, gc: &mut Gc) -> Result { + // Current token was LBracket, already consumed via take(). + self.advance()?; // move past '[' + let mut items: Vec = Vec::new(); + if self.current == Some(JsonToken::RBracket) { + self.advance()?; + let mut obj = ObjectData::new(); + obj.properties.insert( + "length".to_string(), + Property { + value: Value::Number(0.0), + writable: true, + enumerable: false, + configurable: false, + }, + ); + return Ok(Value::Object(gc.alloc(HeapObject::Object(obj)))); + } + loop { + let val = self.parse_value(gc)?; + items.push(val); + match &self.current { + Some(JsonToken::Comma) => { + self.advance()?; + } + Some(JsonToken::RBracket) => { + self.advance()?; + break; + } + _ => { + return Err(RuntimeError::syntax_error( + "Expected ',' or ']' in JSON array", + )); + } + } + } + let mut obj = ObjectData::new(); + for (i, v) in items.iter().enumerate() { + obj.properties + .insert(i.to_string(), Property::data(v.clone())); + } + obj.properties.insert( + "length".to_string(), + Property { + value: Value::Number(items.len() as f64), + writable: true, + enumerable: false, + configurable: false, + }, + ); + Ok(Value::Object(gc.alloc(HeapObject::Object(obj)))) + } + + fn parse_object(&mut self, gc: &mut Gc) -> Result { + self.advance()?; // move past '{' + let mut obj = ObjectData::new(); + if self.current == Some(JsonToken::RBrace) { + self.advance()?; + return Ok(Value::Object(gc.alloc(HeapObject::Object(obj)))); + } + loop { + let key = match self.current.take() { + Some(JsonToken::String(s)) => s, + _ => { + return Err(RuntimeError::syntax_error( + "Expected string key in JSON object", + )); + } + }; + self.advance()?; + if self.current != Some(JsonToken::Colon) { + return Err(RuntimeError::syntax_error( + "Expected ':' after key in JSON object", + )); + } + self.advance()?; + let val = self.parse_value(gc)?; + obj.properties.insert(key, Property::data(val)); + match &self.current { + Some(JsonToken::Comma) => { + self.advance()?; + } + Some(JsonToken::RBrace) => { + self.advance()?; + break; + } + _ => { + return Err(RuntimeError::syntax_error( + "Expected ',' or '}}' in JSON object", + )); + } + } + } + Ok(Value::Object(gc.alloc(HeapObject::Object(obj)))) + } +} + +fn json_parse(args: &[Value], ctx: &mut NativeContext) -> Result { + let text = args + .first() + .map(|v| v.to_js_string(ctx.gc)) + .unwrap_or_default(); + let mut parser = JsonParser::new(&text)?; + let value = parser.parse_value(ctx.gc)?; + // Ensure no trailing content. + if parser.current.is_some() { + return Err(RuntimeError::syntax_error( + "Unexpected token after JSON value", + )); + } + // TODO: reviver support — skip for now. + Ok(value) +} + +// ── JSON.stringify ─────────────────────────────────────────── + +fn json_stringify(args: &[Value], ctx: &mut NativeContext) -> Result { + let value = args.first().cloned().unwrap_or(Value::Undefined); + let indent = match args.get(2) { + Some(Value::Number(n)) => { + let n = *n as usize; + if n > 0 { + " ".repeat(n.min(10)) + } else { + String::new() + } + } + Some(Value::String(s)) => s[..s.len().min(10)].to_string(), + _ => String::new(), + }; + // Track visited objects for circular reference detection. + let mut visited: Vec = Vec::new(); + let result = json_stringify_value(&value, ctx.gc, &indent, "", &mut visited)?; + match result { + Some(s) => Ok(Value::String(s)), + None => Ok(Value::Undefined), + } +} + +fn json_stringify_value( + value: &Value, + gc: &Gc, + indent: &str, + current_indent: &str, + visited: &mut Vec, +) -> Result, RuntimeError> { + match value { + Value::Null => Ok(Some("null".to_string())), + Value::Boolean(true) => Ok(Some("true".to_string())), + Value::Boolean(false) => Ok(Some("false".to_string())), + Value::Number(n) => { + if n.is_nan() || n.is_infinite() { + Ok(Some("null".to_string())) + } else { + Ok(Some(js_number_to_string(*n))) + } + } + Value::String(s) => Ok(Some(json_quote_string(s))), + Value::Undefined | Value::Function(_) => Ok(None), + Value::Object(gc_ref) => { + // Circular reference check. + if visited.contains(gc_ref) { + return Err(RuntimeError::type_error( + "Converting circular structure to JSON", + )); + } + visited.push(*gc_ref); + + let result = match gc.get(*gc_ref) { + Some(HeapObject::Object(data)) => { + // Check for toJSON method. + if let Some(prop) = data.properties.get("toJSON") { + if matches!(prop.value, Value::Function(_)) { + // We can't call JS functions from here easily, + // but for Date objects we recognize the __date_ms__ pattern. + if let Some(date_prop) = data.properties.get("__date_ms__") { + let ms = date_prop.value.to_number(); + if ms.is_nan() { + visited.pop(); + return Ok(Some("null".to_string())); + } + let (y, m, d, h, min, s, ms_part, _) = ms_to_utc_components(ms); + visited.pop(); + return Ok(Some(format!( + "\"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:03}Z\"", + y, + m + 1, + d, + h, + min, + s, + ms_part + ))); + } + } + } + if array_length_exists(gc, *gc_ref) { + json_stringify_array(gc, *gc_ref, indent, current_indent, visited) + } else { + json_stringify_object(gc, data, indent, current_indent, visited) + } + } + _ => Ok(Some("{}".to_string())), + }; + + visited.pop(); + result + } + } +} + +fn json_stringify_array( + gc: &Gc, + arr: GcRef, + indent: &str, + current_indent: &str, + visited: &mut Vec, +) -> Result, RuntimeError> { + let len = array_length(gc, arr); + if len == 0 { + return Ok(Some("[]".to_string())); + } + let has_indent = !indent.is_empty(); + let next_indent = if has_indent { + format!("{}{}", current_indent, indent) + } else { + String::new() + }; + let mut parts: Vec = Vec::new(); + for i in 0..len { + let val = array_get(gc, arr, i); + match json_stringify_value(&val, gc, indent, &next_indent, visited)? { + Some(s) => parts.push(s), + None => parts.push("null".to_string()), + } + } + if has_indent { + Ok(Some(format!( + "[\n{}{}\n{}]", + next_indent, + parts.join(&format!(",\n{}", next_indent)), + current_indent + ))) + } else { + Ok(Some(format!("[{}]", parts.join(",")))) + } +} + +fn json_stringify_object( + gc: &Gc, + data: &ObjectData, + indent: &str, + current_indent: &str, + visited: &mut Vec, +) -> Result, RuntimeError> { + let has_indent = !indent.is_empty(); + let next_indent = if has_indent { + format!("{}{}", current_indent, indent) + } else { + String::new() + }; + let mut parts: Vec = Vec::new(); + // Collect and sort keys for deterministic output. + let mut keys: Vec<&String> = data.properties.keys().collect(); + keys.sort(); + for key in keys { + let prop = &data.properties[key]; + if !prop.enumerable { + continue; + } + if let Some(val_str) = json_stringify_value(&prop.value, gc, indent, &next_indent, visited)? + { + if has_indent { + parts.push(format!("{}: {}", json_quote_string(key), val_str)); + } else { + parts.push(format!("{}:{}", json_quote_string(key), val_str)); + } + } + } + if parts.is_empty() { + return Ok(Some("{}".to_string())); + } + if has_indent { + Ok(Some(format!( + "{{\n{}{}\n{}}}", + next_indent, + parts.join(&format!(",\n{}", next_indent)), + current_indent + ))) + } else { + Ok(Some(format!("{{{}}}", parts.join(",")))) + } +} + +fn json_quote_string(s: &str) -> String { + let mut out = String::with_capacity(s.len() + 2); + out.push('"'); + for ch in s.chars() { + match ch { + '"' => out.push_str("\\\""), + '\\' => out.push_str("\\\\"), + '\n' => out.push_str("\\n"), + '\r' => out.push_str("\\r"), + '\t' => out.push_str("\\t"), + '\u{0008}' => out.push_str("\\b"), + '\u{000C}' => out.push_str("\\f"), + c if c < '\u{0020}' => { + out.push_str(&format!("\\u{:04x}", c as u32)); + } + c => out.push(c), + } + } + out.push('"'); + out +} + // ── 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 4be5521..cdd06f4 100644 --- a/crates/js/src/vm.rs +++ b/crates/js/src/vm.rs @@ -310,7 +310,7 @@ impl Value { } /// Format a number as JS would. -fn js_number_to_string(n: f64) -> String { +pub(crate) fn js_number_to_string(n: f64) -> String { if n.is_nan() { "NaN".to_string() } else if n.is_infinite() { @@ -381,6 +381,13 @@ impl RuntimeError { } } + pub fn syntax_error(msg: impl Into) -> Self { + Self { + kind: ErrorKind::SyntaxError, + message: msg.into(), + } + } + /// Convert to a JS Value (an error object). Allocates through the GC. pub fn to_value(&self, gc: &mut Gc) -> Value { let mut obj = ObjectData::new(); @@ -688,6 +695,8 @@ pub struct Vm { pub number_prototype: Option, /// Built-in Boolean.prototype (for primitive auto-boxing). pub boolean_prototype: Option, + /// Built-in Date.prototype (for Date constructor objects). + pub date_prototype: Option, } /// Maximum register file size. @@ -709,6 +718,7 @@ impl Vm { string_prototype: None, number_prototype: None, boolean_prototype: None, + date_prototype: None, }; crate::builtins::init_builtins(&mut vm); vm @@ -3742,4 +3752,444 @@ mod tests { v => panic!("expected 'world', got {v:?}"), } } + + // ── Math built-in ───────────────────────────────────────── + + #[test] + fn test_math_constants() { + let r = eval("Math.PI").unwrap(); + match r { + Value::Number(n) => assert!((n - std::f64::consts::PI).abs() < 1e-10), + _ => panic!("Expected number"), + } + let r = eval("Math.E").unwrap(); + match r { + Value::Number(n) => assert!((n - std::f64::consts::E).abs() < 1e-10), + _ => panic!("Expected number"), + } + let r = eval("Math.SQRT2").unwrap(); + match r { + Value::Number(n) => assert!((n - std::f64::consts::SQRT_2).abs() < 1e-10), + _ => panic!("Expected number"), + } + } + + #[test] + fn test_math_abs() { + assert_eq!(eval("Math.abs(-5)").unwrap().to_number(), 5.0); + assert_eq!(eval("Math.abs(3)").unwrap().to_number(), 3.0); + assert_eq!(eval("Math.abs(0)").unwrap().to_number(), 0.0); + } + + #[test] + fn test_math_floor_ceil_round_trunc() { + assert_eq!(eval("Math.floor(4.7)").unwrap().to_number(), 4.0); + assert_eq!(eval("Math.ceil(4.2)").unwrap().to_number(), 5.0); + assert_eq!(eval("Math.round(4.5)").unwrap().to_number(), 5.0); + assert_eq!(eval("Math.round(4.4)").unwrap().to_number(), 4.0); + assert_eq!(eval("Math.trunc(4.7)").unwrap().to_number(), 4.0); + assert_eq!(eval("Math.trunc(-4.7)").unwrap().to_number(), -4.0); + } + + #[test] + fn test_math_max_min() { + assert_eq!(eval("Math.max(1, 3, 2)").unwrap().to_number(), 3.0); + assert_eq!(eval("Math.min(1, 3, 2)").unwrap().to_number(), 1.0); + assert_eq!(eval("Math.max()").unwrap().to_number(), f64::NEG_INFINITY); + assert_eq!(eval("Math.min()").unwrap().to_number(), f64::INFINITY); + } + + #[test] + fn test_math_pow_sqrt() { + assert_eq!(eval("Math.pow(2, 10)").unwrap().to_number(), 1024.0); + assert_eq!(eval("Math.sqrt(9)").unwrap().to_number(), 3.0); + let cbrt = eval("Math.cbrt(27)").unwrap().to_number(); + assert!((cbrt - 3.0).abs() < 1e-10); + } + + #[test] + fn test_math_trig() { + let sin = eval("Math.sin(0)").unwrap().to_number(); + assert!(sin.abs() < 1e-10); + let cos = eval("Math.cos(0)").unwrap().to_number(); + assert!((cos - 1.0).abs() < 1e-10); + let atan2 = eval("Math.atan2(1, 1)").unwrap().to_number(); + assert!((atan2 - std::f64::consts::FRAC_PI_4).abs() < 1e-10); + } + + #[test] + fn test_math_log_exp() { + let exp = eval("Math.exp(1)").unwrap().to_number(); + assert!((exp - std::f64::consts::E).abs() < 1e-10); + let log = eval("Math.log(Math.E)").unwrap().to_number(); + assert!((log - 1.0).abs() < 1e-10); + let log2 = eval("Math.log2(8)").unwrap().to_number(); + assert!((log2 - 3.0).abs() < 1e-10); + let log10 = eval("Math.log10(1000)").unwrap().to_number(); + assert!((log10 - 3.0).abs() < 1e-10); + } + + #[test] + fn test_math_sign() { + assert_eq!(eval("Math.sign(5)").unwrap().to_number(), 1.0); + assert_eq!(eval("Math.sign(-5)").unwrap().to_number(), -1.0); + assert_eq!(eval("Math.sign(0)").unwrap().to_number(), 0.0); + } + + #[test] + fn test_math_clz32() { + assert_eq!(eval("Math.clz32(1)").unwrap().to_number(), 31.0); + assert_eq!(eval("Math.clz32(0)").unwrap().to_number(), 32.0); + } + + #[test] + fn test_math_imul() { + assert_eq!(eval("Math.imul(3, 4)").unwrap().to_number(), 12.0); + assert_eq!(eval("Math.imul(0xffffffff, 5)").unwrap().to_number(), -5.0); + } + + #[test] + fn test_math_hypot() { + assert_eq!(eval("Math.hypot(3, 4)").unwrap().to_number(), 5.0); + assert_eq!(eval("Math.hypot()").unwrap().to_number(), 0.0); + } + + #[test] + fn test_math_random() { + match eval("var r = Math.random(); r >= 0 && r < 1").unwrap() { + Value::Boolean(b) => assert!(b), + v => panic!("expected true, got {v:?}"), + } + } + + #[test] + fn test_math_fround() { + let r = eval("Math.fround(5.5)").unwrap().to_number(); + assert_eq!(r, 5.5f32 as f64); + } + + // ── Date built-in ───────────────────────────────────────── + + #[test] + fn test_date_now() { + let r = eval("Date.now()").unwrap().to_number(); + assert!(r > 1_577_836_800_000.0); + } + + #[test] + fn test_date_utc() { + let r = eval("Date.UTC(2020, 0, 1)").unwrap().to_number(); + assert_eq!(r, 1_577_836_800_000.0); + } + + #[test] + fn test_date_parse() { + let r = eval("Date.parse('2020-01-01T00:00:00.000Z')") + .unwrap() + .to_number(); + assert_eq!(r, 1_577_836_800_000.0); + } + + #[test] + fn test_date_constructor_ms() { + let r = eval("var d = new Date(1577836800000); d.getFullYear()") + .unwrap() + .to_number(); + assert_eq!(r, 2020.0); + } + + #[test] + fn test_date_constructor_components() { + let r = eval("var d = new Date(2020, 0, 1, 0, 0, 0, 0); d.getTime()") + .unwrap() + .to_number(); + assert_eq!(r, 1_577_836_800_000.0); + } + + #[test] + fn test_date_getters() { + let src = r#" + var d = new Date(1577836800000); + var results = [ + d.getFullYear(), + d.getMonth(), + d.getDate(), + d.getHours(), + d.getMinutes(), + d.getSeconds(), + d.getMilliseconds(), + d.getDay() + ]; + results[0] === 2020 && results[1] === 0 && results[2] === 1 && + results[3] === 0 && results[4] === 0 && results[5] === 0 && + results[6] === 0 && results[7] === 3 + "#; + match eval(src).unwrap() { + Value::Boolean(b) => assert!(b), + v => panic!("expected true, got {v:?}"), + } + } + + #[test] + fn test_date_setters() { + let src = r#" + var d = new Date(1577836800000); + d.setFullYear(2025); + d.getFullYear() + "#; + assert_eq!(eval(src).unwrap().to_number(), 2025.0); + } + + #[test] + fn test_date_to_iso_string() { + match eval("var d = new Date(1577836800000); d.toISOString()").unwrap() { + Value::String(s) => assert_eq!(s, "2020-01-01T00:00:00.000Z"), + v => panic!("expected ISO string, got {v:?}"), + } + } + + #[test] + fn test_date_value_of() { + let r = eval("var d = new Date(1577836800000); d.valueOf()") + .unwrap() + .to_number(); + assert_eq!(r, 1_577_836_800_000.0); + } + + #[test] + fn test_date_to_string() { + let r = eval("var d = new Date(1577836800000); d.toString()").unwrap(); + match r { + Value::String(s) => assert!(s.contains("2020") && s.contains("GMT")), + _ => panic!("Expected string"), + } + } + + #[test] + fn test_date_to_json() { + match eval("var d = new Date(1577836800000); d.toJSON()").unwrap() { + Value::String(s) => assert_eq!(s, "2020-01-01T00:00:00.000Z"), + v => panic!("expected ISO string, got {v:?}"), + } + } + + #[test] + fn test_date_constructor_string() { + let r = eval("var d = new Date('2020-06-15T12:30:00Z'); d.getMonth()") + .unwrap() + .to_number(); + assert_eq!(r, 5.0); + } + + // ── JSON built-in ───────────────────────────────────────── + + #[test] + fn test_json_parse_primitives() { + assert!(matches!(eval("JSON.parse('null')").unwrap(), Value::Null)); + match eval("JSON.parse('true')").unwrap() { + Value::Boolean(b) => assert!(b), + v => panic!("expected true, got {v:?}"), + } + match eval("JSON.parse('false')").unwrap() { + Value::Boolean(b) => assert!(!b), + v => panic!("expected false, got {v:?}"), + } + assert_eq!(eval("JSON.parse('42')").unwrap().to_number(), 42.0); + match eval(r#"JSON.parse('"hello"')"#).unwrap() { + Value::String(s) => assert_eq!(s, "hello"), + v => panic!("expected 'hello', got {v:?}"), + } + } + + #[test] + fn test_json_parse_array() { + let src = r#" + var a = JSON.parse('[1, 2, 3]'); + a.length === 3 && a[0] === 1 && a[1] === 2 && a[2] === 3 + "#; + match eval(src).unwrap() { + Value::Boolean(b) => assert!(b), + v => panic!("expected true, got {v:?}"), + } + } + + #[test] + fn test_json_parse_object() { + let src = r#" + var o = JSON.parse('{"name":"test","value":42}'); + o.name === "test" && o.value === 42 + "#; + match eval(src).unwrap() { + Value::Boolean(b) => assert!(b), + v => panic!("expected true, got {v:?}"), + } + } + + #[test] + fn test_json_parse_nested() { + let src = r#" + var o = JSON.parse('{"a":[1,{"b":2}]}'); + o.a[1].b === 2 + "#; + match eval(src).unwrap() { + Value::Boolean(b) => assert!(b), + v => panic!("expected true, got {v:?}"), + } + } + + #[test] + fn test_json_parse_invalid() { + assert!(eval("JSON.parse('{invalid}')").is_err()); + assert!(eval("JSON.parse('')").is_err()); + } + + #[test] + fn test_json_stringify_primitives() { + match eval("JSON.stringify(null)").unwrap() { + Value::String(s) => assert_eq!(s, "null"), + v => panic!("expected 'null', got {v:?}"), + } + match eval("JSON.stringify(true)").unwrap() { + Value::String(s) => assert_eq!(s, "true"), + v => panic!("expected 'true', got {v:?}"), + } + match eval("JSON.stringify(42)").unwrap() { + Value::String(s) => assert_eq!(s, "42"), + v => panic!("expected '42', got {v:?}"), + } + match eval(r#"JSON.stringify("hello")"#).unwrap() { + Value::String(s) => assert_eq!(s, "\"hello\""), + v => panic!("expected quoted hello, got {v:?}"), + } + } + + #[test] + fn test_json_stringify_array() { + match eval("JSON.stringify([1, 2, 3])").unwrap() { + Value::String(s) => assert_eq!(s, "[1,2,3]"), + v => panic!("expected '[1,2,3]', got {v:?}"), + } + } + + #[test] + fn test_json_stringify_object() { + let src = r#" + var o = {a: 1, b: "hello"}; + JSON.stringify(o) + "#; + let r = eval(src).unwrap(); + match r { + Value::String(s) => { + assert!(s.contains("\"a\"") && s.contains("\"b\"")); + assert!(s.contains('1') && s.contains("\"hello\"")); + } + _ => panic!("Expected string"), + } + } + + #[test] + fn test_json_stringify_nested() { + let src = r#" + JSON.stringify({a: [1, 2], b: {c: 3}}) + "#; + let r = eval(src).unwrap(); + match r { + Value::String(s) => { + assert!(s.contains("[1,2]")); + assert!(s.contains("\"c\":3") || s.contains("\"c\": 3")); + } + _ => panic!("Expected string"), + } + } + + #[test] + fn test_json_stringify_special_values() { + match eval("JSON.stringify(NaN)").unwrap() { + Value::String(s) => assert_eq!(s, "null"), + v => panic!("expected 'null', got {v:?}"), + } + match eval("JSON.stringify(Infinity)").unwrap() { + Value::String(s) => assert_eq!(s, "null"), + v => panic!("expected 'null', got {v:?}"), + } + assert!(matches!( + eval("JSON.stringify(undefined)").unwrap(), + Value::Undefined + )); + } + + #[test] + fn test_json_stringify_with_indent() { + let src = r#"JSON.stringify([1, 2], null, 2)"#; + let r = eval(src).unwrap(); + match r { + Value::String(s) => { + assert!(s.contains('\n')); + assert!(s.contains(" 1")); + } + _ => panic!("Expected string"), + } + } + + #[test] + fn test_json_parse_escape_sequences() { + let src = r#"JSON.parse('"hello\\nworld"')"#; + match eval(src).unwrap() { + Value::String(s) => assert_eq!(s, "hello\nworld"), + v => panic!("expected escaped string, got {v:?}"), + } + } + + #[test] + fn test_json_roundtrip() { + let src = r#" + var original = {name: "test", values: [1, 2, 3], nested: {ok: true}}; + var json = JSON.stringify(original); + var parsed = JSON.parse(json); + parsed.name === "test" && parsed.values[1] === 2 && parsed.nested.ok === true + "#; + match eval(src).unwrap() { + Value::Boolean(b) => assert!(b), + v => panic!("expected true, got {v:?}"), + } + } + + #[test] + fn test_json_stringify_circular_detection() { + let src = r#" + var obj = {}; + obj.self = obj; + try { + JSON.stringify(obj); + false; + } catch (e) { + e.message.indexOf("circular") !== -1; + } + "#; + match eval(src).unwrap() { + Value::Boolean(b) => assert!(b), + v => panic!("expected true, got {v:?}"), + } + } + + #[test] + fn test_json_parse_unicode_escape() { + let src = r#"JSON.parse('"\\u0041"')"#; + match eval(src).unwrap() { + Value::String(s) => assert_eq!(s, "A"), + v => panic!("expected 'A', got {v:?}"), + } + } + + #[test] + fn test_json_stringify_empty() { + match eval("JSON.stringify([])").unwrap() { + Value::String(s) => assert_eq!(s, "[]"), + v => panic!("expected '[]', got {v:?}"), + } + match eval("JSON.stringify({})").unwrap() { + Value::String(s) => assert_eq!(s, "{}"), + v => panic!("expected '{{}}', got {v:?}"), + } + } }