Skip to main content

hermes_parser/json/
mod.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//! Faithful Rust port of Hermes' JSONParser (include/hermes/Parser/JSONParser.h,
9//! lib/Parser/JSONParser.cpp): the JSON value model, the uniquing/hidden-class
10//! `JSONFactory`, and the recursive-descent `JSONParser` over `JSLexer`.
11
12pub mod factory;
13pub mod parser;
14
15pub use factory::{JSONFactory, Prop};
16pub use parser::JSONParser;
17
18use hermes_atom_table::AtomBytes;
19
20/// Port of `JSONKind` (JSONParser.h:36).
21#[derive(Clone, Copy, PartialEq, Eq, Debug)]
22pub enum JSONKind {
23    /// A `{...}` object.
24    Object,
25    /// A `[...]` array.
26    Array,
27    /// A string literal.
28    String,
29    /// A numeric literal.
30    Number,
31    /// `true` or `false`.
32    Boolean,
33    /// `null`.
34    Null,
35}
36
37/// Port of `JSONKindToString` (JSONParser.cpp:21).
38pub fn kind_to_string(kind: JSONKind) -> &'static str {
39    match kind {
40        JSONKind::Object => "Object",
41        JSONKind::Array => "Array",
42        JSONKind::String => "String",
43        JSONKind::Number => "Number",
44        JSONKind::Boolean => "Boolean",
45        JSONKind::Null => "Null",
46    }
47}
48
49/// A descriptor with a sorted list of names; objects of the same shape share one
50/// (JSONParser.h:180 `JSONHiddenClass`). `keys` are sorted by string content.
51pub struct JSONHiddenClass<'a> {
52    pub(crate) keys: &'a [AtomBytes],
53}
54
55impl<'a> JSONHiddenClass<'a> {
56    /// Returns the number of keys in the hidden class.
57    pub fn size(&self) -> usize {
58        self.keys.len()
59    }
60
61    /// Returns the sorted slice of key atoms.
62    pub fn keys(&self) -> &'a [AtomBytes] {
63        self.keys
64    }
65
66    /// JSONParser.h:225 — binary-search the sorted keys for `name` (compared by
67    /// bytes); return its index. `atoms` resolves AtomBytes -> bytes.
68    pub fn find(&self, name: &[u8], atoms: &hermes_atom_table::AtomTable) -> Option<usize> {
69        self.keys
70            .binary_search_by(|k| atoms.bytes(*k).cmp(name))
71            .ok()
72    }
73}
74
75/// The base type for all JSON values (JSONParser.h:49). `&'a JSONValue<'a>` IS
76/// the C++ `JSONValue*`: nodes live in a `bumpalo` arena; the variant replaces
77/// the kind tag + LLVM RTTI; arena identity gives pointer equality.
78pub enum JSONValue<'a> {
79    /// `null`. A singleton within a [`JSONFactory`].
80    Null,
81    /// `true` or `false`. Two singletons within a [`JSONFactory`].
82    Boolean(bool),
83    /// A number, already converted to `f64`. Uniqued by bit pattern, so
84    /// `-0.0` and `0.0` are distinct nodes.
85    Number(f64),
86    /// A string, interned in the `AtomTable` and uniqued by that handle.
87    String(AtomBytes),
88    /// An array, as an arena slice of its elements in source order.
89    Array(&'a [&'a JSONValue<'a>]),
90    /// An object: the hidden class holding the sorted key names, plus the
91    /// values in the same order as `JSONHiddenClass::keys`.
92    Object(&'a JSONHiddenClass<'a>, &'a [&'a JSONValue<'a>]),
93}
94
95impl<'a> JSONValue<'a> {
96    /// Returns the `JSONKind` tag for this value.
97    pub fn kind(&self) -> JSONKind {
98        match self {
99            JSONValue::Null => JSONKind::Null,
100            JSONValue::Boolean(_) => JSONKind::Boolean,
101            JSONValue::Number(_) => JSONKind::Number,
102            JSONValue::String(_) => JSONKind::String,
103            JSONValue::Array(_) => JSONKind::Array,
104            JSONValue::Object(..) => JSONKind::Object,
105        }
106    }
107
108    /// Returns `Some(f)` if this is a `Number`, else `None`.
109    pub fn as_number(&self) -> Option<f64> {
110        match self {
111            JSONValue::Number(n) => Some(*n),
112            _ => None,
113        }
114    }
115
116    /// Returns `Some(b)` if this is a `Boolean`, else `None`.
117    pub fn as_boolean(&self) -> Option<bool> {
118        match self {
119            JSONValue::Boolean(b) => Some(*b),
120            _ => None,
121        }
122    }
123
124    /// Returns the interned handle (resolve bytes via the AtomTable).
125    pub fn as_string(&self) -> Option<AtomBytes> {
126        match self {
127            JSONValue::String(a) => Some(*a),
128            _ => None,
129        }
130    }
131
132    /// Returns an `ArrayView` if this is an `Array`, else `None`.
133    pub fn as_array(&self) -> Option<ArrayView<'a>> {
134        match self {
135            JSONValue::Array(v) => Some(ArrayView { values: v }),
136            _ => None,
137        }
138    }
139
140    /// Returns an `ObjectView` if this is an `Object`, else `None`.
141    pub fn as_object(&self) -> Option<ObjectView<'a>> {
142        match self {
143            JSONValue::Object(c, v) => Some(ObjectView { class: c, values: v }),
144            _ => None,
145        }
146    }
147
148    /// Port of `JSONValue::emitInto` (JSONParser.cpp:39). `atoms` resolves
149    /// interned string handles to bytes. String handles store WTF-8 bytes
150    /// (surrogate-encoded astral chars / lone surrogates are valid), which are
151    /// decoded to UTF-16 for emission, matching C++ `primitiveEmitString` via
152    /// `decodeUTF8<true>` (surrogate-tolerant). Plain valid UTF-8 input also
153    /// works correctly via the same path.
154    pub fn emit_into(
155        &self,
156        emitter: &mut hermes_support::json_emitter::JSONEmitter,
157        atoms: &hermes_atom_table::AtomTable,
158    ) {
159        match self {
160            JSONValue::Object(class, values) => {
161                emitter.open_dict();
162                for (k, v) in class.keys.iter().copied().zip(values.iter().copied()) {
163                    let ku = crate::utf8::convert_utf8_with_surrogates_to_utf16(atoms.bytes(k));
164                    emitter.emit_key_u16(&ku);
165                    v.emit_into(emitter, atoms);
166                }
167                emitter.close_dict();
168            }
169            JSONValue::Array(values) => {
170                emitter.open_array();
171                for &v in values.iter() {
172                    v.emit_into(emitter, atoms);
173                }
174                emitter.close_array();
175            }
176            JSONValue::String(a) => {
177                let vu = crate::utf8::convert_utf8_with_surrogates_to_utf16(atoms.bytes(*a));
178                emitter.emit_u16(&vu);
179            }
180            JSONValue::Number(n) => emitter.emit_f64(*n),
181            JSONValue::Boolean(b) => emitter.emit_bool(*b),
182            JSONValue::Null => emitter.emit_null_value(),
183        }
184    }
185}
186
187/// Borrowed view over an array (JSONParser.h:458 `JSONArray`).
188pub struct ArrayView<'a> {
189    values: &'a [&'a JSONValue<'a>],
190}
191
192impl<'a> ArrayView<'a> {
193    /// Returns the number of elements in the array.
194    pub fn len(&self) -> usize {
195        self.values.len()
196    }
197
198    /// Returns `true` if the array is empty.
199    pub fn is_empty(&self) -> bool {
200        self.values.is_empty()
201    }
202
203    /// Element at `pos`; panics if out of bounds (faithful to C++ `JSONArray::at`).
204    pub fn at(&self, pos: usize) -> &'a JSONValue<'a> {
205        self.values[pos]
206    }
207
208    /// Returns an iterator over `&'a JSONValue<'a>` references.
209    pub fn iter(&self) -> impl Iterator<Item = &'a JSONValue<'a>> + '_ {
210        self.values.iter().copied()
211    }
212}
213
214/// Borrowed view over an object (JSONParser.h:239 `JSONObject`). Grown in B3
215/// with name lookups + iteration.
216pub struct ObjectView<'a> {
217    pub(crate) class: &'a JSONHiddenClass<'a>,
218    pub(crate) values: &'a [&'a JSONValue<'a>],
219}
220
221impl<'a> ObjectView<'a> {
222    /// Number of members (faithful to C++ `JSONObject::size`).
223    pub fn size(&self) -> usize {
224        self.values.len()
225    }
226
227    /// Returns the hidden class descriptor shared among same-shape objects.
228    pub fn get_hidden_class(&self) -> &'a JSONHiddenClass<'a> {
229        self.class
230    }
231
232    /// JSONParser.h:286 — value for `name`, or None.
233    pub fn get(
234        &self,
235        name: &str,
236        atoms: &hermes_atom_table::AtomTable,
237    ) -> Option<&'a JSONValue<'a>> {
238        self.class.find(name.as_bytes(), atoms).map(|i| self.values[i])
239    }
240
241    /// JSONParser.h:295 — value for `name`; panics if absent (C++ asserts).
242    pub fn at(&self, name: &str, atoms: &hermes_atom_table::AtomTable) -> &'a JSONValue<'a> {
243        self.get(name, atoms).expect("name not found")
244    }
245
246    /// JSONParser.h:323 — 1 if present else 0.
247    pub fn count(&self, name: &str, atoms: &hermes_atom_table::AtomTable) -> usize {
248        if self.class.find(name.as_bytes(), atoms).is_some() {
249            1
250        } else {
251            0
252        }
253    }
254
255    /// Value by position (0..size). Panics if out of range.
256    pub fn value_at(&self, index: usize) -> &'a JSONValue<'a> {
257        self.values[index]
258    }
259
260    /// Key (interned handle) by position. Panics if out of range.
261    pub fn key_at(&self, index: usize) -> hermes_atom_table::AtomBytes {
262        self.class.keys[index]
263    }
264
265    /// JSONParser.h:440 — index of `name` in the (sorted) members, or None.
266    /// (C++ returns an iterator; we return the positional index for use with
267    /// `value_at`/`key_at`.)
268    pub fn find(&self, name: &str, atoms: &hermes_atom_table::AtomTable) -> Option<usize> {
269        self.class.find(name.as_bytes(), atoms)
270    }
271
272    /// JSONParser.h:330 — (key, value) pairs, in the hidden class's sorted order.
273    pub fn iter(
274        &self,
275    ) -> impl Iterator<Item = (hermes_atom_table::AtomBytes, &'a JSONValue<'a>)> + '_ {
276        self.class.keys.iter().copied().zip(self.values.iter().copied())
277    }
278}
279
280/// Port of `JSONSharedValue` (JSONParser.h:704-726). Pairs a JSON value
281/// reference with the `Rc<Bump>` arena that backs it so the caller can own
282/// both without worrying about arena lifetime. C++ uses `const JSONValue*` +
283/// `shared_ptr<const Allocator>`; here the raw pointer is lifetime-erased to
284/// `'static` and re-tied in `get()` via one encapsulated `unsafe` deref.
285///
286/// # Invariant
287/// `value` is allocated in `allocator`. The `Rc<Bump>` keeps the arena alive
288/// for at least as long as this holder, so the pointer is always valid.
289pub struct JSONSharedValue {
290    /// Points into `*allocator`. Lifetime-erased to `'static`; never used at
291    /// that lifetime — `get` re-ties it to `&self`.
292    value: *const JSONValue<'static>,
293    /// Keeps the arena (and therefore `*value`) alive.
294    #[allow(dead_code)] // kept to hold the arena alive via its Drop
295    allocator: std::rc::Rc<bumpalo::Bump>,
296}
297
298impl JSONSharedValue {
299    /// `value` MUST be allocated in `allocator`. The `Rc` keeps the arena alive
300    /// for as long as this holder, so the pointer stays valid.
301    pub fn new(value: &JSONValue<'_>, allocator: std::rc::Rc<bumpalo::Bump>) -> JSONSharedValue {
302        // Erase the lifetime: cast to raw pointer, then transmute-via-cast to
303        // `'static`. Transmute would be cleaner but can't change unsized
304        // lifetimes in fat pointers; the double-cast is the idiomatic pattern.
305        let value: *const JSONValue<'static> =
306            (value as *const JSONValue<'_>).cast::<JSONValue<'static>>();
307        JSONSharedValue { value, allocator }
308    }
309
310    /// The held value, re-tied to `&self`'s lifetime.
311    pub fn get(&self) -> &JSONValue<'_> {
312        // SAFETY: `self.allocator` (an `Rc<Bump>`) keeps the arena alive for at
313        // least `&self`, and `self.value` was allocated in it (constructor
314        // contract), so the pointer is valid and the returned reference cannot
315        // outlive the arena.
316        #[allow(unsafe_code)] // mirror cursor.rs; the sole JSON-component unsafe
317        unsafe { &*self.value }
318    }
319}
320
321#[cfg(test)]
322mod model_tests {
323    use super::*;
324    use bumpalo::Bump;
325
326    #[test]
327    fn kinds_and_scalar_accessors() {
328        let arena = Bump::new();
329        let n: &JSONValue = arena.alloc(JSONValue::Number(1.5));
330        let b: &JSONValue = arena.alloc(JSONValue::Boolean(true));
331        assert_eq!(n.kind(), JSONKind::Number);
332        assert_eq!(b.kind(), JSONKind::Boolean);
333        assert_eq!(n.as_number(), Some(1.5));
334        assert_eq!(b.as_boolean(), Some(true));
335        assert_eq!(n.as_boolean(), None);
336        assert_eq!(JSONValue::Null.kind(), JSONKind::Null);
337        assert_eq!(kind_to_string(JSONKind::Array), "Array");
338    }
339
340    #[test]
341    fn array_accessors() {
342        let arena = Bump::new();
343        let a = arena.alloc(JSONValue::Number(10.0));
344        let b = arena.alloc(JSONValue::Number(20.0));
345        let elems: &[&JSONValue] = arena.alloc_slice_copy(&[&*a, &*b]);
346        let arr = arena.alloc(JSONValue::Array(elems));
347        let view = arr.as_array().unwrap();
348        assert_eq!(view.len(), 2);
349        assert_eq!(view.at(0).as_number(), Some(10.0));
350        assert_eq!(view.iter().count(), 2);
351    }
352
353    #[test]
354    fn kind_to_string_all_variants() {
355        use JSONKind::*;
356        let pairs = [
357            (Object, "Object"),
358            (Array, "Array"),
359            (String, "String"),
360            (Number, "Number"),
361            (Boolean, "Boolean"),
362            (Null, "Null"),
363        ];
364        for (k, s) in pairs {
365            assert_eq!(kind_to_string(k), s);
366        }
367    }
368
369    #[test]
370    fn emit_into_round_trip() {
371        use super::JSONFactory;
372        use bumpalo::Bump;
373        use hermes_atom_table::AtomTable;
374        use hermes_support::json_emitter::JSONEmitter;
375
376        let arena = Bump::new();
377        let atoms = AtomTable::new();
378        let f = JSONFactory::new(&arena, &atoms);
379
380        // {'key1':1,'key2':'value2','key3':{'nested1':true},'key4':[false,null,'value2']}
381        let nested = {
382            let p = (f.get_string_str("nested1"), f.get_boolean(true));
383            f.new_object(&mut [p]).unwrap()
384        };
385        let arr = f.new_array(&[f.get_boolean(false), f.get_null(), f.get_string_str("value2")]);
386        let obj = f.new_object(&mut [
387            (f.get_string_str("key1"), f.get_number(1.0)),
388            (f.get_string_str("key2"), f.get_string_str("value2")),
389            (f.get_string_str("key3"), nested),
390            (f.get_string_str("key4"), arr),
391        ]).unwrap();
392
393        let mut s = String::new();
394        {
395            let mut e = JSONEmitter::new(&mut s, false);
396            obj.emit_into(&mut e, &atoms);
397        }
398        // sorted-key order: key1,key2,key3,key4
399        assert_eq!(s, r#"{"key1":1,"key2":"value2","key3":{"nested1":true},"key4":[false,null,"value2"]}"#);
400    }
401
402    #[test]
403    fn emit_into_astral_string() {
404        use super::JSONFactory;
405        use bumpalo::Bump;
406        use hermes_atom_table::AtomTable;
407        use hermes_support::json_emitter::JSONEmitter;
408        let arena = Bump::new();
409        let atoms = AtomTable::new();
410        let f = JSONFactory::new(&arena, &atoms);
411        // A string node whose interned bytes are the valid UTF-8 of U+10000 ("𐀀").
412        // convert_utf8_with_surrogates_to_utf16 decodes f0 90 80 80 to U+10000
413        // and then encode_utf16 splits it to surrogate pair [0xD800,0xDC00].
414        // emit_u16 escapes each unit as \uXXXX, matching C++ primitiveEmitString.
415        let s = f.get_string_str("\u{10000}");
416        let mut out = String::new();
417        { let mut e = JSONEmitter::new(&mut out, false); s.emit_into(&mut e, &atoms); }
418        assert_eq!(out, "\"\\ud800\\udc00\"");
419    }
420
421    #[test]
422    fn shared_value_outlives_parse() {
423        use std::rc::Rc;
424        use bumpalo::Bump;
425        // Build a value in an Rc<Bump>, wrap it, drop the local Rc, still read.
426        let shared: JSONSharedValue = {
427            let arena = Rc::new(Bump::new());
428            let v: &JSONValue = arena.alloc(JSONValue::Number(3.5));
429            JSONSharedValue::new(v, arena.clone())
430        };
431        assert_eq!(shared.get().as_number(), Some(3.5));
432    }
433
434    #[test]
435    fn string_accessor_and_hidden_class_find() {
436        use hermes_atom_table::AtomTable;
437        let arena = Bump::new();
438        let atoms = AtomTable::new();
439        let a = atoms.atom_bytes("foo");
440        let s = arena.alloc(JSONValue::String(a));
441        assert_eq!(s.as_string(), Some(a));
442        assert_eq!(s.as_number(), None);
443
444        // sorted keys: "a","b","c" -> find by bytes
445        let ka = atoms.atom_bytes("a");
446        let kb = atoms.atom_bytes("b");
447        let kc = atoms.atom_bytes("c");
448        let keys: &[hermes_atom_table::AtomBytes] = arena.alloc_slice_copy(&[ka, kb, kc]);
449        let hc = JSONHiddenClass { keys };
450        assert_eq!(hc.find(b"a", &atoms), Some(0));
451        assert_eq!(hc.find(b"b", &atoms), Some(1));
452        assert_eq!(hc.find(b"c", &atoms), Some(2));
453        assert_eq!(hc.find(b"z", &atoms), None);
454    }
455
456    #[test]
457    fn object_find_index() {
458        use super::JSONFactory;
459        use bumpalo::Bump;
460        use hermes_atom_table::AtomTable;
461        let arena = Bump::new();
462        let atoms = AtomTable::new();
463        let f = JSONFactory::new(&arena, &atoms);
464        let obj = f
465            .new_object(&mut [
466                (f.get_string_str("b"), f.get_number(2.0)),
467                (f.get_string_str("a"), f.get_number(1.0)),
468            ])
469            .unwrap();
470        let o = obj.as_object().unwrap();
471        // sorted order: a=0, b=1
472        assert_eq!(o.find("a", &atoms), Some(0));
473        assert_eq!(o.find("b", &atoms), Some(1));
474        assert_eq!(o.find("zzz", &atoms), None);
475        // find composes with value_at
476        assert_eq!(o.value_at(o.find("a", &atoms).unwrap()).as_number(), Some(1.0));
477    }
478}