diff --git a/crates/js/src/builtins.rs b/crates/js/src/builtins.rs index 30bc3be1b..c72382e27 100644 --- a/crates/js/src/builtins.rs +++ b/crates/js/src/builtins.rs @@ -78,7 +78,7 @@ fn try_install(interp: &mut Interp) -> JsResult<()> { } interp.intrinsics.array_proto = alloc(interp, Class::Array { length: 0, length_writable: true }, Some(object_proto)); interp.intrinsics.string_proto = alloc(interp, Class::String(Str::from("")), Some(object_proto)); - interp.intrinsics.number_proto = alloc(interp, Class::Number, Some(object_proto)); + interp.intrinsics.number_proto = alloc(interp, Class::Number(0.0), Some(object_proto)); interp.intrinsics.boolean_proto = alloc(interp, Class::Boolean(false), Some(object_proto)); interp.intrinsics.regexp_proto = alloc(interp, Class::Ordinary, Some(object_proto)); let thrower = native(interp, "", throw_type_error, 0, NativeKind::Plain, false)?; diff --git a/crates/js/src/builtins/json.rs b/crates/js/src/builtins/json.rs index 93d9061dc..4aa5c9ac5 100644 --- a/crates/js/src/builtins/json.rs +++ b/crates/js/src/builtins/json.rs @@ -12,7 +12,7 @@ use std::collections::HashSet; use super::{Builder, ELEMENT_BYTES, arg, data, key_of, length_of, method, new_array, new_string, to_integer}; use crate::heap::{Class, Key, OBJECT_BYTES, OPEN, ObjId, Object, Prop, key, string_bytes}; -use crate::interp::{Desc, ErrorType, Interp, JsResult}; +use crate::interp::{Desc, ErrorType, Hint, Interp, JsResult}; use crate::value::{Str, Value, number_to_str}; pub(super) fn install(interp: &mut Interp) -> JsResult<()> { @@ -459,14 +459,7 @@ fn settings(interp: &mut Interp, replacer: &Value, space: &Value) -> JsResult match interp.heap.object(*o).map(|x| &x.class) { - Some(Class::Number) => Value::Number(interp.to_number(space)?), - Some(Class::String(_)) => Value::String(interp.to_str(space)?), - _ => space.clone(), - }, - _ => space.clone(), - }; + let space = unwrap_primitive(interp, space, false)?.unwrap_or_else(|| space.clone()); match space { Value::Number(_) => { let n = to_integer(interp, &space)?.clamp(0.0, 10.0) as usize; @@ -489,9 +482,10 @@ fn property_list(interp: &mut Interp, r: ObjId) -> JsResult> { let item = match &v { Value::String(s) => Some(s.clone()), Value::Number(_) => Some(interp.primitive_to_str(&v)), - Value::Object(o) if interp.heap.object(*o).is_some_and(|x| matches!(x.class, Class::String(_) | Class::Number)) => { - Some(interp.to_str(&v)?) - } + Value::Object(_) => match unwrap_primitive(interp, &v, true)? { + Some(Value::String(s)) => Some(s), + _ => None, + }, _ => None, }; let Some(s) = item else { continue }; @@ -546,12 +540,11 @@ fn resolve_rooted(interp: &mut Interp, settings: &Settings, holder: ObjId, k: &K keep(interp, &v); } if let Value::Object(o) = &v { - v = match interp.heap.object(*o).map(|x| &x.class) { - Some(Class::Number) => Value::Number(interp.to_number(&v)?), - Some(Class::String(_)) => Value::String(interp.to_str(&v)?), - Some(Class::Boolean(b)) => Value::Bool(*b), - _ => v, - }; + if let Some(Class::Boolean(b)) = interp.heap.object(*o).map(|x| &x.class) { + v = Value::Bool(*b); + } else if let Some(p) = unwrap_primitive(interp, &v, false)? { + v = p; + } } Ok(match v { Value::Undefined => Item::Skip, @@ -561,6 +554,43 @@ fn resolve_rooted(interp: &mut Interp, settings: &Settings, holder: ObjId, k: &K }) } +/// ToNumber of a Number object, or ToString of a String object (of either when `as_string` is +/// set), as section 15.12.3 applies them in Str step 4, step 4.b.ii and step 5. Returns `None` +/// for any other value. When the method that [[DefaultValue]] (section 8.12.8) calls first is not +/// found on the object or on its prototype chain before the object's standard prototype, the +/// result is the object's [[PrimitiveValue]] (sections 8.6.2 and 9.9), the value that +/// `Number.prototype.valueOf` (15.7.4.4), `Number.prototype.toString` (15.7.4.2) and +/// `String.prototype.toString` (15.5.4.2) return. Otherwise ToPrimitive calls the method found. +fn unwrap_primitive(interp: &mut Interp, v: &Value, as_string: bool) -> JsResult> { + let Value::Object(o) = v else { return Ok(None) }; + let (primitive, proto, string) = match interp.heap.object(*o).map(|x| &x.class) { + Some(Class::Number(n)) => (Value::Number(*n), interp.intrinsics.number_proto, as_string), + Some(Class::String(s)) => (Value::String(s.clone()), interp.intrinsics.string_proto, true), + _ => return Ok(None), + }; + let (hint, name) = if string { (Hint::String, "toString") } else { (Hint::Number, "valueOf") }; + let p = if reaches_prototype(interp, *o, proto, name)? { primitive } else { interp.to_primitive(v, hint)? }; + Ok(Some(if string { Value::String(interp.primitive_to_str(&p)) } else { Value::Number(interp.to_number(&p)?) })) +} + +/// Whether looking up `name` from `o` reaches the object `proto` on the prototype chain before +/// any object on the way, `proto` included, has its own property `name`. +fn reaches_prototype(interp: &mut Interp, o: ObjId, proto: ObjId, name: &str) -> JsResult { + let k = key(name); + let mut cur = Some(o); + while let Some(id) = cur { + interp.use_fuel()?; + if interp.get_own_property(id, &k)?.is_some() { + return Ok(false); + } + if id == proto { + return Ok(true); + } + cur = interp.heap.object(id).and_then(|x| x.proto); + } + Ok(false) +} + /// Quote (section 15.12.3): the string in double quotes with the characters JSON escapes. fn write_quoted(interp: &mut Interp, out: &mut Builder, s: &[u16]) -> JsResult<()> { out.push_units(interp, &[QUOTE])?; diff --git a/crates/js/src/builtins_tests.rs b/crates/js/src/builtins_tests.rs index aaf2d245a..a9184c0c3 100644 --- a/crates/js/src/builtins_tests.rs +++ b/crates/js/src/builtins_tests.rs @@ -529,6 +529,25 @@ fn json_stringify_writes_values_with_replacers_and_indentation() { assert_eq!(string("var x = {}; JSON.stringify([x, x])"), "[{},{}]"); } +#[test] +fn json_stringify_unwraps_number_string_and_boolean_objects() { + assert_eq!(string("JSON.stringify(Object(3))"), "3"); + assert_eq!(string("JSON.stringify(Object('abc'))"), "\"abc\""); + assert_eq!(string("JSON.stringify(Object(true))"), "true"); + assert_eq!(string("JSON.stringify({n: Object(-0), m: Object(NaN), s: Object('')})"), "{\"n\":0,\"m\":null,\"s\":\"\"}"); + assert_eq!(string("JSON.stringify([Object.getPrototypeOf(Object(3)), Object.getPrototypeOf(Object('abc'))])"), "[0,\"\"]"); + // The space argument (step 5). + assert_eq!(string("JSON.stringify([1], null, Object(2))"), "[\n 1\n]"); + assert_eq!(string("JSON.stringify([1], null, Object('--'))"), "[\n--1\n]"); + // Replacer list entries (step 4.b.ii). + assert_eq!(string("JSON.stringify({a: 1, 3: 2, b: 3}, [Object('a'), Object(3)])"), "{\"a\":1,\"3\":2}"); + // Methods on Object.prototype do not reach the objects; methods on their own prototypes do. + assert_eq!(string("Object.prototype.toString = function () { return 'z'; }; JSON.stringify(Object('abc'))"), "\"abc\""); + assert_eq!(string("Object.prototype.valueOf = function () { return 9; }; JSON.stringify(Object(3))"), "3"); + assert_eq!(string("Object.getPrototypeOf(Object(1)).valueOf = function () { return 4; }; JSON.stringify(Object(3))"), "4"); + assert_eq!(string("Object.getPrototypeOf(Object('')).toString = function () { return 'q'; }; JSON.stringify(Object('abc'))"), "\"q\""); +} + #[test] fn deep_json_nesting_needs_no_native_stack() { let handle = std::thread::Builder::new() diff --git a/crates/js/src/heap.rs b/crates/js/src/heap.rs index 66dc00b24..143de10c3 100644 --- a/crates/js/src/heap.rs +++ b/crates/js/src/heap.rs @@ -209,8 +209,8 @@ pub(crate) enum Class { Arguments(Box), /// A `Boolean` object made by ToObject, with its primitive value. Boolean(bool), - /// A `Number` object made by ToObject. - Number, + /// A `Number` object made by ToObject, with its primitive value. + Number(f64), String(Str), RegExp, ForIn(Box), @@ -232,7 +232,7 @@ impl Class { Class::Error => "Error", Class::Arguments(_) => "Arguments", Class::Boolean(_) => "Boolean", - Class::Number => "Number", + Class::Number(_) => "Number", Class::String(_) => "String", Class::RegExp => "RegExp", } diff --git a/crates/js/src/interp.rs b/crates/js/src/interp.rs index 2293984ce..472d21c19 100644 --- a/crates/js/src/interp.rs +++ b/crates/js/src/interp.rs @@ -2240,7 +2240,7 @@ impl Interp { Value::Object(o) => return Ok(*o), Value::Undefined | Value::Null => return Err(self.type_error("cannot convert undefined or null to an object")), Value::Bool(b) => (Class::Boolean(*b), self.intrinsics.boolean_proto), - Value::Number(_) => (Class::Number, self.intrinsics.number_proto), + Value::Number(n) => (Class::Number(*n), self.intrinsics.number_proto), Value::String(s) => (Class::String(s.clone()), self.intrinsics.string_proto), }; Ok(self.heap.alloc_object(Object::new(class, Some(proto))))