Skip to main content

nanvm_lib/vm/any/
to_json.rs

1use core::{
2    fmt::{self, Display, Formatter, Write},
3    mem,
4};
5
6use crate::vm::{
7    Any, Array, BigInt, Function, IVm, Number, Object, String, Unpacked, dispatch::Dispatch,
8    nullish::Nullish, string_coercion::number_to_string,
9};
10
11/// An `Any<A>` shape [`Any::to_json`] does not (yet) know how to render.
12///
13/// Scoped deliberately: arrays and objects recurse (see [`Any::to_json`]),
14/// so every remaining gap here bottoms out at a scalar with no direct JSON
15/// counterpart — `mvp-roadmap.md`'s open question 2 ("does the MVP print DJS
16/// for those, or report an error?") is left for later, so this reports an
17/// error rather than guessing at a representation.
18///
19/// `to_json` itself is hand-written Rust standing in for FJS-compiled
20/// logic — see `nanvm/lib/todo/to-json-fjs-migration.md` for the plan to
21/// retire it once `fjs compile` can reach
22/// `fjs/media/json/serializer/module.f.mjs`.
23#[derive(Debug, PartialEq)]
24pub enum JsonError {
25    Undefined,
26    NonFiniteNumber(Number),
27    BigInt,
28    Function,
29}
30
31/// `String(v)` for the three numbers `NonFiniteNumber` can hold — the
32/// spellings `Number::toString` gives them, needing no VM to build.
33fn non_finite_spelling(v: Number) -> &'static str {
34    if v.is_nan() {
35        "NaN"
36    } else if v > 0.into() {
37        "Infinity"
38    } else {
39        "-Infinity"
40    }
41}
42
43impl Display for JsonError {
44    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
45        match self {
46            JsonError::Undefined => write!(f, "`undefined` has no JSON representation"),
47            JsonError::NonFiniteNumber(v) => {
48                write!(f, "{} has no JSON representation", non_finite_spelling(*v))
49            }
50            JsonError::BigInt => write!(f, "a BigInt has no JSON representation"),
51            JsonError::Function => write!(f, "a function has no JSON representation"),
52        }
53    }
54}
55
56/// Renders `v` as a JSON string literal, quotes and escapes included.
57///
58/// Shared by [`ToJson::string`] and object-key serialization
59/// ([`Any::to_json`]): a key is itself a `String<A>` and needs exactly the
60/// same escaping a string *value* does.
61fn json_string<A: IVm>(v: String<A>) -> std::string::String {
62    let mut out = std::string::String::from("\"");
63    for r in char::decode_utf16(v) {
64        match r {
65            Ok('"') => out.push_str("\\\""),
66            Ok('\\') => out.push_str("\\\\"),
67            Ok('\u{8}') => out.push_str("\\b"),
68            Ok('\u{c}') => out.push_str("\\f"),
69            Ok('\n') => out.push_str("\\n"),
70            Ok('\r') => out.push_str("\\r"),
71            Ok('\t') => out.push_str("\\t"),
72            Ok(c) if (c as u32) < 0x20 => {
73                let _ = write!(out, "\\u{:04x}", c as u32);
74            }
75            Ok(c) => out.push(c),
76            // A lone UTF-16 surrogate has no Unicode text representation,
77            // but well-formed JSON stringification (ES2019) still emits
78            // it as its own `\uXXXX` escape rather than refusing the
79            // whole string — the same convention this repository's own
80            // JSON serializer follows
81            // (`fjs/media/json/serializer/module.f.mjs`).
82            Err(e) => {
83                let _ = write!(out, "\\u{:04x}", e.unpaired_surrogate());
84            }
85        }
86    }
87    out.push('"');
88    out
89}
90
91struct ToJson;
92
93impl<A: IVm> Dispatch<A> for ToJson {
94    type Result = Result<std::string::String, JsonError>;
95
96    fn nullish(self, v: Nullish) -> Self::Result {
97        match v {
98            Nullish::Null => Ok("null".into()),
99            Nullish::Undefined => Err(JsonError::Undefined),
100        }
101    }
102
103    fn bool(self, v: bool) -> Self::Result {
104        Ok(if v { "true" } else { "false" }.into())
105    }
106
107    fn number(self, v: Number) -> Self::Result {
108        if !v.is_finite() {
109            return Err(JsonError::NonFiniteNumber(v));
110        }
111        // `number_to_string` (`string_coercion.rs`) is `Number::toString`
112        // proper: correct exponential-notation switching and the `-0` ->
113        // `"0"` case, not Rust's `f64::to_string()` (which never switches to
114        // exponential notation, so e.g. `1e21` would come out as a 22-digit
115        // plain integer instead of `"1e+21"`). `v` is already known finite
116        // above, so the `NaN`/`Infinity` spellings that function also
117        // produces never surface here.
118        Ok(number_to_string::<A>(v).into())
119    }
120
121    fn string(self, v: String<A>) -> Self::Result {
122        Ok(json_string(v))
123    }
124
125    fn bigint(self, _: BigInt<A>) -> Self::Result {
126        Err(JsonError::BigInt)
127    }
128
129    /// Containers are walked by [`Any::to_json`]'s own stack, never here.
130    fn object(self, _: Object<A>) -> Self::Result {
131        unreachable!("an object is walked by `to_json`'s stack")
132    }
133
134    fn array(self, _: Array<A>) -> Self::Result {
135        unreachable!("an array is walked by `to_json`'s stack")
136    }
137
138    fn function(self, _: Function<A>) -> Self::Result {
139        Err(JsonError::Function)
140    }
141}
142
143/// A container being written: what is left of it, and whether a comma is due
144/// before the next item.
145enum Open<A: IVm> {
146    Array(<Array<A> as IntoIterator>::IntoIter, bool),
147    Object(std::vec::IntoIter<(String<A>, Any<A>)>, bool),
148}
149
150impl<A: IVm> Any<A> {
151    /// An `Any<A>` -> JSON serializer covering everything with a direct JSON
152    /// counterpart: numbers, strings, booleans, `null`, and arrays/objects
153    /// recursed into. See [`JsonError`] for what's deliberately unhandled.
154    ///
155    /// The containers being written are a stack on the heap and the text goes
156    /// into one buffer, so traversal does not recurse with nesting and does not
157    /// copy each container's text into its parent's buffer.
158    ///
159    /// Destruction still depends on the VM. With `Naive`, consuming a sole-owned
160    /// deeply nested value can overflow the stack when its containers are
161    /// released, including on an error. See `nanvm/lib/todo/array-deep-nesting.md`.
162    ///
163    /// An object's entries are its own, in `[[OwnPropertyKeys]]` order, each
164    /// key once with its last value: the view `JSON.stringify` and an object
165    /// spread both read ([`Object::own_entries`]).
166    pub fn to_json(self) -> Result<std::string::String, JsonError> {
167        let mut out = std::string::String::new();
168        let mut stack: Vec<Open<A>> = Vec::new();
169        let mut value = Some(self);
170        loop {
171            if let Some(v) = value.take() {
172                match Unpacked::from(v) {
173                    Unpacked::Array(a) => {
174                        out.push('[');
175                        stack.push(Open::Array(a.into_iter(), false));
176                    }
177                    Unpacked::Object(o) => {
178                        out.push('{');
179                        stack.push(Open::Object(o.own_entries().into_iter(), false));
180                    }
181                    scalar => out.push_str(&Any::from(scalar).dispatch(ToJson)?),
182                }
183            }
184            match stack.last_mut() {
185                None => return Ok(out),
186                Some(Open::Array(items, comma)) => match items.next() {
187                    Some(item) => {
188                        if mem::replace(comma, true) {
189                            out.push(',');
190                        }
191                        value = Some(item);
192                    }
193                    None => {
194                        out.push(']');
195                        stack.pop();
196                    }
197                },
198                Some(Open::Object(entries, comma)) => match entries.next() {
199                    Some((key, item)) => {
200                        if mem::replace(comma, true) {
201                            out.push(',');
202                        }
203                        out.push_str(&json_string(key));
204                        out.push(':');
205                        value = Some(item);
206                    }
207                    None => {
208                        out.push('}');
209                        stack.pop();
210                    }
211                },
212            }
213        }
214    }
215}
216
217#[cfg(test)]
218mod tests {
219    use crate::{
220        naive::Naive,
221        vm::{Any, Function, IVm, Nullish, String, ToAny, ToArray, ToObject, ToString},
222    };
223
224    type A = Naive;
225
226    fn s(v: &str) -> Any<A> {
227        String::<A>::from(v).to_any()
228    }
229
230    #[test]
231    fn number() {
232        assert_eq!(42.0.to_any::<A>().to_json(), Ok("42".into()));
233        assert_eq!((-1.5).to_any::<A>().to_json(), Ok("-1.5".into()));
234    }
235
236    #[test]
237    fn negative_zero_renders_as_zero() {
238        // ECMAScript's `Number::toString(-0)` (and so `JSON.stringify(-0)`)
239        // is `"0"`, not `"-0"` — handled by `number_to_string`.
240        assert_eq!((-0.0).to_any::<A>().to_json(), Ok("0".into()));
241    }
242
243    #[test]
244    fn large_and_small_magnitudes_use_exponential_notation() {
245        // `f64::to_string()` never switches to exponential notation, but
246        // `JSON.stringify` (via `Number::toString`) does; `number_to_string`
247        // (`string_coercion.rs`) gets this right, so `to_json` inherits it.
248        assert_eq!(1e21.to_any::<A>().to_json(), Ok("1e+21".into()));
249        assert_eq!(1e-7.to_any::<A>().to_json(), Ok("1e-7".into()));
250    }
251
252    #[test]
253    fn non_finite_number_errors() {
254        // `NaN != NaN`, so this checks the variant by pattern rather than
255        // `assert_eq!` against a `JsonError::NonFiniteNumber(Number::NAN)`.
256        assert!(matches!(
257            f64::NAN.to_any::<A>().to_json(),
258            Err(super::JsonError::NonFiniteNumber(v)) if v.is_nan()
259        ));
260        assert_eq!(
261            f64::INFINITY.to_any::<A>().to_json(),
262            Err(super::JsonError::NonFiniteNumber(f64::INFINITY.into()))
263        );
264    }
265
266    /// The error spells its number as JavaScript's `String(v)` would.
267    #[test]
268    fn non_finite_number_error_message() {
269        let message = |v: f64| format!("{}", v.to_any::<A>().to_json().unwrap_err());
270        assert_eq!(message(f64::NAN), "NaN has no JSON representation");
271        assert_eq!(
272            message(f64::INFINITY),
273            "Infinity has no JSON representation"
274        );
275        assert_eq!(
276            message(f64::NEG_INFINITY),
277            "-Infinity has no JSON representation"
278        );
279    }
280
281    #[test]
282    fn string() {
283        assert_eq!(s("hello").to_json(), Ok(r#""hello""#.into()));
284        assert_eq!(s("a\"b\\c\nd").to_json(), Ok(r#""a\"b\\c\nd""#.into()));
285    }
286
287    #[test]
288    fn backspace_and_form_feed_use_short_escapes() {
289        // `JSON.stringify` spells U+0008/U+000C as the two-character `\b`/
290        // `\f`, not the generic four-hex-digit `\u0008`/`\u000c` form.
291        assert_eq!(s("\u{8}").to_json(), Ok(r#""\b""#.into()));
292        assert_eq!(s("\u{c}").to_json(), Ok(r#""\f""#.into()));
293    }
294
295    #[test]
296    fn unpaired_surrogate_is_escaped_not_an_error() {
297        let lone_high_surrogate: String<A> = [0xd800u16].to_string();
298        assert_eq!(
299            lone_high_surrogate.to_any::<A>().to_json(),
300            Ok(r#""\ud800""#.into())
301        );
302    }
303
304    #[test]
305    fn bigint_errors() {
306        let bi: crate::vm::BigInt<A> = 123u64.into();
307        assert_eq!(bi.to_any::<A>().to_json(), Err(super::JsonError::BigInt));
308    }
309
310    #[test]
311    fn empty_array() {
312        let a = [].to_array::<A>();
313        assert_eq!(a.to_any::<A>().to_json(), Ok("[]".into()));
314    }
315
316    #[test]
317    fn array_of_scalars() {
318        let a: crate::vm::Array<A> = [1.0.to_any(), true.to_any(), s("x")].to_array();
319        assert_eq!(a.to_any::<A>().to_json(), Ok(r#"[1,true,"x"]"#.into()));
320    }
321
322    #[test]
323    fn nested_arrays() {
324        let inner: crate::vm::Array<A> = [1.0.to_any()].to_array();
325        let outer: crate::vm::Array<A> = [inner.to_any(), 2.0.to_any()].to_array();
326        assert_eq!(outer.to_any::<A>().to_json(), Ok("[[1],2]".into()));
327    }
328
329    #[test]
330    fn array_element_error_propagates_as_the_scalar_it_is() {
331        // The element that can't serialize is what's reported, never a
332        // generic "array" refusal — arrays themselves can no longer be the
333        // *reason* a serialization fails now that they recurse.
334        let a: crate::vm::Array<A> = [1.0.to_any(), Any::undefined()].to_array();
335        assert_eq!(a.to_any::<A>().to_json(), Err(super::JsonError::Undefined));
336    }
337
338    #[test]
339    fn empty_object() {
340        let o = [].to_object::<A>();
341        assert_eq!(o.to_any::<A>().to_json(), Ok("{}".into()));
342    }
343
344    #[test]
345    fn object_of_scalars() {
346        let o: crate::vm::Object<A> =
347            [("a".into(), 1.0.to_any()), ("b".into(), s("two"))].to_object();
348        assert_eq!(o.to_any::<A>().to_json(), Ok(r#"{"a":1,"b":"two"}"#.into()));
349    }
350
351    #[test]
352    fn nested_objects() {
353        let inner: crate::vm::Object<A> = [("x".into(), 1.0.to_any())].to_object();
354        let outer: crate::vm::Object<A> = [("a".into(), inner.to_any())].to_object();
355        assert_eq!(outer.to_any::<A>().to_json(), Ok(r#"{"a":{"x":1}}"#.into()));
356    }
357
358    #[test]
359    fn duplicate_key_keeps_last_value_at_first_position() {
360        // `JSON.stringify({ a: 1, b: 2, a: 3 })` is `'{"a":3,"b":2}'`: the
361        // later `a` wins the value, but the member list still shows `a`
362        // first, since that's where it was first declared.
363        let o: crate::vm::Object<A> = [
364            ("a".into(), 1.0.to_any()),
365            ("b".into(), 2.0.to_any()),
366            ("a".into(), 3.0.to_any()),
367        ]
368        .to_object();
369        assert_eq!(o.to_any::<A>().to_json(), Ok(r#"{"a":3,"b":2}"#.into()));
370    }
371
372    #[test]
373    fn object_member_error_propagates_as_the_scalar_it_is() {
374        let o: crate::vm::Object<A> = [("a".into(), Any::undefined())].to_object();
375        assert_eq!(o.to_any::<A>().to_json(), Err(super::JsonError::Undefined));
376    }
377
378    #[test]
379    fn integer_like_keys_sort_ascending_ahead_of_other_keys() {
380        // `JSON.stringify({ a: 0, 2: 2, 1: 1 })` is `'{"1":1,"2":2,"a":0}'`:
381        // array-index keys first, ascending, regardless of where they were
382        // written; `a` keeps its insertion position among the rest.
383        let o: crate::vm::Object<A> = [
384            ("a".into(), 0.0.to_any()),
385            ("2".into(), 2.0.to_any()),
386            ("1".into(), 1.0.to_any()),
387        ]
388        .to_object();
389        assert_eq!(
390            o.to_any::<A>().to_json(),
391            Ok(r#"{"1":1,"2":2,"a":0}"#.into())
392        );
393    }
394
395    #[test]
396    fn integer_like_keys_sort_numerically_not_lexicographically() {
397        // `"10"` sorts after `"2"` by value, the opposite of string order.
398        let o: crate::vm::Object<A> = [("10".into(), s("b")), ("2".into(), s("a"))].to_object();
399        assert_eq!(
400            o.to_any::<A>().to_json(),
401            Ok(r#"{"2":"a","10":"b"}"#.into())
402        );
403    }
404
405    #[test]
406    fn zero_key_sorts_first() {
407        let o: crate::vm::Object<A> =
408            [("b".into(), 1.0.to_any()), ("0".into(), 2.0.to_any())].to_object();
409        assert_eq!(o.to_any::<A>().to_json(), Ok(r#"{"0":2,"b":1}"#.into()));
410    }
411
412    #[test]
413    fn duplicate_integer_like_key_keeps_last_value_sorted_by_number() {
414        // Dedup (last-write-wins) happens before the array-index sort, so a
415        // repeated key's *value* is the last one written, but its *position*
416        // among the other array-index keys is by number, not by either
417        // occurrence's insertion order.
418        let o: crate::vm::Object<A> = [
419            ("1".into(), s("a")),
420            ("0".into(), s("x")),
421            ("1".into(), s("b")),
422        ]
423        .to_object();
424        assert_eq!(o.to_any::<A>().to_json(), Ok(r#"{"0":"x","1":"b"}"#.into()));
425    }
426
427    #[test]
428    fn leading_zero_key_is_not_integer_like() {
429        // `"01"`'s canonical form is `"1"`, not itself — `ToUint32("01")` is
430        // `1`, but `ToString(1)` is `"1"` ≠ `"01"`, so `JSON.stringify`
431        // treats it as an ordinary insertion-order key, not an array index.
432        let o: crate::vm::Object<A> = [("01".into(), s("a"))].to_object();
433        assert_eq!(o.to_any::<A>().to_json(), Ok(r#"{"01":"a"}"#.into()));
434    }
435
436    #[test]
437    fn max_uint32_key_is_not_integer_like() {
438        // `2^32 - 1` (`4294967295`) is the one `ToUint32` value ECMAScript's
439        // own array-index definition excludes.
440        let o: crate::vm::Object<A> = [("4294967295".into(), s("a"))].to_object();
441        assert_eq!(
442            o.to_any::<A>().to_json(),
443            Ok(r#"{"4294967295":"a"}"#.into())
444        );
445    }
446
447    #[test]
448    fn overflowing_digit_string_key_is_not_integer_like() {
449        let o: crate::vm::Object<A> = [("99999999999".into(), s("a"))].to_object();
450        assert_eq!(
451            o.to_any::<A>().to_json(),
452            Ok(r#"{"99999999999":"a"}"#.into())
453        );
454    }
455
456    #[test]
457    fn integer_like_key_nested_in_an_array_serializes() {
458        let o: crate::vm::Object<A> = [("0".into(), 1.0.to_any())].to_object();
459        let a: crate::vm::Array<A> = [o.to_any()].to_array();
460        assert_eq!(a.to_any::<A>().to_json(), Ok(r#"[{"0":1}]"#.into()));
461    }
462
463    #[test]
464    fn function_errors() {
465        let f: Function<A> =
466            A::static_function(|_, _| Ok(Any::undefined()), 0, [].to_array(), None);
467        assert_eq!(f.to_any::<A>().to_json(), Err(super::JsonError::Function));
468    }
469
470    #[test]
471    fn boolean() {
472        assert_eq!(true.to_any::<A>().to_json(), Ok("true".into()));
473        assert_eq!(false.to_any::<A>().to_json(), Ok("false".into()));
474    }
475
476    #[test]
477    fn null_and_undefined() {
478        assert_eq!(Nullish::Null.to_any::<A>().to_json(), Ok("null".into()));
479        assert_eq!(
480            Any::<A>::undefined().to_json(),
481            Err(super::JsonError::Undefined)
482        );
483    }
484
485    /// A value nested far past the stack is written, in one buffer: a text as
486    /// long as the nesting is deep, which a recursion that copied each level's
487    /// text into its parent's would take quadratic time to write.
488    #[test]
489    fn writes_a_deeply_nested_value() {
490        use crate::vm::test::deep::{DEPTH, leak, nested_arrays, nested_objects, small_stack};
491        small_stack(|| {
492            let arrays = nested_arrays(DEPTH, Nullish::Null.to_any());
493            leak(arrays.clone());
494            assert_eq!(
495                arrays.clone().to_json(),
496                Ok(format!("{}null{}", "[".repeat(DEPTH), "]".repeat(DEPTH)))
497            );
498            let objects = nested_objects(DEPTH, Nullish::Null.to_any());
499            leak(objects.clone());
500            assert_eq!(
501                objects.clone().to_json(),
502                Ok(format!(
503                    "{}null{}",
504                    r#"{"a":"#.repeat(DEPTH),
505                    "}".repeat(DEPTH)
506                ))
507            );
508        });
509    }
510
511    /// A refusal deep inside is the first one in document order, as before.
512    #[test]
513    fn a_deep_refusal_is_reported() {
514        use crate::vm::test::deep::{DEPTH, leak, nested_arrays, small_stack};
515        small_stack(|| {
516            let a = nested_arrays(DEPTH, Any::undefined());
517            leak(a.clone());
518            assert_eq!(a.clone().to_json(), Err(super::JsonError::Undefined));
519        });
520    }
521
522    /// Commas fall between siblings only, whatever the nesting.
523    #[test]
524    fn commas_between_siblings_in_nested_containers() {
525        let inner: crate::vm::Any<A> = [s("x"), s("y")].to_array().to_any();
526        let o: crate::vm::Any<A> = [
527            ("a".into(), inner.clone()),
528            ("b".into(), [].to_array().to_any()),
529            ("c".into(), [inner, s("z")].to_array().to_any()),
530        ]
531        .to_object()
532        .to_any();
533        assert_eq!(
534            o.to_json(),
535            Ok(r#"{"a":["x","y"],"b":[],"c":[["x","y"],"z"]}"#.into())
536        );
537    }
538}