Skip to main content

hermes_support/
json_emitter.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8//! Port of `hermes::JSONEmitter` (include/hermes/Support/JSONEmitter.{h,cpp})
9//! plus the `numberToString` helper it relies on (lib/Support/Conversions.cpp).
10
11use std::fmt::Write;
12
13/// Port of `hermes::numberToString` (lib/Support/Conversions.cpp:211) — the
14/// ECMAScript Number::toString algorithm. Produces the shortest round-tripping
15/// decimal. The C++ obtains the shortest (significand, exponent) via
16/// `fastDoubleToDecimal`; Rust's `{:e}` formatting yields the same unique
17/// shortest digits, so we extract digits + exponent from it and then apply the
18/// identical fixed/scientific formatting rules (spec steps 6-12).
19pub fn number_to_string(m: f64) -> String {
20    // 1. NaN.
21    if m.is_nan() {
22        return "NaN".to_string();
23    }
24    // 2. +0 or -0 -> "0".
25    if m == 0.0 {
26        return "0".to_string();
27    }
28    // 4. +/- Infinity.
29    if m == f64::INFINITY {
30        return "Infinity".to_string();
31    }
32    if m == f64::NEG_INFINITY {
33        return "-Infinity".to_string();
34    }
35
36    let mut out = String::new();
37    // 3. Negative: prepend '-' and operate on the absolute value.
38    if m < 0.0 {
39        out.push('-');
40    }
41
42    // Shortest significand digits + decimal exponent from `{:e}`:
43    //   456.7 -> "4.567e2", 1.0 -> "1e0", 1e21 -> "1e21".
44    // Rust strips trailing zeros, matching `fastDoubleToDecimal` (significand
45    // not divisible by 10).
46    let sci = format!("{:e}", m.abs());
47    let (mantissa, exp_str) = sci.split_once('e').expect("`{:e}` always has 'e'");
48    let e: i32 = exp_str.parse().expect("valid exponent");
49    let digits: Vec<u8> = mantissa.bytes().filter(|&b| b != b'.').collect();
50    let k = digits.len() as i32;
51    // value = digits_int * 10^(e-(k-1)); decimal-point position n = e + 1.
52    let n = e + 1;
53
54    if (-5..=21).contains(&n) {
55        if n >= k {
56            // k digits, then n-k zeros.
57            for &d in &digits {
58                out.push(d as char);
59            }
60            for _ in 0..(n - k) {
61                out.push('0');
62            }
63        } else if n > 0 {
64            // n digits, '.', remaining k-n digits.
65            for i in 0..n {
66                out.push(digits[i as usize] as char);
67            }
68            out.push('.');
69            for i in n..k {
70                out.push(digits[i as usize] as char);
71            }
72        } else {
73            // "0.", -n zeros, then k digits.
74            out.push('0');
75            out.push('.');
76            for _ in 0..(-n) {
77                out.push('0');
78            }
79            for &d in &digits {
80                out.push(d as char);
81            }
82        }
83    } else {
84        // Scientific notation, e.g. 1.2e+3.
85        let exponent_sign = if n < 0 { '-' } else { '+' };
86        let exp_val = (n - 1).unsigned_abs();
87        out.push(digits[0] as char);
88        if k != 1 {
89            out.push('.');
90            for i in 1..k {
91                out.push(digits[i as usize] as char);
92            }
93        }
94        out.push('e');
95        out.push(exponent_sign);
96        out.push_str(&exp_val.to_string());
97    }
98    out
99}
100
101/// A single object (Dictionary or Array) being emitted.
102/// Port of `JSONEmitter::State` (JSONEmitter.h:170).
103#[derive(Clone, Copy, PartialEq, Eq, Debug)]
104enum StateType {
105    Dict,
106    Array,
107}
108
109#[derive(Debug)]
110struct State {
111    /// Whether this is a dictionary or array.
112    ty: StateType,
113    /// Whether a comma is needed before the next value.
114    needs_comma: bool,
115    /// Whether we are a dictionary expecting a key next.
116    needs_key: bool,
117    /// Whether we expect a value (after a key in a dict).
118    needs_value: bool,
119    /// Whether the dict/array is still empty.
120    is_empty: bool,
121}
122
123impl State {
124    fn new(ty: StateType) -> State {
125        State {
126            ty,
127            needs_comma: false,
128            needs_key: ty == StateType::Dict,
129            needs_value: false,
130            is_empty: true,
131        }
132    }
133}
134
135/// Port of `hermes::JSONEmitter` (include/hermes/Support/JSONEmitter.h). Emits
136/// JSON to a `String`. `pretty` adds newlines + indentation. Unbalanced
137/// dict/array use is caught via `debug_assert!` (C++ `assert`).
138///
139/// Strings must be valid UTF-8 (C++ takes `StringRef` and treats invalid UTF-8
140/// as fatal). Non-ASCII code points are emitted as escaped UTF-16 code units.
141pub struct JSONEmitter<'w> {
142    out: &'w mut String,
143    pretty: bool,
144    indent: u32,
145    states: Vec<State>,
146}
147
148impl<'w> JSONEmitter<'w> {
149    pub fn new(out: &'w mut String, pretty: bool) -> JSONEmitter<'w> {
150        JSONEmitter { out, pretty, indent: 0, states: Vec::new() }
151    }
152
153    fn in_dict(&self) -> bool {
154        matches!(self.states.last(), Some(s) if s.ty == StateType::Dict)
155    }
156    fn in_array(&self) -> bool {
157        matches!(self.states.last(), Some(s) if s.ty == StateType::Array)
158    }
159
160    /// JSONEmitter.cpp:239 — housekeeping before emitting any value (not a key).
161    fn will_emit_value(&mut self) {
162        if self.states.is_empty() {
163            return;
164        }
165        let is_array;
166        {
167            let state = self.states.last_mut().unwrap();
168            debug_assert!(!state.needs_key, "Expected a key");
169            if state.needs_comma {
170                self.out.push(',');
171            }
172            state.needs_key = state.ty == StateType::Dict;
173            state.needs_comma = true;
174            state.needs_value = false;
175            state.is_empty = false;
176            is_array = state.ty == StateType::Array;
177        }
178        if is_array {
179            self.pretty_new_line();
180        }
181    }
182
183    pub fn emit_bool(&mut self, val: bool) {
184        self.will_emit_value();
185        self.out.push_str(if val { "true" } else { "false" });
186    }
187
188    /// Covers the C++ integer overloads (short/int/long/...).
189    pub fn emit_i64(&mut self, val: i64) {
190        self.will_emit_value();
191        let _ = write!(self.out, "{val}");
192    }
193    pub fn emit_u64(&mut self, val: u64) {
194        self.will_emit_value();
195        let _ = write!(self.out, "{val}");
196    }
197
198    /// JSONEmitter.cpp:67 — finite via numberToString, non-finite -> "null".
199    pub fn emit_f64(&mut self, val: f64) {
200        self.will_emit_value();
201        if val.is_finite() {
202            self.out.push_str(&number_to_string(val));
203        } else {
204            self.out.push_str("null");
205        }
206    }
207
208    /// JSONEmitter.cpp:78 — emit a UTF-8 string value (not a dict key).
209    pub fn emit_str(&mut self, val: &str) {
210        self.will_emit_value();
211        self.primitive_emit_string(val);
212    }
213
214    /// JSONEmitter.cpp:193 — emit a value from UTF-16 code units. Each unit is
215    /// emitted independently (no surrogate combination).
216    pub fn emit_u16(&mut self, val: &[u16]) {
217        self.will_emit_value();
218        self.out.push('"');
219        for &curr in val {
220            self.emit_one_escaped_unit(curr);
221        }
222        self.out.push('"');
223    }
224
225    pub fn emit_null_value(&mut self) {
226        self.will_emit_value();
227        self.out.push_str("null");
228    }
229
230    /// Like `emit_key` but takes UTF-16 code units (for WTF-8 keys that are not
231    /// valid UTF-8). Port-compatible with C++ `emitKey` -> `primitiveEmitString`
232    /// which decodes WTF-8 and escapes per code unit.
233    pub fn emit_key_u16(&mut self, key: &[u16]) {
234        debug_assert!(self.in_dict(), "Not emitting a dictionary");
235        {
236            let state = self.states.last_mut().unwrap();
237            debug_assert!(state.needs_key, "Not expecting a key");
238            debug_assert!(!state.needs_value, "Missing a value for a key.");
239            if state.needs_comma {
240                self.out.push(',');
241            }
242            state.needs_comma = false;
243            state.needs_key = false;
244            state.needs_value = true;
245        }
246        self.pretty_new_line();
247        self.out.push('"');
248        for &unit in key {
249            self.emit_one_escaped_unit(unit);
250        }
251        self.out.push('"');
252        self.out.push(':');
253        if self.pretty {
254            self.out.push(' ');
255        }
256    }
257
258    /// JSONEmitter.cpp:88 — emit a dict key.
259    pub fn emit_key(&mut self, key: &str) {
260        debug_assert!(self.in_dict(), "Not emitting a dictionary");
261        {
262            let state = self.states.last_mut().unwrap();
263            debug_assert!(state.needs_key, "Not expecting a key");
264            debug_assert!(!state.needs_value, "Missing a value for a key.");
265            if state.needs_comma {
266                self.out.push(',');
267            }
268            state.needs_comma = false;
269            state.needs_key = false;
270            state.needs_value = true;
271        }
272        // Note: the state fields are set before pretty_new_line() (which reads only
273        // `pretty`/`indent`, not `states`); this reordering vs the C++ is output-identical.
274        self.pretty_new_line();
275        self.primitive_emit_string(key);
276        self.out.push(':');
277        if self.pretty {
278            self.out.push(' ');
279        }
280    }
281
282    pub fn open_dict(&mut self) {
283        self.will_emit_value();
284        self.out.push('{');
285        self.indent_more();
286        self.states.push(State::new(StateType::Dict));
287    }
288    pub fn close_dict(&mut self) {
289        debug_assert!(self.in_dict(), "Not currently emitting a dictionary");
290        debug_assert!(!self.states.last().unwrap().needs_value, "Missing a value for a key.");
291        self.indent_less();
292        if !self.states.last().unwrap().is_empty {
293            self.pretty_new_line();
294        }
295        self.out.push('}');
296        self.states.pop();
297    }
298    pub fn open_array(&mut self) {
299        self.will_emit_value();
300        self.indent_more();
301        self.out.push('[');
302        self.states.push(State::new(StateType::Array));
303    }
304    pub fn close_array(&mut self) {
305        debug_assert!(self.in_array(), "Not currently emitting an array");
306        self.indent_less();
307        if !self.states.last().unwrap().is_empty {
308            self.pretty_new_line();
309        }
310        self.out.push(']');
311        self.states.pop();
312    }
313
314    /// JSONEmitter.cpp:234 — terminate a JSON Lines record.
315    pub fn end_jsonl(&mut self) {
316        debug_assert!(self.states.is_empty(), "Previous object was not terminated.");
317        self.out.push('\n');
318    }
319
320    /// JSONEmitter.cpp:141 — escape + emit a UTF-8 string (key or value).
321    fn primitive_emit_string(&mut self, s: &str) {
322        self.out.push('"');
323        for ch in s.chars() {
324            let cp = ch as u32;
325            if cp > 0x7F {
326                // encodeUTF16(cp) -> 1 or 2 units, each as \uXXXX.
327                if cp <= 0xFFFF {
328                    self.write_u_escape(cp as u16);
329                } else {
330                    let c = cp - 0x10000;
331                    self.write_u_escape(0xD800 + (c >> 10) as u16);
332                    self.write_u_escape(0xDC00 + (c & 0x3FF) as u16);
333                }
334                continue;
335            }
336            if cp == 0x22 || cp == 0x5C || cp == 0x2F {
337                // escape " \ /
338                self.out.push('\\');
339            }
340            if cp >= 0x20 {
341                self.out.push(cp as u8 as char);
342                continue;
343            }
344            match cp {
345                0x08 => self.out.push_str("\\b"),
346                0x0C => self.out.push_str("\\f"),
347                0x0A => self.out.push_str("\\n"),
348                0x0D => self.out.push_str("\\r"),
349                0x09 => self.out.push_str("\\t"),
350                _ => self.write_u_escape(cp as u16),
351            }
352        }
353        self.out.push('"');
354    }
355
356    /// One UTF-16 code unit, escaped per JSONEmitter.cpp:193 (the char16 path).
357    fn emit_one_escaped_unit(&mut self, curr: u16) {
358        let c = curr as u32;
359        if c > 0x7F {
360            self.write_u_escape(curr);
361            return;
362        }
363        if c >= 0x20 {
364            if c == 0x22 || c == 0x5C || c == 0x2F {
365                self.out.push('\\');
366            }
367            self.out.push(c as u8 as char);
368            return;
369        }
370        match c {
371            0x08 => self.out.push_str("\\b"),
372            0x0C => self.out.push_str("\\f"),
373            0x0A => self.out.push_str("\\n"),
374            0x0D => self.out.push_str("\\r"),
375            0x09 => self.out.push_str("\\t"),
376            _ => self.write_u_escape(curr),
377        }
378    }
379
380    fn write_u_escape(&mut self, u: u16) {
381        let _ = write!(self.out, "\\u{u:04x}");
382    }
383
384    fn pretty_new_line(&mut self) {
385        if !self.pretty {
386            return;
387        }
388        self.out.push('\n');
389        for _ in 0..self.indent {
390            self.out.push(' ');
391        }
392    }
393    fn indent_more(&mut self) {
394        if self.pretty {
395            self.indent += 2;
396        }
397    }
398    fn indent_less(&mut self) {
399        if self.pretty {
400            debug_assert!(self.indent >= 2, "Unbalanced indentation.");
401            self.indent -= 2;
402        }
403    }
404}
405
406#[cfg(test)]
407mod tests {
408    use super::*;
409
410    fn emit<F: FnOnce(&mut JSONEmitter)>(f: F) -> String {
411        let mut s = String::new();
412        {
413            let mut j = JSONEmitter::new(&mut s, false);
414            f(&mut j);
415        }
416        s
417    }
418
419    #[test]
420    fn empty_array() {
421        assert_eq!(emit(|j| { j.open_array(); j.close_array(); }), "[]");
422    }
423
424    #[test]
425    fn empty_dict() {
426        assert_eq!(emit(|j| { j.open_dict(); j.close_dict(); }), "{}");
427    }
428
429    #[test]
430    fn sample() {
431        // unittests/Support/JSONEmitterTest.cpp: Sample
432        let s = emit(|j| {
433            j.open_dict();
434            j.emit_key("name"); j.emit_str("hermes");
435            j.emit_key("age"); j.emit_i64(2);
436            j.emit_key("hot"); j.emit_bool(true);
437            j.emit_key("cold"); j.emit_bool(false);
438            j.emit_key("tags");
439            j.open_array();
440            j.emit_str("small"); j.emit_str("light");
441            j.close_array();
442            j.close_dict();
443        });
444        assert_eq!(s, r#"{"name":"hermes","age":2,"hot":true,"cold":false,"tags":["small","light"]}"#);
445    }
446
447    #[test]
448    fn smoke_with_double_and_escapes() {
449        // unittests/Support/JSONEmitterTest.cpp: SmokeTest
450        let s = emit(|j| {
451            j.open_dict();
452            j.emit_key("a"); j.emit_i64(123);
453            j.emit_key("b"); j.emit_f64(456.7);
454            j.emit_key("dict1");
455            j.open_dict();
456            j.emit_key("dict1_arr1");
457            j.open_array();
458            j.emit_str("val1"); j.emit_str("val2"); j.emit_str("val3");
459            j.close_array();
460            j.emit_key("dict1_empty"); j.open_dict(); j.close_dict();
461            j.emit_key("dict1_empty2"); j.open_array(); j.close_array();
462            j.emit_key("str1"); j.emit_str("\"ABC\u{8}DEF\\");
463            j.close_dict();
464            j.close_dict();
465        });
466        assert_eq!(s, r#"{"a":123,"b":456.7,"dict1":{"dict1_arr1":["val1","val2","val3"],"dict1_empty":{},"dict1_empty2":[],"str1":"\"ABC\bDEF\\"}}"#);
467    }
468
469    #[test]
470    fn escapes() {
471        // unittests/Support/JSONEmitterTest.cpp: Escapes
472        let s = emit(|j| j.emit_str("x\"\\/\u{8}\u{c}\n\r\tx"));
473        assert_eq!(s, r#""x\"\\\/\b\f\n\r\tx""#);
474    }
475
476    #[test]
477    fn forward_slashes() {
478        // EmitGroupsOfForwardSlashes
479        let s = emit(|j| {
480            j.open_dict();
481            j.emit_key("url"); j.emit_str("http://www.example.com");
482            j.close_dict();
483        });
484        assert_eq!(s, r#"{"url":"http:\/\/www.example.com"}"#);
485    }
486
487    #[test]
488    fn non_ascii_and_astral() {
489        // NonAsciiEscapes + EmitUTF8
490        let s = emit(|j| {
491            j.open_dict();
492            j.emit_key("ha"); j.emit_str("\u{54C8}");
493            j.emit_key("gClef"); j.emit_str("\u{1D11E}");
494            j.emit_key("wave"); j.emit_str("hi\u{1F44B}");
495            j.close_dict();
496        });
497        assert_eq!(s, r#"{"ha":"\u54c8","gClef":"\ud834\udd1e","wave":"hi\ud83d\udc4b"}"#);
498    }
499
500    #[test]
501    fn non_finite_is_null() {
502        // NonFinite — the emitter (not number_to_string) maps non-finite to null.
503        let s = emit(|j| {
504            j.open_array();
505            j.emit_f64(f64::INFINITY); j.emit_f64(f64::NEG_INFINITY); j.emit_f64(f64::NAN);
506            j.close_array();
507        });
508        assert_eq!(s, "[null,null,null]");
509    }
510
511    #[test]
512    fn null_value() {
513        assert_eq!(emit(|j| j.emit_null_value()), "null");
514    }
515
516    #[test]
517    fn jsonl() {
518        let mut s = String::new();
519        {
520            let mut j = JSONEmitter::new(&mut s, false);
521            j.open_dict(); j.close_dict(); j.end_jsonl();
522            j.open_dict(); j.close_dict(); j.end_jsonl();
523        }
524        assert_eq!(s, "{}\n{}\n");
525    }
526
527    #[test]
528    fn emit_utf16() {
529        // EmitUTF16: u"hi\xd83d\xdc4b" -> each surrogate unit escaped as \uXXXX
530        let units: Vec<u16> = vec![b'h' as u16, b'i' as u16, 0xd83d, 0xdc4b];
531        let mut s = String::new();
532        {
533            let mut j = JSONEmitter::new(&mut s, false);
534            j.open_dict();
535            j.emit_key("str"); j.emit_u16(&units);
536            j.close_dict();
537        }
538        assert_eq!(s, r#"{"str":"hi\ud83d\udc4b"}"#);
539    }
540
541    #[test]
542    fn pretty_print() {
543        // unittests/Support/JSONEmitterTest.cpp: PrettyPrint
544        let mut s = String::new();
545        {
546            let mut j = JSONEmitter::new(&mut s, true);
547            j.open_dict();
548            j.emit_key("artist"); j.emit_str("prince");
549            j.emit_key("instruments");
550            j.open_array();
551            j.emit_str("piano");
552            j.open_dict();
553            j.emit_key("guitars");
554            j.open_array();
555            j.emit_str("cloud"); j.emit_str("love symbol"); j.emit_str("telecaster");
556            j.close_array();
557            j.close_dict();
558            j.emit_str("drums");
559            j.close_array();
560            j.emit_key("songs");
561            j.open_dict();
562            j.emit_key("purple rain"); j.emit_i64(1984);
563            j.emit_key("1999"); j.emit_i64(1982);
564            j.close_dict();
565            j.emit_key("color"); j.emit_str("purple");
566            j.emit_key("emptyDict"); j.open_dict(); j.close_dict();
567            j.emit_key("emptyArray"); j.open_array(); j.close_array();
568            j.close_dict();
569        }
570        let expected = "{\n  \"artist\": \"prince\",\n  \"instruments\": [\n    \"piano\",\n    {\n      \"guitars\": [\n        \"cloud\",\n        \"love symbol\",\n        \"telecaster\"\n      ]\n    },\n    \"drums\"\n  ],\n  \"songs\": {\n    \"purple rain\": 1984,\n    \"1999\": 1982\n  },\n  \"color\": \"purple\",\n  \"emptyDict\": {},\n  \"emptyArray\": []\n}";
571        assert_eq!(s, expected);
572    }
573
574    #[test]
575    fn emit_u16_astral_and_lone_surrogate() {
576        // astral U+10000 encoded as surrogate pair [0xD800,0xDC00]: each unit
577        // is > 0x7F so emit_one_escaped_unit emits each as \uXXXX ->
578        // key "\ud800\udc00"; lone surrogate 0xD800 value -> "\ud800".
579        let mut s = String::new();
580        {
581            let mut j = JSONEmitter::new(&mut s, false);
582            j.open_dict();
583            j.emit_key_u16(&[0xD800, 0xDC00]); // key = surrogate pair for U+10000
584            j.emit_u16(&[0xD800]);             // lone surrogate value
585            j.close_dict();
586        }
587        assert_eq!(s, "{\"\\ud800\\udc00\":\"\\ud800\"}");
588    }
589
590    #[test]
591    fn number_to_string_matches_ecmascript() {
592        // Port-of-numberToString spot checks (lib/Support/Conversions.cpp:211).
593        let cases: &[(f64, &str)] = &[
594            (0.0, "0"),
595            (-0.0, "0"),
596            (1.0, "1"),
597            (-1.0, "-1"),
598            (456.7, "456.7"),
599            (100.0, "100"),
600            (0.1, "0.1"),
601            (0.0001, "0.0001"),     // n=-3, fixed
602            (1e-6, "0.000001"),     // n=-5, fixed boundary
603            (1e-7, "1e-7"),         // n=-6, scientific
604            (1e20, "100000000000000000000"), // n=21, fixed
605            (1e21, "1e+21"),        // n=22, scientific
606            (123.45, "123.45"),
607            (5e-324, "5e-324"),     // min subnormal
608            (f64::NAN, "NaN"),
609            (f64::INFINITY, "Infinity"),
610            (f64::NEG_INFINITY, "-Infinity"),
611        ];
612        for &(v, expected) in cases {
613            assert_eq!(number_to_string(v), expected, "for {v:?}");
614        }
615    }
616}