From b1a49cda55368c904069bf0ee03860ea9420606d Mon Sep 17 00:00:00 2001 From: Zaid <161572905+iammdzaidalam@users.noreply.github.com> Date: Thu, 19 Mar 2026 07:07:16 +0530 Subject: [PATCH 1/4] perf(json): use FxHashSet for cycle detection in JSON.stringify --- core/engine/src/builtins/json/mod.rs | 379 +++++++++++++------------ core/engine/src/builtins/json/tests.rs | 27 ++ 2 files changed, 230 insertions(+), 176 deletions(-) diff --git a/core/engine/src/builtins/json/mod.rs b/core/engine/src/builtins/json/mod.rs index 7b4d9aafed3..fc574282468 100644 --- a/core/engine/src/builtins/json/mod.rs +++ b/core/engine/src/builtins/json/mod.rs @@ -15,6 +15,8 @@ use std::{borrow::Cow, iter::once}; +use rustc_hash::FxHashSet; + use boa_ast::scope::Scope; use boa_gc::{Finalize, Gc, Trace}; use boa_macros::utf16; @@ -748,6 +750,7 @@ impl Json { let mut state = StateRecord { replacer_function, stack, + stack_set: FxHashSet::default(), indent, gap, property_list, @@ -965,7 +968,7 @@ impl Json { context: &mut Context, ) -> JsResult { // 1. If state.[[Stack]] contains value, throw a TypeError exception because the structure is cyclical. - if state.stack.contains(value) { + if state.stack_set.contains(value) { return Err(JsNativeError::typ() .with_message("cyclic object value") .into()); @@ -973,119 +976,131 @@ impl Json { // 2. Append value to state.[[Stack]]. state.stack.push(value.clone()); + state.stack_set.insert(value.clone()); - // 3. Let stepback be state.[[Indent]]. - let stepback = state.indent.clone(); + // Use a closure to ensure we always pop the stack and remove from the set, even on early returns (Err). + let mut inner = || -> JsResult { + // 3. Let stepback be state.[[Indent]]. + let stepback = state.indent.clone(); - // 4. Set state.[[Indent]] to the string-concatenation of state.[[Indent]] and state.[[Gap]]. - state.indent = js_string!(&state.indent, &state.gap); + // 4. Set state.[[Indent]] to the string-concatenation of state.[[Indent]] and state.[[Gap]]. + state.indent = js_string!(&state.indent, &state.gap); - // 5. If state.[[PropertyList]] is not undefined, then - let k = if let Some(p) = &state.property_list { - // a. Let K be state.[[PropertyList]]. - p.clone() - // 6. Else, - } else { - // a. Let K be ? EnumerableOwnPropertyNames(value, key). - let keys = value.enumerable_own_property_names(PropertyNameKind::Key, context)?; - // Unwrap is safe, because EnumerableOwnPropertyNames with kind "key" only returns string values. - keys.iter() - .map(|v| { - v.to_string(context) - .js_expect("EnumerableOwnPropertyNames only returns strings") - }) - .collect::, _>>()? - }; + // 5. If state.[[PropertyList]] is not undefined, then + let k = if let Some(p) = &state.property_list { + // a. Let K be state.[[PropertyList]]. + p.clone() + // 6. Else, + } else { + // a. Let K be ? EnumerableOwnPropertyNames(value, key). + let keys = value.enumerable_own_property_names(PropertyNameKind::Key, context)?; + // Unwrap is safe, because EnumerableOwnPropertyNames with kind "key" only returns string values. + keys.iter() + .map(|v| { + v.to_string(context) + .js_expect("EnumerableOwnPropertyNames only returns strings") + }) + .collect::, _>>()? + }; - // 7. Let partial be a new empty List. - let mut partial = Vec::with_capacity(k.len()); + // 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). - let str_p = Self::serialize_json_property(state, p.clone(), value, context)?; + // 8. For each element P of K, do + for p in &k { + // a. Let strP be ? SerializeJSONProperty(state, P, value). + let str_p = Self::serialize_json_property(state, p.clone(), value, context)?; - // b. If strP is not undefined, then - if let Some(str_p) = str_p { - // i. Let member be QuoteJSONString(P). - let mut member = Self::quote_json_string(p).iter().collect::>(); + // b. If strP is not undefined, then + if let Some(str_p) = str_p { + // i. Let member be QuoteJSONString(P). + let mut member = Self::quote_json_string(p).iter().collect::>(); - // ii. Set member to the string-concatenation of member and ":". - member.push(':' as u16); + // ii. Set member to the string-concatenation of member and ":". + member.push(':' as u16); - // 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(' ' as u16); - } + // 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(' ' as u16); + } - // iv. Set member to the string-concatenation of member and strP. - member.extend(str_p.iter()); + // iv. Set member to the string-concatenation of member and strP. + member.extend(str_p.iter()); - // v. Append member to partial. - partial.push(member); + // 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 "{}". - js_string!("{}") - // 10. Else, - } 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 "}". - let separator = utf16!(","); - let result = once(utf16!("{")) - .chain(Itertools::intersperse( - partial.iter().map(Vec::as_slice), - separator, - )) - .chain(once(utf16!("}"))) - .flatten() - .copied() - .collect::>(); - js_string!(&result[..]) - // b. Else, + // 9. If partial is empty, then + let r#final = if partial.is_empty() { + // a. Let final be "{}". + js_string!("{}") + // 10. Else, } else { - // i. Let separator be the string-concatenation of the code unit 0x002C (COMMA), - // the code unit 0x000A (LINE FEED), and state.[[Indent]]. - let mut separator = utf16!(",\n").to_vec(); - separator.extend(state.indent.iter()); - // 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 "}". - let indent_vec = state.indent.to_vec(); - let stepback_vec = stepback.to_vec(); - let result = [utf16!("{\n"), &indent_vec[..]] - .into_iter() - .chain(Itertools::intersperse( - partial.iter().map(Vec::as_slice), - &separator, - )) - .chain([utf16!("\n"), &stepback_vec[..], utf16!("}")]) - .flatten() - .copied() - .collect::>(); - js_string!(&result[..]) - } + // 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 "}". + let separator = utf16!(","); + let result = once(utf16!("{")) + .chain(Itertools::intersperse( + partial.iter().map(Vec::as_slice), + separator, + )) + .chain(once(utf16!("}"))) + .flatten() + .copied() + .collect::>(); + js_string!(&result[..]) + // b. Else, + } else { + // i. Let separator be the string-concatenation of the code unit 0x002C (COMMA), + // the code unit 0x000A (LINE FEED), and state.[[Indent]]. + let mut separator = utf16!(",\n").to_vec(); + separator.extend(state.indent.iter()); + // 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 "}". + let indent_vec = state.indent.to_vec(); + let stepback_vec = stepback.to_vec(); + let result = [utf16!("{\n"), &indent_vec[..]] + .into_iter() + .chain(Itertools::intersperse( + partial.iter().map(Vec::as_slice), + &separator, + )) + .chain([utf16!("\n"), &stepback_vec[..], utf16!("}")]) + .flatten() + .copied() + .collect::>(); + js_string!(&result[..]) + } + }; + + // 12. Set state.[[Indent]] to stepback. + state.indent = stepback; + + // 13. Return final. + Ok(r#final) }; - // 11. Remove the last element of state.[[Stack]]. - state.stack.pop(); + let result = inner(); - // 12. Set state.[[Indent]] to stepback. - state.indent = stepback; + // 11. Remove the last element of state.[[Stack]]. + let popped = state + .stack + .pop() + .expect("json stringify stack should not be empty"); + state.stack_set.remove(&popped); - // 13. Return final. - Ok(r#final) + result } /// `25.5.2.5 SerializeJSONArray ( state, value )` @@ -1100,7 +1115,7 @@ impl Json { context: &mut Context, ) -> JsResult { // 1. If state.[[Stack]] contains value, throw a TypeError exception because the structure is cyclical. - if state.stack.contains(value) { + if state.stack_set.contains(value) { return Err(JsNativeError::typ() .with_message("cyclic object value") .into()); @@ -1108,104 +1123,116 @@ impl Json { // 2. Append value to state.[[Stack]]. state.stack.push(value.clone()); + state.stack_set.insert(value.clone()); - // 3. Let stepback be state.[[Indent]]. - let stepback = state.indent.clone(); + let mut inner = || -> JsResult { + // 3. Let stepback be state.[[Indent]]. + let stepback = state.indent.clone(); - // 4. Set state.[[Indent]] to the string-concatenation of state.[[Indent]] and state.[[Gap]]. - state.indent = js_string!(&state.indent, &state.gap); + // 4. Set state.[[Indent]] to the string-concatenation of state.[[Indent]] and state.[[Gap]]. + state.indent = js_string!(&state.indent, &state.gap); - // 6. Let len be ? LengthOfArrayLike(value). - let len = value.length_of_array_like(context)?; + // 6. Let len be ? LengthOfArrayLike(value). + let len = value.length_of_array_like(context)?; - // 5. Let partial be a new empty List. - let mut partial = Vec::with_capacity(len as usize); + // 5. Let partial be a new empty List. + let mut partial = Vec::with_capacity(len as usize); - // 7. Let index be 0. - let mut index = 0; + // 7. Let index be 0. + let mut index = 0; - // 8. Repeat, while index < len, - while index < len { - // a. Let strP be ? SerializeJSONProperty(state, ! ToString(𝔽(index)), value). - let str_p = Self::serialize_json_property(state, index.into(), value, context)?; + // 8. Repeat, while index < len, + while index < len { + // a. Let strP be ? SerializeJSONProperty(state, ! ToString(𝔽(index)), value). + let str_p = Self::serialize_json_property(state, index.into(), value, context)?; - // b. If strP is undefined, then - if let Some(str_p) = str_p { - // i. Append strP to partial. - partial.push(Cow::Owned(str_p.iter().collect::<_>())); - // c. Else, - } else { - // i. Append "null" to partial. - partial.push(Cow::Borrowed(utf16!("null"))); - } + // b. If strP is undefined, then + if let Some(str_p) = str_p { + // i. Append strP to partial. + partial.push(Cow::Owned(str_p.iter().collect::<_>())); + // c. Else, + } else { + // i. Append "null" to partial. + partial.push(Cow::Borrowed(utf16!("null"))); + } - // d. Set index to index + 1. - index += 1; - } + // 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 "[]". - js_string!("[]") - // 10. Else, - } 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 "]". - let separator = utf16!(","); - let result = once(utf16!("[")) - .chain(Itertools::intersperse( - partial.iter().map(Cow::as_ref), - separator, - )) - .chain(once(utf16!("]"))) - .flatten() - .copied() - .collect::>(); - js_string!(&result[..]) - // b. Else, + // 9. If partial is empty, then + let r#final = if partial.is_empty() { + // a. Let final be "[]". + js_string!("[]") + // 10. Else, } else { - // i. Let separator be the string-concatenation of the code unit 0x002C (COMMA), - // the code unit 0x000A (LINE FEED), and state.[[Indent]]. - let mut separator = utf16!(",\n").to_vec(); - separator.extend(state.indent.iter()); - // 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 "]". - let indent_vec = state.indent.to_vec(); - let stepback_vec = stepback.to_vec(); - let result = [utf16!("[\n"), &indent_vec[..]] - .into_iter() - .chain(Itertools::intersperse( - partial.iter().map(Cow::as_ref), - &separator, - )) - .chain([utf16!("\n"), &stepback_vec[..], utf16!("]")]) - .flatten() - .copied() - .collect::>(); - js_string!(&result[..]) - } + // 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 "]". + let separator = utf16!(","); + let result = once(utf16!("[")) + .chain(Itertools::intersperse( + partial.iter().map(Cow::as_ref), + separator, + )) + .chain(once(utf16!("]"))) + .flatten() + .copied() + .collect::>(); + js_string!(&result[..]) + // b. Else, + } else { + // i. Let separator be the string-concatenation of the code unit 0x002C (COMMA), + // the code unit 0x000A (LINE FEED), and state.[[Indent]]. + let mut separator = utf16!(",\n").to_vec(); + separator.extend(state.indent.iter()); + // 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 "]". + let indent_vec = state.indent.to_vec(); + let stepback_vec = stepback.to_vec(); + let result = [utf16!("[\n"), &indent_vec[..]] + .into_iter() + .chain(Itertools::intersperse( + partial.iter().map(Cow::as_ref), + &separator, + )) + .chain([utf16!("\n"), &stepback_vec[..], utf16!("]")]) + .flatten() + .copied() + .collect::>(); + js_string!(&result[..]) + } + }; + + // 12. Set state.[[Indent]] to stepback. + state.indent = stepback; + + // 13. Return final. + Ok(r#final) }; - // 11. Remove the last element of state.[[Stack]]. - state.stack.pop(); + let result = inner(); - // 12. Set state.[[Indent]] to stepback. - state.indent = stepback; + // 11. Remove the last element of state.[[Stack]]. + let popped = state + .stack + .pop() + .expect("json stringify stack should not be empty"); + state.stack_set.remove(&popped); - // 13. Return final. - Ok(r#final) + result } } struct StateRecord { replacer_function: Option, stack: Vec, + stack_set: FxHashSet, indent: JsString, gap: JsString, property_list: Option>, diff --git a/core/engine/src/builtins/json/tests.rs b/core/engine/src/builtins/json/tests.rs index af3d1f6a8a8..6c58066c1c8 100644 --- a/core/engine/src/builtins/json/tests.rs +++ b/core/engine/src/builtins/json/tests.rs @@ -315,3 +315,30 @@ fn json_parse_with_no_args_throws_syntax_error() { "expected value at line 1 column 1", )]); } + +#[test] +fn json_stringify_cyclic_object_throws_type_error() { + run_test_actions([TestAction::assert_native_error( + "var a = {}; a.a = a; JSON.stringify(a);", + JsNativeErrorKind::Type, + "cyclic object value", + )]); +} + +#[test] +fn json_stringify_cyclic_array_throws_type_error() { + run_test_actions([TestAction::assert_native_error( + "var a = []; a[0] = a; JSON.stringify(a);", + JsNativeErrorKind::Type, + "cyclic object value", + )]); +} + +#[test] +fn json_stringify_cyclic_nested_object_throws_type_error() { + run_test_actions([TestAction::assert_native_error( + "var a = {}; var b = { a }; a.b = b; JSON.stringify(a);", + JsNativeErrorKind::Type, + "cyclic object value", + )]); +} From 0c60957e05d01bd3c229efb2845ec663adec1709 Mon Sep 17 00:00:00 2001 From: Zaid <161572905+iammdzaidalam@users.noreply.github.com> Date: Thu, 19 Mar 2026 22:40:07 +0530 Subject: [PATCH 2/4] bench(json): add focused JSON.stringify benchmarks --- benches/scripts/json/stringify_circular.js | 24 ++++++++++++++++++++++ benches/scripts/json/stringify_deep.js | 19 +++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 benches/scripts/json/stringify_circular.js create mode 100644 benches/scripts/json/stringify_deep.js diff --git a/benches/scripts/json/stringify_circular.js b/benches/scripts/json/stringify_circular.js new file mode 100644 index 00000000000..98bffc858d2 --- /dev/null +++ b/benches/scripts/json/stringify_circular.js @@ -0,0 +1,24 @@ +// Creates a circular structure at depth to measure efficient cycle detection. +// Catches the error to allow the benchmark to run repeatedly. + +function createCircularObject(depth) { + let root = {}; + let cur = root; + for (let i = 0; i < depth; i++) { + cur.next = {}; + cur = cur.next; + } + cur.next = root; // Create the cycle back to the root + return root; +} + +const circularObj = createCircularObject(100); + +function main() { + try { + JSON.stringify(circularObj); + } catch (_) { + // Expected TypeError: cyclic object value + return; + } +} diff --git a/benches/scripts/json/stringify_deep.js b/benches/scripts/json/stringify_deep.js new file mode 100644 index 00000000000..5c79d5c40e2 --- /dev/null +++ b/benches/scripts/json/stringify_deep.js @@ -0,0 +1,19 @@ +// Creates a deep acyclic object to measure linear vs O(1) cycle detection cost. +// Depth of 400 is large enough to show the benefit while avoiding stack overflow. + +function createDeepObject(depth) { + let root = {}; + let cur = root; + for (let i = 0; i < depth; i++) { + cur.value = i; + cur.next = {}; + cur = cur.next; + } + return root; +} + +const deepObj = createDeepObject(400); + +function main() { + return JSON.stringify(deepObj); +} From deff662b7063db6607a80c0fbd5417947714f1cc Mon Sep 17 00:00:00 2001 From: Zaid <161572905+iammdzaidalam@users.noreply.github.com> Date: Thu, 19 Mar 2026 22:45:13 +0530 Subject: [PATCH 3/4] bench(json): add focused JSON.stringify benchmarks --- benches/scripts/json/stringify_circular.js | 28 +++++++++++----------- benches/scripts/json/stringify_deep.js | 18 +++++++------- 2 files changed, 23 insertions(+), 23 deletions(-) diff --git a/benches/scripts/json/stringify_circular.js b/benches/scripts/json/stringify_circular.js index 98bffc858d2..3c986764284 100644 --- a/benches/scripts/json/stringify_circular.js +++ b/benches/scripts/json/stringify_circular.js @@ -2,23 +2,23 @@ // Catches the error to allow the benchmark to run repeatedly. function createCircularObject(depth) { - let root = {}; - let cur = root; - for (let i = 0; i < depth; i++) { - cur.next = {}; - cur = cur.next; - } - cur.next = root; // Create the cycle back to the root - return root; + let root = {}; + let cur = root; + for (let i = 0; i < depth; i++) { + cur.next = {}; + cur = cur.next; + } + cur.next = root; // Create the cycle back to the root + return root; } const circularObj = createCircularObject(100); function main() { - try { - JSON.stringify(circularObj); - } catch (_) { - // Expected TypeError: cyclic object value - return; - } + try { + JSON.stringify(circularObj); + } catch (_) { + // Expected TypeError: cyclic object value + return; + } } diff --git a/benches/scripts/json/stringify_deep.js b/benches/scripts/json/stringify_deep.js index 5c79d5c40e2..a598911bf0d 100644 --- a/benches/scripts/json/stringify_deep.js +++ b/benches/scripts/json/stringify_deep.js @@ -2,18 +2,18 @@ // Depth of 400 is large enough to show the benefit while avoiding stack overflow. function createDeepObject(depth) { - let root = {}; - let cur = root; - for (let i = 0; i < depth; i++) { - cur.value = i; - cur.next = {}; - cur = cur.next; - } - return root; + let root = {}; + let cur = root; + for (let i = 0; i < depth; i++) { + cur.value = i; + cur.next = {}; + cur = cur.next; + } + return root; } const deepObj = createDeepObject(400); function main() { - return JSON.stringify(deepObj); + return JSON.stringify(deepObj); } From 3fbccc7a630920fb70bb49988d155a8121d22403 Mon Sep 17 00:00:00 2001 From: Zaid <161572905+iammdzaidalam@users.noreply.github.com> Date: Sat, 21 Mar 2026 00:46:05 +0530 Subject: [PATCH 4/4] perf(json): remove redundant stringify stack --- core/engine/src/builtins/json/mod.rs | 435 ++++++++++++++------------- 1 file changed, 222 insertions(+), 213 deletions(-) diff --git a/core/engine/src/builtins/json/mod.rs b/core/engine/src/builtins/json/mod.rs index fc574282468..cc99f2fc42b 100644 --- a/core/engine/src/builtins/json/mod.rs +++ b/core/engine/src/builtins/json/mod.rs @@ -625,13 +625,10 @@ impl Json { args: &[JsValue], context: &mut Context, ) -> JsResult { - // 1. Let stack be a new empty List. - let stack = Vec::new(); - - // 2. Let indent be the empty String. + // 1. Let indent be the empty String. let indent = js_string!(); - // 3. Let PropertyList and ReplacerFunction be undefined. + // 2. Let PropertyList and ReplacerFunction be undefined. let mut property_list = None; let mut replacer_function = None; @@ -749,7 +746,6 @@ impl Json { // 11. Let state be the Record { [[ReplacerFunction]]: ReplacerFunction, [[Stack]]: stack, [[Indent]]: indent, [[Gap]]: gap, [[PropertyList]]: PropertyList }. let mut state = StateRecord { replacer_function, - stack, stack_set: FxHashSet::default(), indent, gap, @@ -968,139 +964,151 @@ impl Json { context: &mut Context, ) -> JsResult { // 1. If state.[[Stack]] contains value, throw a TypeError exception because the structure is cyclical. - if state.stack_set.contains(value) { + if !state.stack_set.insert(value.clone()) { return Err(JsNativeError::typ() .with_message("cyclic object value") .into()); } - // 2. Append value to state.[[Stack]]. - state.stack.push(value.clone()); - state.stack_set.insert(value.clone()); - - // Use a closure to ensure we always pop the stack and remove from the set, even on early returns (Err). - let mut inner = || -> JsResult { - // 3. Let stepback be state.[[Indent]]. - let stepback = state.indent.clone(); + // 3. Let stepback be state.[[Indent]]. + let stepback = state.indent.clone(); - // 4. Set state.[[Indent]] to the string-concatenation of state.[[Indent]] and state.[[Gap]]. - state.indent = js_string!(&state.indent, &state.gap); + // 4. Set state.[[Indent]] to the string-concatenation of state.[[Indent]] and state.[[Gap]]. + state.indent = js_string!(&state.indent, &state.gap); - // 5. If state.[[PropertyList]] is not undefined, then - let k = if let Some(p) = &state.property_list { - // a. Let K be state.[[PropertyList]]. - p.clone() - // 6. Else, - } else { - // a. Let K be ? EnumerableOwnPropertyNames(value, key). - let keys = value.enumerable_own_property_names(PropertyNameKind::Key, context)?; - // Unwrap is safe, because EnumerableOwnPropertyNames with kind "key" only returns string values. - keys.iter() - .map(|v| { - v.to_string(context) - .js_expect("EnumerableOwnPropertyNames only returns strings") - }) - .collect::, _>>()? + // 5. If state.[[PropertyList]] is not undefined, then + let k = if let Some(p) = &state.property_list { + // a. Let K be state.[[PropertyList]]. + p.clone() + // 6. Else, + } else { + // a. Let K be ? EnumerableOwnPropertyNames(value, key). + let keys = value.enumerable_own_property_names(PropertyNameKind::Key, context); + // 11. Remove value from state.[[Stack]] on error. + let keys = match keys { + Ok(k) => k, + Err(e) => { + let removed = state.stack_set.remove(value); + debug_assert!(removed); + return Err(e); + } }; + // Unwrap is safe, because EnumerableOwnPropertyNames with kind "key" only returns string values. + match keys + .iter() + .map(|v| { + v.to_string(context) + .js_expect("EnumerableOwnPropertyNames only returns strings") + }) + .collect::, _>>() + { + Ok(k) => k, + Err(e) => { + let removed = state.stack_set.remove(value); + debug_assert!(removed); + return Err(e.into()); + } + } + }; - // 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). - let str_p = Self::serialize_json_property(state, p.clone(), value, context)?; + // 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). + let str_p = Self::serialize_json_property(state, p.clone(), value, context); + let str_p = match str_p { + Ok(v) => v, + Err(e) => { + let removed = state.stack_set.remove(value); + debug_assert!(removed); + return Err(e); + } + }; - // b. If strP is not undefined, then - if let Some(str_p) = str_p { - // i. Let member be QuoteJSONString(P). - let mut member = Self::quote_json_string(p).iter().collect::>(); + // b. If strP is not undefined, then + if let Some(str_p) = str_p { + // i. Let member be QuoteJSONString(P). + let mut member = Self::quote_json_string(p).iter().collect::>(); - // ii. Set member to the string-concatenation of member and ":". - member.push(':' as u16); + // ii. Set member to the string-concatenation of member and ":". + member.push(':' as u16); - // 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(' ' as u16); - } + // 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(' ' as u16); + } - // iv. Set member to the string-concatenation of member and strP. - member.extend(str_p.iter()); + // iv. Set member to the string-concatenation of member and strP. + member.extend(str_p.iter()); - // v. Append member to partial. - partial.push(member); - } + // 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 "{}". - js_string!("{}") - // 10. Else, + // 9. If partial is empty, then + let r#final = if partial.is_empty() { + // a. Let final be "{}". + js_string!("{}") + // 10. Else, + } 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 "}". + let separator = utf16!(","); + let result = once(utf16!("{")) + .chain(Itertools::intersperse( + partial.iter().map(Vec::as_slice), + separator, + )) + .chain(once(utf16!("}"))) + .flatten() + .copied() + .collect::>(); + js_string!(&result[..]) + // b. Else, } 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 "}". - let separator = utf16!(","); - let result = once(utf16!("{")) - .chain(Itertools::intersperse( - partial.iter().map(Vec::as_slice), - separator, - )) - .chain(once(utf16!("}"))) - .flatten() - .copied() - .collect::>(); - js_string!(&result[..]) - // b. Else, - } else { - // i. Let separator be the string-concatenation of the code unit 0x002C (COMMA), - // the code unit 0x000A (LINE FEED), and state.[[Indent]]. - let mut separator = utf16!(",\n").to_vec(); - separator.extend(state.indent.iter()); - // 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 "}". - let indent_vec = state.indent.to_vec(); - let stepback_vec = stepback.to_vec(); - let result = [utf16!("{\n"), &indent_vec[..]] - .into_iter() - .chain(Itertools::intersperse( - partial.iter().map(Vec::as_slice), - &separator, - )) - .chain([utf16!("\n"), &stepback_vec[..], utf16!("}")]) - .flatten() - .copied() - .collect::>(); - js_string!(&result[..]) - } - }; - - // 12. Set state.[[Indent]] to stepback. - state.indent = stepback; - - // 13. Return final. - Ok(r#final) + // i. Let separator be the string-concatenation of the code unit 0x002C (COMMA), + // the code unit 0x000A (LINE FEED), and state.[[Indent]]. + let mut separator = utf16!(",\n").to_vec(); + separator.extend(state.indent.iter()); + // 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 "}". + let indent_vec = state.indent.to_vec(); + let stepback_vec = stepback.to_vec(); + let result = [utf16!("{\n"), &indent_vec[..]] + .into_iter() + .chain(Itertools::intersperse( + partial.iter().map(Vec::as_slice), + &separator, + )) + .chain([utf16!("\n"), &stepback_vec[..], utf16!("}")]) + .flatten() + .copied() + .collect::>(); + js_string!(&result[..]) + } }; - let result = inner(); + // 12. Set state.[[Indent]] to stepback. + state.indent = stepback; - // 11. Remove the last element of state.[[Stack]]. - let popped = state - .stack - .pop() - .expect("json stringify stack should not be empty"); - state.stack_set.remove(&popped); + // 11. Remove value from state.[[Stack]]. + let removed = state.stack_set.remove(value); + debug_assert!(removed); - result + // 13. Return final. + Ok(r#final) } /// `25.5.2.5 SerializeJSONArray ( state, value )` @@ -1115,123 +1123,124 @@ impl Json { context: &mut Context, ) -> JsResult { // 1. If state.[[Stack]] contains value, throw a TypeError exception because the structure is cyclical. - if state.stack_set.contains(value) { + if !state.stack_set.insert(value.clone()) { return Err(JsNativeError::typ() .with_message("cyclic object value") .into()); } - // 2. Append value to state.[[Stack]]. - state.stack.push(value.clone()); - state.stack_set.insert(value.clone()); - - let mut inner = || -> JsResult { - // 3. Let stepback be state.[[Indent]]. - let stepback = state.indent.clone(); - - // 4. Set state.[[Indent]] to the string-concatenation of state.[[Indent]] and state.[[Gap]]. - state.indent = js_string!(&state.indent, &state.gap); - - // 6. Let len be ? LengthOfArrayLike(value). - let len = value.length_of_array_like(context)?; - - // 5. Let partial be a new empty List. - let mut partial = Vec::with_capacity(len as usize); - - // 7. Let index be 0. - let mut index = 0; + // 3. Let stepback be state.[[Indent]]. + let stepback = state.indent.clone(); - // 8. Repeat, while index < len, - while index < len { - // a. Let strP be ? SerializeJSONProperty(state, ! ToString(𝔽(index)), value). - let str_p = Self::serialize_json_property(state, index.into(), value, context)?; + // 4. Set state.[[Indent]] to the string-concatenation of state.[[Indent]] and state.[[Gap]]. + state.indent = js_string!(&state.indent, &state.gap); - // b. If strP is undefined, then - if let Some(str_p) = str_p { - // i. Append strP to partial. - partial.push(Cow::Owned(str_p.iter().collect::<_>())); - // c. Else, - } else { - // i. Append "null" to partial. - partial.push(Cow::Borrowed(utf16!("null"))); - } - - // d. Set index to index + 1. - index += 1; + // 6. Let len be ? LengthOfArrayLike(value). + let len = match value.length_of_array_like(context) { + Ok(n) => n, + Err(e) => { + let removed = state.stack_set.remove(value); + debug_assert!(removed); + return Err(e); } + }; - // 9. If partial is empty, then - let r#final = if partial.is_empty() { - // a. Let final be "[]". - js_string!("[]") - // 10. Else, - } 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 "]". - let separator = utf16!(","); - let result = once(utf16!("[")) - .chain(Itertools::intersperse( - partial.iter().map(Cow::as_ref), - separator, - )) - .chain(once(utf16!("]"))) - .flatten() - .copied() - .collect::>(); - js_string!(&result[..]) - // b. Else, - } else { - // i. Let separator be the string-concatenation of the code unit 0x002C (COMMA), - // the code unit 0x000A (LINE FEED), and state.[[Indent]]. - let mut separator = utf16!(",\n").to_vec(); - separator.extend(state.indent.iter()); - // 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 "]". - let indent_vec = state.indent.to_vec(); - let stepback_vec = stepback.to_vec(); - let result = [utf16!("[\n"), &indent_vec[..]] - .into_iter() - .chain(Itertools::intersperse( - partial.iter().map(Cow::as_ref), - &separator, - )) - .chain([utf16!("\n"), &stepback_vec[..], utf16!("]")]) - .flatten() - .copied() - .collect::>(); - js_string!(&result[..]) + // 5. Let partial be a new empty List. + let mut partial = Vec::with_capacity(len as usize); + + // 7. Let index be 0. + let mut index = 0; + + // 8. Repeat, while index < len, + while index < len { + // a. Let strP be ? SerializeJSONProperty(state, ! ToString(𝔽(index)), value). + let str_p = Self::serialize_json_property(state, index.into(), value, context); + let str_p = match str_p { + Ok(v) => v, + Err(e) => { + let removed = state.stack_set.remove(value); + debug_assert!(removed); + return Err(e); } }; - // 12. Set state.[[Indent]] to stepback. - state.indent = stepback; + // b. If strP is undefined, then + if let Some(str_p) = str_p { + // i. Append strP to partial. + partial.push(Cow::Owned(str_p.iter().collect::<_>())); + // c. Else, + } else { + // i. Append "null" to partial. + partial.push(Cow::Borrowed(utf16!("null"))); + } - // 13. Return final. - Ok(r#final) + // 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 "[]". + js_string!("[]") + // 10. Else, + } 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 "]". + let separator = utf16!(","); + let result = once(utf16!("[")) + .chain(Itertools::intersperse( + partial.iter().map(Cow::as_ref), + separator, + )) + .chain(once(utf16!("]"))) + .flatten() + .copied() + .collect::>(); + js_string!(&result[..]) + // b. Else, + } else { + // i. Let separator be the string-concatenation of the code unit 0x002C (COMMA), + // the code unit 0x000A (LINE FEED), and state.[[Indent]]. + let mut separator = utf16!(",\n").to_vec(); + separator.extend(state.indent.iter()); + // 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 "]". + let indent_vec = state.indent.to_vec(); + let stepback_vec = stepback.to_vec(); + let result = [utf16!("[\n"), &indent_vec[..]] + .into_iter() + .chain(Itertools::intersperse( + partial.iter().map(Cow::as_ref), + &separator, + )) + .chain([utf16!("\n"), &stepback_vec[..], utf16!("]")]) + .flatten() + .copied() + .collect::>(); + js_string!(&result[..]) + } }; - let result = inner(); + // 12. Set state.[[Indent]] to stepback. + state.indent = stepback; - // 11. Remove the last element of state.[[Stack]]. - let popped = state - .stack - .pop() - .expect("json stringify stack should not be empty"); - state.stack_set.remove(&popped); + // 11. Remove value from state.[[Stack]]. + let removed = state.stack_set.remove(value); + debug_assert!(removed); - result + // 13. Return final. + Ok(r#final) } } struct StateRecord { replacer_function: Option, - stack: Vec, stack_set: FxHashSet, indent: JsString, gap: JsString,