diff --git a/nova_vm/src/ecmascript/builtins/primitive_objects.rs b/nova_vm/src/ecmascript/builtins/primitive_objects.rs index 5524bba7..457e7813 100644 --- a/nova_vm/src/ecmascript/builtins/primitive_objects.rs +++ b/nova_vm/src/ecmascript/builtins/primitive_objects.rs @@ -631,6 +631,28 @@ impl TryFrom for Symbol<'_> { } } +impl IntoValue for PrimitiveObjectData { + fn into_value(self) -> Value { + self.into() + } +} + +impl From for Value { + fn from(value: PrimitiveObjectData) -> Self { + match value { + PrimitiveObjectData::Boolean(data) => Value::Boolean(data), + PrimitiveObjectData::String(data) => Value::String(data), + PrimitiveObjectData::SmallString(data) => Value::SmallString(data), + PrimitiveObjectData::Symbol(data) => Value::Symbol(data), + PrimitiveObjectData::Number(data) => Value::Number(data), + PrimitiveObjectData::Integer(data) => Value::Integer(data), + PrimitiveObjectData::Float(data) => Value::SmallF64(data), + PrimitiveObjectData::BigInt(data) => Value::BigInt(data), + PrimitiveObjectData::SmallBigInt(data) => Value::SmallBigInt(data), + } + } +} + #[derive(Debug, Clone, Copy)] pub struct PrimitiveObjectHeapData { pub(crate) object_index: Option>, diff --git a/nova_vm/src/ecmascript/builtins/structured_data/json_object.rs b/nova_vm/src/ecmascript/builtins/structured_data/json_object.rs index 38f61df0..eb1fa934 100644 --- a/nova_vm/src/ecmascript/builtins/structured_data/json_object.rs +++ b/nova_vm/src/ecmascript/builtins/structured_data/json_object.rs @@ -2,13 +2,21 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. +use std::iter::once; + use sonic_rs::{JsonContainerTrait, JsonValueTrait}; +use crate::ecmascript::abstract_operations::operations_on_objects::enumerable_properties_kind::EnumerateKeys; use crate::ecmascript::abstract_operations::operations_on_objects::{ - length_of_array_like, try_create_data_property, try_create_data_property_or_throw, + enumerable_own_properties, get_v, length_of_array_like, try_create_data_property, + try_create_data_property_or_throw, }; use crate::ecmascript::abstract_operations::testing_and_comparison::is_array; -use crate::ecmascript::types::{IntoObject, IntoValue}; +use crate::ecmascript::abstract_operations::type_conversion::{ + to_integer_or_infinity_number, to_number, +}; +use crate::ecmascript::builtins::primitive_objects::{PrimitiveObject, PrimitiveObjectData}; +use crate::ecmascript::types::{IntoObject, IntoValue, PropertyDescriptor}; use crate::engine::context::{GcScope, NoGcScope}; use crate::engine::{unwrap_try, Scoped}; use crate::{ @@ -166,13 +174,216 @@ impl JSONObject { Ok(unfiltered) } + /// ### [25.5.1 JSON.stringify ( value \[ , replacer \[ , space \] ] )](https://tc39.es/ecma262/#sec-json.stringify) + /// + /// This function returns a String in UTF-16 encoded JSON format + /// representing an ECMAScript language value, or undefined. It can take + /// three parameters. The value parameter is an ECMAScript language value, + /// which is usually an object or array, although it can also be a String, + /// Boolean, Number or null. The optional replacer parameter is either a + /// function that alters the way objects and arrays are stringified, or an + /// array of Strings and Numbers that acts as an inclusion list for + /// selecting the object properties that will be stringified. The optional + /// space parameter is a String or Number that allows the result to have + /// white space injected into it to improve human readability. + /// + /// > Note 1 + /// > + /// > JSON structures are allowed to be nested to any depth, but they must be acyclic. If value is or contains a cyclic structure, then this function must throw a TypeError exception. This is an example of a value that cannot be stringified: + /// > + /// > ```js + /// > a = []; + /// > a[0] = a; + /// > my_text = JSON.stringify(a); // This must throw a TypeError. + /// > ``` + /// + /// > Note 2 + /// > + /// > Symbolic primitive values are rendered as follows: + /// > + /// > - The null value is rendered in JSON text as the String value "null". + /// > - The undefined value is not rendered. + /// > - The true value is rendered in JSON text as the String value "true". + /// > - The false value is rendered in JSON text as the String value "false". + /// + /// > Note 3 + /// > + /// > String values are wrapped in QUOTATION MARK (`"``) code units. The code + /// > units `"` and `\` are escaped with `\` prefixes. Control characters code + /// > units are replaced with escape sequences `\uHHHH`, or with the shorter + /// > forms, `\b` (BACKSPACE), `\f` (FORM FEED), `\n` (LINE FEED), `\r` (CARRIAGE + /// > RETURN), `\t` (CHARACTER TABULATION). + /// + /// > Note 4 + /// > + /// > Finite numbers are stringified as if by calling ToString(number). NaN + /// > and Infinity regardless of sign are represented as the String value + /// > "null". + /// + /// > Note 5 + /// > + /// > Values that do not have a JSON representation (such as undefined and + /// > functions) do not produce a String. Instead they produce the undefined + /// > value. In arrays these values are represented as the String value + /// > "null". In objects an unrepresentable value causes the property to be + /// > excluded from stringification. + /// + /// > Note 6 + /// > + /// > An object is rendered as U+007B (LEFT CURLY BRACKET) followed by zero + /// > or more properties, separated with a U+002C (COMMA), closed with a + /// > U+007D (RIGHT CURLY BRACKET). A property is a quoted String + /// > representing the property name, a U+003A (COLON), and then the + /// > stringified property value. An array is rendered as an opening U+005B + /// > (LEFT SQUARE BRACKET) followed by zero or more values, separated with a + /// > U+002C (COMMA), closed with a U+005D (RIGHT SQUARE BRACKET). fn stringify( - _agent: &mut Agent, + agent: &mut Agent, _this_value: Value, - _arguments: ArgumentsList, - _gc: GcScope<'_, '_>, + arguments: ArgumentsList, + mut gc: GcScope<'_, '_>, ) -> JsResult { - todo!(); + let value = arguments.get(0); + let replacer = arguments.get(1); + let space = arguments.get(2); + + // 1. Let stack be a new empty List. + let stack = Vec::new(); + // 3. Let PropertyList be undefined. + let mut property_list: Option>> = None; + + // 4. Let ReplacerFunction be undefined. + // a. If IsCallable(replacer) is true, then + let replacer_function = if let Ok(replacer) = Function::try_from(replacer) { + // i. Set ReplacerFunction to replacer. + Some(replacer) + } else if let Ok(replacer) = Object::try_from(replacer) { + // 5. If replacer is an Object, then + // b. Else, + // i. Let isArray be ? IsArray(replacer). + if is_array(agent, replacer, gc.nogc())? { + // ii. If isArray is true, then + // 2. Let len be ? LengthOfArrayLike(replacer). + let len = length_of_array_like(agent, replacer, gc.reborrow())?; + // 1. Set PropertyList to a new empty List. + property_list = Some(Vec::with_capacity(len as usize)); + // 3. Let k be 0. + // 4. Repeat, while k < len, + // h. Set k to k + 1. + for k in 0..len { + // a. Let prop be ! ToString(𝔽(k)). + let prop = PropertyKey::from(SmallInteger::try_from(k).unwrap()); + // b. Let v be ? Get(replacer, prop). + let v = get(agent, replacer, prop, gc.reborrow())?; + // c. Let item be undefined. + let item = if let Ok(v) = String::try_from(v) { + // d. If v is a String, then + // i. Set item to v. + Some(v.unbind()) + } else if v.is_number() { + // e. Else if v is a Number, then + // i. Set item to ! ToString(v). + Some(to_string(agent, v, gc.reborrow()).unwrap().unbind()) + } else if let Ok(v) = PrimitiveObject::try_from(v) { + // f. Else if v is an Object, then + // i. If v has a [[StringData]] or [[NumberData]] internal slot, set item to ? ToString(v). + match agent[v].data { + PrimitiveObjectData::String(_) + | PrimitiveObjectData::SmallString(_) + | PrimitiveObjectData::Number(_) + | PrimitiveObjectData::Integer(_) + | PrimitiveObjectData::Float(_) => { + Some(to_string(agent, v, gc.reborrow())?.unbind()) + } + _ => None, + } + } else { + None + }; + // g. If item is not undefined and PropertyList does not contain item, then + // i. Append item to PropertyList. + if let Some(item) = item { + let property_list = property_list.as_mut().unwrap(); + if !property_list.contains(&item) { + property_list.push(item); + } + } + } + } + None + } else { + None + }; + + // 6. If space is an Object, then + let space = if let Ok(space) = PrimitiveObject::try_from(space) { + match agent[space].data { + // a. If space has a [[NumberData]] internal slot, then + // i. Set space to ? ToNumber(space). + PrimitiveObjectData::Number(_) + | PrimitiveObjectData::Integer(_) + | PrimitiveObjectData::Float(_) => { + Some(to_number(agent, space, gc.reborrow())?.into_value()) + } + // b. Else if space has a [[StringData]] internal slot, then + // i. Set space to ? ToString(space). + PrimitiveObjectData::String(_) | PrimitiveObjectData::SmallString(_) => { + Some(to_string(agent, space, gc.reborrow())?.into_value()) + } + _ => None, + } + } else { + None + }; + + let gap = space.map_or("".to_string(), |space| { + // 7. If space is a Number, then + if let Ok(space) = Number::try_from(space) { + // a. Let spaceMV be ! ToIntegerOrInfinity(space). + let space_mv = to_integer_or_infinity_number(agent, space, gc.nogc()); + // b. Set spaceMV to min(10, spaceMV). + // c. If spaceMV < 1, let gap be the empty String; otherwise let gap be the String value containing spaceMV occurrences of the code unit 0x0020 (SPACE). + let space_mv = space_mv.into_i64().clamp(0, 10) as usize; + " ".repeat(space_mv as usize) + } else if let Ok(space) = String::try_from(space) { + // 8. Else if space is a String, then + // a. If the length of space ≤ 10, let gap be space; otherwise let gap be the substring of space from 0 to 10. + space.as_str(agent)[..10].to_string() + } else { + // 9. Else, + // a. Let gap be the empty String. + "".to_string() + } + }); + + // 10. Let wrapper be OrdinaryObjectCreate(%Object.prototype%). + let wrapper = + ordinary_object_create_with_intrinsics(agent, Some(ProtoIntrinsics::Object), None); + // 11. Perform ! CreateDataPropertyOrThrow(wrapper, the empty String, value). + wrapper.property_storage().set( + agent, + String::EMPTY_STRING.to_property_key(), + PropertyDescriptor::new_data_descriptor(value), + ); + // 12. Let state be the JSON Serialization Record { [[ReplacerFunction]]: ReplacerFunction, [[Stack]]: stack, [[Indent]]: indent, [[Gap]]: gap, [[PropertyList]]: PropertyList }. + let mut state = JSONSerializationRecord { + replacer_function, + stack, + // 2. Let indent be the empty String. + indent: "".to_owned(), + gap, + property_list, + }; + // 13. Return ? SerializeJSONProperty(state, the empty String, wrapper). + Ok(serialize_json_property( + agent, + &mut state, + String::EMPTY_STRING, + wrapper, + gc.reborrow(), + )? + .map(|s| s.into_value()) + .unwrap_or(Value::Undefined)) } pub(crate) fn create_intrinsic(agent: &mut Agent, realm: RealmIdentifier) { @@ -317,6 +528,424 @@ fn internalize_json_property<'a>( ) } +struct JSONSerializationRecord { + replacer_function: Option>, + stack: Vec, + indent: std::string::String, + gap: std::string::String, + property_list: Option>>, +} + +/// ### [25.5.2.2 SerializeJSONProperty ( state, key, holder )](https://tc39.es/ecma262/#sec-serializejsonproperty) +/// +/// The abstract operation SerializeJSONProperty takes arguments state (a JSON +/// Serialization Record), key (a String), and holder (an Object) and returns +/// either a normal completion containing either a String or undefined, or a +/// throw completion. +fn serialize_json_property( + agent: &mut Agent, + state: &mut JSONSerializationRecord, + key: String<'static>, + holder: Object, + mut gc: GcScope<'_, '_>, +) -> JsResult>> { + // 1. Let value be ? Get(holder, key). + let mut value = get(agent, holder, PropertyKey::from(key), gc.reborrow())?; + // 2. If value is an Object or value is a BigInt, then + if value.is_object() || value.is_bigint() { + // a. Let toJSON be ? GetV(value, "toJSON"). + let to_json = get_v( + agent, + value, + BUILTIN_STRING_MEMORY.toJSON.to_property_key(), + gc.reborrow(), + )?; + // b. If IsCallable(toJSON) is true, then + if let Some(to_json) = is_callable(to_json, gc.nogc()) { + // i. Set value to ? Call(toJSON, value, « key »). + value = call_function( + agent, + to_json.unbind(), + value, + Some(ArgumentsList(&[key.into()])), + gc.reborrow(), + )?; + } + } + // 3. If state.[[ReplacerFunction]] is not undefined, then + if let Some(replacer_function) = &state.replacer_function { + // a. Set value to ? Call(state.[[ReplacerFunction]], holder, « key, value »). + value = call_function( + agent, + *replacer_function, + holder.into(), + Some(ArgumentsList(&[key.into(), value])), + gc.reborrow(), + )?; + } + + // 4. If value is an Object, then + if let Ok(obj) = PrimitiveObject::try_from(value) { + let data = agent[obj].data; + match data { + // a. If value has a [[NumberData]] internal slot, then + // i. Set value to ? ToNumber(value). + PrimitiveObjectData::Number(_) + | PrimitiveObjectData::Integer(_) + | PrimitiveObjectData::Float(_) => { + value = to_number(agent, obj, gc.reborrow())?.into_value() + } + // b. Else if value has a [[StringData]] internal slot, then + // i. Set value to ? ToString(value). + PrimitiveObjectData::String(_) | PrimitiveObjectData::SmallString(_) => { + value = to_string(agent, obj, gc.reborrow())?.into_value() + } + // c. Else if value has a [[BooleanData]] internal slot, then + // i. Set value to value.[[BooleanData]]. + // d. Else if value has a [[BigIntData]] internal slot, then + // i. Set value to value.[[BigIntData]]. + PrimitiveObjectData::Boolean(_) + | PrimitiveObjectData::BigInt(_) + | PrimitiveObjectData::SmallBigInt(_) => value = data.into_value(), + _ => {} + } + } + + match value { + // 5. If value is null, return "null". + Value::Null => return Ok(Some(BUILTIN_STRING_MEMORY.null)), + // 6. If value is true, return "true". + Value::Boolean(true) => return Ok(Some(BUILTIN_STRING_MEMORY.r#true)), + // 7. If value is false, return "false". + Value::Boolean(false) => return Ok(Some(BUILTIN_STRING_MEMORY.r#false)), + // 8. If value is a String, return QuoteJSONString(value). + Value::String(_) | Value::SmallString(_) => { + let value = value.to_string(agent, gc.reborrow()).unwrap().unbind(); + return Ok(Some( + quote_json_string(agent, value, gc.into_nogc()).unbind(), + )); + } + // 9. If value is a Number, then + Value::Number(_) | Value::SmallF64(_) | Value::Integer(_) => { + let Ok(value) = Number::try_from(value) else { + unreachable!() + }; + // a. If value is finite, return ! ToString(value). + if value.is_finite(agent) { + return Ok(Some( + to_string(agent, value, gc.reborrow()).unwrap().unbind(), + )); + } + // b. Return "null". + return Ok(Some(BUILTIN_STRING_MEMORY.null)); + } + Value::BigInt(_) | Value::SmallBigInt(_) => { + // 10. If value is a BigInt, throw a TypeError exception. + return Err(agent.throw_exception_with_static_message( + ExceptionType::TypeError, + "Cannot serialize BigInt to JSON", + gc.nogc(), + )); + } + _ => {} + } + + // 11. If value is an Object and IsCallable(value) is false, then + if let Ok(value) = Object::try_from(value) { + if is_callable(value, gc.nogc()).is_none() { + // a. Let isArray be ? IsArray(value). + // b. If isArray is true, return ? SerializeJSONArray(state, value). + if is_array(agent, value, gc.nogc())? { + return Ok(Some(serialize_json_array(agent, state, value, gc)?)); + } + // c. Return ? SerializeJSONObject(state, value). + return Ok(Some(serialize_json_object(agent, state, value, gc)?)); + } + } + // 12. Return undefined. + Ok(None) +} + +/// ### [25.5.2.3 QuoteJSONString ( value )](https://tc39.es/ecma262/#sec-quotejsonstring) +/// +/// The abstract operation QuoteJSONString takes argument value (a String) and +/// returns a String. It wraps value in 0x0022 (QUOTATION MARK) code units and +/// escapes certain other code units within it. This operation interprets value +/// as a sequence of UTF-16 encoded code points, as described in 6.1.4. +pub(crate) fn quote_json_string<'a>( + agent: &mut Agent, + value: String, + gc: NoGcScope<'a, '_>, +) -> String<'a> { + // 1. Let product be the String value consisting solely of the code unit 0x0022 (QUOTATION MARK). + let mut product = Vec::with_capacity(value.utf16_len(agent) + 2); + product.push('"'); + // 2. For each code point C of StringToCodePoints(value), do + for c in value.as_str(agent).chars() { + match c { + // a. If C is listed in the “Code Point” column of Table 75, then + // i. Set product to the string-concatenation of product and the escape sequence for C as specified in the “Escape Sequence” column of the corresponding row. + '\u{0008}' => product.extend_from_slice(&['\\', 'b']), + '\u{0009}' => product.extend_from_slice(&['\\', 't']), + '\u{000A}' => product.extend_from_slice(&['\\', 'n']), + '\u{000C}' => product.extend_from_slice(&['\\', 'f']), + '\u{000D}' => product.extend_from_slice(&['\\', 'r']), + '\u{0022}' => product.extend_from_slice(&['\\', '"']), + '\u{005C}' => product.extend_from_slice(&['\\', '\\']), + // b. Else if C has a numeric value less than 0x0020 (SPACE) or C has the same numeric value as a leading surrogate or trailing surrogate, then + // i. Let unit be the code unit whose numeric value is the numeric value of C. + // ii. Set product to the string-concatenation of product and UnicodeEscape(unit). + _ if c < '\u{0020}' => product.extend(format!("\\u{:04x}", c as u32).chars()), + // c. Else, + // i. Set product to the string-concatenation of product and UTF16EncodeCodePoint(C). + _ => product.push(c), + } + } + // 3. Set product to the string-concatenation of product and the code unit 0x0022 (QUOTATION MARK). + product.push('"'); + // 4. Return product. + String::from_string(agent, product.iter().collect(), gc) +} + +/// ### [25.5.2.5 SerializeJSONObject ( state, value )](https://tc39.es/ecma262/#sec-serializejsonobject) +/// +/// The abstract operation SerializeJSONObject takes arguments state (a JSON +/// Serialization Record) and value (an Object) and returns either a normal +/// completion containing a String or a throw completion. It serializes an +/// object. +fn serialize_json_object( + agent: &mut Agent, + state: &mut JSONSerializationRecord, + value: Object, + mut gc: GcScope<'_, '_>, +) -> JsResult> { + // 1. If state.[[Stack]] contains value, throw a TypeError exception because the structure is cyclical. + if state.stack.contains(&value) { + return Err(agent.throw_exception_with_static_message( + ExceptionType::TypeError, + "Cyclical structure in JSON", + gc.nogc(), + )); + } + // 2. Append value to state.[[Stack]]. + state.stack.push(value); + // 3. Let stepBack be state.[[Indent]]. + let step_back = state.indent.clone(); + // 4. Set state.[[Indent]] to the string-concatenation of state.[[Indent]] and state.[[Gap]]. + state.indent.push_str(&state.gap); + // 5. If state.[[PropertyList]] is not undefined, then + // a. Let K be state.[[PropertyList]]. + // 6. Else, + // a. Let K be ? EnumerableOwnProperties(value, key). + let k = state.property_list.as_ref().map_or_else( + || { + enumerable_own_properties::(agent, value, gc.reborrow()).map(|k| { + k.iter() + .map(|k| k.to_string(agent, gc.reborrow()).unwrap().unbind()) + .collect() + }) + }, + |k| Ok(k.to_vec()), + )?; + // 7. Let partial be a new empty List. + let mut partial = Vec::with_capacity(k.len()); + // 8. For each element P of K, do + for p in k { + // a. Let strP be ? SerializeJSONProperty(state, P, value). + // SAFETY: Because the enumerable type is key we are guaranteed that the property is a valid property key. + let str_p = serialize_json_property(agent, state, p, value, gc.reborrow())?; + // b. If strP is not undefined, then + let Some(str_p) = str_p else { + continue; + }; + + // i. Let member be QuoteJSONString(P). + let member = quote_json_string(agent, p, gc.nogc()).unbind(); + // ii. Set member to the string-concatenation of member and ":". + let mut member = vec![ + member, + String::from_static_str(agent, ":", gc.nogc()).unbind(), + ]; + // iii. If state.[[Gap]] is not the empty String, then + if !state.gap.is_empty() { + // 1. Set member to the string-concatenation of member and the code unit 0x0020 (SPACE). + member.push(BUILTIN_STRING_MEMORY.__); + }; + // iv. Set member to the string-concatenation of member and strP. + member.push(str_p); + // let member = String::concat(agent, member, gc.nogc()).unbind(); + // v. Append member to partial. + partial.push(member); + } + + // 9. If partial is empty, then + let r#final = if partial.is_empty() { + // a. Let final be "{}". + String::from_static_str(agent, "{}", gc.nogc()) + } else { + let newline = String::from_static_str(agent, "\n", gc.nogc()).unbind(); + let opening_brace = String::from_static_str(agent, "{", gc.nogc()).unbind(); + let closing_brace = String::from_static_str(agent, "}", gc.nogc()).unbind(); + let mut separator = vec![String::from_static_str(agent, ",", gc.nogc()).unbind()]; + // 10. Else, + // a. If state.[[Gap]] is the empty String, then + if state.gap.is_empty() { + // i. Let properties be the String value formed by concatenating all the element Strings of partial with each adjacent pair of Strings separated with the code unit 0x002C (COMMA). A comma is not inserted either before the first String or after the last String. + // ii. Let final be the string-concatenation of "{", properties, and "}". + String::concat( + agent, + once(opening_brace) + .chain(partial.into_iter().intersperse(separator).flatten()) + .chain(once(closing_brace)) + .collect::>>(), + gc.nogc(), + ) + } else { + let indent = String::from_string(agent, state.indent.clone(), gc.nogc()).unbind(); + let step_back = String::from_string(agent, step_back.clone(), gc.nogc()).unbind(); + separator.extend_from_slice(&[newline, indent]); + // b. Else, + // i. Let separator be the string-concatenation of the code unit 0x002C (COMMA), the code unit 0x000A (LINE FEED), and state.[[Indent]]. + // ii. Let properties be the String value formed by concatenating all the element Strings of partial with each adjacent pair of Strings separated with separator. The separator String is not inserted either before the first String or after the last String. + // iii. Let final be the string-concatenation of "{", the code unit 0x000A (LINE FEED), state.[[Indent]], properties, the code unit 0x000A (LINE FEED), stepBack, and "}". + String::concat( + agent, + once(opening_brace) + .chain(once(newline)) + .chain(once(indent)) + .chain(partial.into_iter().intersperse(separator).flatten()) + .chain(once(newline)) + .chain(once(step_back)) + .chain(once(closing_brace)) + .collect::>>(), + gc.nogc(), + ) + } + }; + // 11. Remove the last element of state.[[Stack]]. + state.stack.pop(); + // 12. Set state.[[Indent]] to stepBack. + state.indent = step_back; + // 13. Return final. + Ok(r#final.unbind()) +} + +/// ### [25.5.2.6 SerializeJSONArray ( state, value )](https://tc39.es/ecma262/#sec-serializejsonarray) +/// +/// The abstract operation SerializeJSONArray takes arguments state (a JSON +/// Serialization Record) and value (an ECMAScript language value) and returns +/// either a normal completion containing a String or a throw completion. It +/// serializes an array. +fn serialize_json_array( + agent: &mut Agent, + state: &mut JSONSerializationRecord, + value: Object, + mut gc: GcScope<'_, '_>, +) -> JsResult> { + // 1. If state.[[Stack]] contains value, throw a TypeError exception because the structure is cyclical. + if state.stack.contains(&value) { + return Err(agent.throw_exception_with_static_message( + ExceptionType::TypeError, + "Cyclical structure in JSON", + gc.nogc(), + )); + } + // 2. Append value to state.[[Stack]]. + state.stack.push(value); + // 3. Let stepBack be state.[[Indent]]. + let step_back = state.indent.clone(); + // 4. Set state.[[Indent]] to the string-concatenation of state.[[Indent]] and state.[[Gap]]. + state.indent.push_str(&state.gap); + // 5. Let partial be a new empty List. + let mut partial = Vec::new(); + // 6. Let len be ? LengthOfArrayLike(value). + let len = length_of_array_like(agent, value, gc.reborrow())?; + // 7. Let index be 0. + let mut index = 0; + // 8. Repeat, while index < len, + while index < len { + let key = to_string(agent, Number::try_from(index).unwrap(), gc.reborrow()) + .unwrap() + .unbind(); + partial.push( + // a. Let strP be ? SerializeJSONProperty(state, ! ToString(𝔽(index)), value). + if let Some(str_p) = serialize_json_property(agent, state, key, value, gc.reborrow())? { + // c. Else, + // i. Append strP to partial. + str_p.unbind() + } else { + // b. If strP is undefined, then + // i. Append "null" to partial. + BUILTIN_STRING_MEMORY.null.unbind() + }, + ); + // d. Set index to index + 1. + index += 1; + } + // 9. If partial is empty, then + let r#final = if partial.is_empty() { + // a. Let final be "[]". + String::from_static_str(agent, "[]", gc.nogc()) + } else { + let newline = String::from_static_str(agent, "\n", gc.nogc()).unbind(); + let opening_bracket = String::from_static_str(agent, "[", gc.nogc()).unbind(); + let closing_bracket = String::from_static_str(agent, "]", gc.nogc()).unbind(); + let mut separator = vec![String::from_static_str(agent, ",", gc.nogc()).unbind()]; + // 10. Else, + // a. If state.[[Gap]] is the empty String, then + if state.gap.is_empty() { + // i. Let properties be the String value formed by concatenating all the element Strings of partial with each adjacent pair of Strings separated with the code unit 0x002C (COMMA). A comma is not inserted either before the first String or after the last String. + // ii. Let final be the string-concatenation of "[", properties, and "]". + String::concat( + agent, + once(opening_bracket) + .chain( + partial + .into_iter() + .map(|item| vec![item]) + .intersperse(separator) + .flatten(), + ) + .chain(once(closing_bracket)) + .collect::>>(), + gc.nogc(), + ) + } else { + let indent = String::from_string(agent, state.indent.clone(), gc.nogc()).unbind(); + let step_back = String::from_string(agent, step_back.clone(), gc.nogc()).unbind(); + separator.extend_from_slice(&[newline, indent]); + // b. Else, + // i. Let separator be the string-concatenation of the code unit 0x002C (COMMA), the code unit 0x000A (LINE FEED), and state.[[Indent]]. + // ii. Let properties be the String value formed by concatenating all the element Strings of partial with each adjacent pair of Strings separated with separator. The separator String is not inserted either before the first String or after the last String. + // iii. Let final be the string-concatenation of "[", the code unit 0x000A (LINE FEED), state.[[Indent]], properties, the code unit 0x000A (LINE FEED), stepBack, and "]". + String::concat( + agent, + once(opening_bracket) + .chain(once(newline)) + .chain(once(indent)) + .chain( + partial + .into_iter() + .map(|item| vec![item]) + .intersperse(separator) + .flatten(), + ) + .chain(once(newline)) + .chain(once(step_back)) + .chain(once(closing_bracket)) + .collect::>>(), + gc.nogc(), + ) + } + }; + // 11. Remove the last element of state.[[Stack]]. + state.stack.pop(); + // 12. Set state.[[Indent]] to stepBack. + state.indent = step_back; + // 13. Return final. + Ok(r#final.unbind()) +} + pub(crate) fn value_from_json( agent: &mut Agent, json: &sonic_rs::Value, diff --git a/nova_vm/src/ecmascript/types/language/object/property_key.rs b/nova_vm/src/ecmascript/types/language/object/property_key.rs index 705145b4..6c6b3c4d 100644 --- a/nova_vm/src/ecmascript/types/language/object/property_key.rs +++ b/nova_vm/src/ecmascript/types/language/object/property_key.rs @@ -309,7 +309,15 @@ impl<'a> From> for PropertyKey<'a> { fn from(value: String<'a>) -> Self { match value { String::String(x) => PropertyKey::String(x), - String::SmallString(x) => PropertyKey::SmallString(x), + String::SmallString(x) => { + // NOTE: Makes property keys slightly more correct by converting + // small strings to integers when possible. + if let Ok(n) = x.as_str().parse::() { + return PropertyKey::Integer(SmallInteger::try_from(n).unwrap()); + } + + PropertyKey::SmallString(x) + } } } } diff --git a/nova_vm/src/lib.rs b/nova_vm/src/lib.rs index 99489c0b..068a3738 100644 --- a/nova_vm/src/lib.rs +++ b/nova_vm/src/lib.rs @@ -3,6 +3,7 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. #![allow(dead_code)] +#![feature(iter_intersperse)] pub mod ecmascript; pub mod engine; diff --git a/tests/expectations.json b/tests/expectations.json index 0830b1e8..a5135ae5 100644 --- a/tests/expectations.json +++ b/tests/expectations.json @@ -1963,67 +1963,20 @@ "built-ins/JSON/parse/reviver-object-delete-err.js": "CRASH", "built-ins/JSON/parse/reviver-object-own-keys-err.js": "CRASH", "built-ins/JSON/parse/text-negative-zero.js": "FAIL", - "built-ins/JSON/stringify/property-order.js": "FAIL", - "built-ins/JSON/stringify/replacer-array-abrupt.js": "CRASH", - "built-ins/JSON/stringify/replacer-array-duplicates.js": "FAIL", - "built-ins/JSON/stringify/replacer-array-empty.js": "FAIL", - "built-ins/JSON/stringify/replacer-array-number-object.js": "FAIL", - "built-ins/JSON/stringify/replacer-array-number.js": "FAIL", - "built-ins/JSON/stringify/replacer-array-order.js": "FAIL", "built-ins/JSON/stringify/replacer-array-proxy-revoked-realm.js": "FAIL", "built-ins/JSON/stringify/replacer-array-proxy-revoked.js": "CRASH", - "built-ins/JSON/stringify/replacer-array-proxy.js": "CRASH", - "built-ins/JSON/stringify/replacer-array-string-object.js": "FAIL", - "built-ins/JSON/stringify/replacer-array-undefined.js": "FAIL", - "built-ins/JSON/stringify/replacer-array-wrong-type.js": "CRASH", - "built-ins/JSON/stringify/replacer-function-abrupt.js": "FAIL", - "built-ins/JSON/stringify/replacer-function-arguments.js": "FAIL", - "built-ins/JSON/stringify/replacer-function-array-circular.js": "CRASH", - "built-ins/JSON/stringify/replacer-function-object-circular.js": "CRASH", - "built-ins/JSON/stringify/replacer-function-object-deleted-property.js": "FAIL", - "built-ins/JSON/stringify/replacer-function-result-undefined.js": "FAIL", - "built-ins/JSON/stringify/replacer-function-result.js": "FAIL", - "built-ins/JSON/stringify/replacer-function-tojson.js": "FAIL", - "built-ins/JSON/stringify/replacer-function-wrapper.js": "FAIL", - "built-ins/JSON/stringify/replacer-wrong-type.js": "FAIL", "built-ins/JSON/stringify/space-number-float.js": "FAIL", "built-ins/JSON/stringify/space-number-object.js": "FAIL", - "built-ins/JSON/stringify/space-number-range.js": "FAIL", - "built-ins/JSON/stringify/space-number.js": "FAIL", "built-ins/JSON/stringify/space-string-object.js": "FAIL", - "built-ins/JSON/stringify/space-string-range.js": "FAIL", "built-ins/JSON/stringify/space-string.js": "FAIL", - "built-ins/JSON/stringify/space-wrong-type.js": "FAIL", - "built-ins/JSON/stringify/value-array-abrupt.js": "CRASH", - "built-ins/JSON/stringify/value-array-circular.js": "CRASH", "built-ins/JSON/stringify/value-array-proxy-revoked.js": "CRASH", - "built-ins/JSON/stringify/value-array-proxy.js": "CRASH", "built-ins/JSON/stringify/value-bigint-cross-realm.js": "FAIL", - "built-ins/JSON/stringify/value-bigint-order.js": "FAIL", - "built-ins/JSON/stringify/value-bigint-replacer.js": "FAIL", "built-ins/JSON/stringify/value-bigint-tojson-receiver.js": "FAIL", "built-ins/JSON/stringify/value-bigint-tojson.js": "FAIL", - "built-ins/JSON/stringify/value-bigint.js": "CRASH", - "built-ins/JSON/stringify/value-boolean-object.js": "FAIL", - "built-ins/JSON/stringify/value-function.js": "FAIL", - "built-ins/JSON/stringify/value-number-negative-zero.js": "FAIL", - "built-ins/JSON/stringify/value-number-non-finite.js": "FAIL", - "built-ins/JSON/stringify/value-number-object.js": "FAIL", - "built-ins/JSON/stringify/value-object-abrupt.js": "FAIL", - "built-ins/JSON/stringify/value-object-circular.js": "CRASH", "built-ins/JSON/stringify/value-object-proxy-revoked.js": "CRASH", "built-ins/JSON/stringify/value-object-proxy.js": "CRASH", - "built-ins/JSON/stringify/value-primitive-top-level.js": "FAIL", "built-ins/JSON/stringify/value-string-escape-ascii.js": "CRASH", "built-ins/JSON/stringify/value-string-escape-unicode.js": "FAIL", - "built-ins/JSON/stringify/value-string-object.js": "FAIL", - "built-ins/JSON/stringify/value-symbol.js": "FAIL", - "built-ins/JSON/stringify/value-tojson-abrupt.js": "FAIL", - "built-ins/JSON/stringify/value-tojson-arguments.js": "FAIL", - "built-ins/JSON/stringify/value-tojson-array-circular.js": "CRASH", - "built-ins/JSON/stringify/value-tojson-not-function.js": "FAIL", - "built-ins/JSON/stringify/value-tojson-object-circular.js": "CRASH", - "built-ins/JSON/stringify/value-tojson-result.js": "FAIL", "built-ins/Map/groupBy/callback-arg.js": "CRASH", "built-ins/Map/groupBy/callback-throws.js": "CRASH", "built-ins/Map/groupBy/emptyList.js": "CRASH", @@ -11227,10 +11180,7 @@ "harness/asyncHelpers-throwsAsync-resolved-error.js": "CRASH", "harness/asyncHelpers-throwsAsync-same-realm.js": "CRASH", "harness/asyncHelpers-throwsAsync-single-arg.js": "CRASH", - "harness/deepEqual-array.js": "CRASH", - "harness/deepEqual-mapset.js": "CRASH", "harness/deepEqual-primitives-bigint.js": "CRASH", - "harness/deepEqual-primitives.js": "FAIL", "harness/nativeFunctionMatcher.js": "CRASH", "harness/testTypedArray-conversions.js": "CRASH", "language/arguments-object/10.6-10-c-ii-1.js": "FAIL", diff --git a/tests/metrics.json b/tests/metrics.json index 0a581c32..74ac6858 100644 --- a/tests/metrics.json +++ b/tests/metrics.json @@ -1,8 +1,8 @@ { "results": { - "crash": 14292, - "fail": 7339, - "pass": 23612, + "crash": 14234, + "fail": 7347, + "pass": 23662, "skip": 45, "timeout": 3, "unresolved": 0