Skip to main content

morphir_core/ir/
json.rs

1//! The JSON storage profile: a strict reader and the canonical writer.
2//!
3//! See `docs/spec/ir/schemas/v4/yaml-profile.md`'s JSON twin: this reader refuses what
4//! `serde_json::Value` alone cannot see — a repeated object member, and a document nested past
5//! the ceiling every reader in this crate shares — before it lets serde_json fold the text into a
6//! value tree.
7
8use std::collections::HashSet;
9use std::fmt;
10
11use serde::Deserialize;
12use serde::de::{DeserializeSeed, MapAccess, SeqAccess, Visitor};
13use serde_json::Value as Json;
14
15use crate::ir::v4::{IRFile, TypeEncoding, with_type_encoding};
16use crate::ir::{Diagnostic, DiagnosticCode, DiagnosticError, Warning};
17
18/// How many nested containers a document may carry, matching the reference reader's own ceiling
19/// (`MAX_DEPTH` in `packages/ir/src/codec/json/value.ts`) and the YAML reader's
20/// [`crate::ir::yaml::MAX_DEPTH`].
21///
22/// A reader that follows arbitrary nesting turns a small input into a deep recursion, so the
23/// profile puts a ceiling on it and reports `nesting_too_deep` rather than failing some other way
24/// at some other depth.
25pub const MAX_DEPTH: usize = 1000;
26
27/// The stack `read` grows onto when it needs to, rather than assumes it already has.
28///
29/// [`MAX_DEPTH`] is a promise: a document nesting that many containers is conforming, and the
30/// answer to one nesting a container more is `nesting_too_deep`, not a crashed process. Both the
31/// syntax probe and the parser recurse once per level, and 1000 levels of an unoptimized build's
32/// frames do not fit in the stack a thread is given by default — on Windows the main thread's
33/// stack is whatever the linker reserved, which is 1 MiB unless someone says otherwise. This is
34/// the size of the stack [`stacker::maybe_grow`] allocates when [`RED_ZONE`] says the caller's own
35/// stack is too shallow to recurse that far, matching `morphir-common`'s own
36/// `IR_RECURSION_STACK_BYTES` and the mck adapter's own decode-thread stack.
37pub(crate) const READ_STACK_BYTES: usize = 64 * 1024 * 1024;
38
39/// How much headroom `read` demands before it recurses, below which [`stacker::maybe_grow`] grows
40/// a fresh [`READ_STACK_BYTES`] stack rather than running the probe and the parse on what the
41/// caller's stack has left.
42///
43/// This has to be large enough that an ordinary caller — a test thread, a plain function call from
44/// the document-tree layout reading one file among hundreds, the process's main thread — always
45/// grows before recursing [`MAX_DEPTH`] levels: with less than this much free, a document at the
46/// ceiling could run out of native stack partway through the probe or the parse, on a build
47/// without optimizations giving each recursive frame its least economical layout. It also has to
48/// be small enough that a caller already running on a stack [`READ_STACK_BYTES`] or larger — the
49/// mck adapter's own decode thread, or a document-tree read that already grew once for the whole
50/// tree — is not made to grow again for every file.
51pub(crate) const RED_ZONE: usize = 16 * 1024 * 1024;
52
53/// Reads a JSON document under the storage profile: no repeated object member, no more than
54/// [`MAX_DEPTH`] nested containers, and otherwise whatever `serde_json` accepts.
55///
56/// Grows onto a [`READ_STACK_BYTES`] stack via [`stacker::maybe_grow`] when the caller's own stack
57/// is shallower than [`RED_ZONE`], so a document at the nesting ceiling answers `nesting_too_deep`
58/// rather than overflowing whatever stack the caller happens to be on — without paying for a
59/// spawned thread on every call, which matters here because the document-tree layout calls this
60/// once per file of a tree.
61pub fn read(text: &str) -> Result<Json, Diagnostic> {
62    stacker::maybe_grow(RED_ZONE, READ_STACK_BYTES, || read_here(text))
63}
64
65fn read_here(text: &str) -> Result<Json, Diagnostic> {
66    // A repeated member and a document nested past the ceiling are properties of the text, not of
67    // any value: `serde_json::Value` folds a repeated member onto the last one written and would
68    // hide it, so both are settled before the text becomes a value.
69    if let Some(diagnostic) = probe_syntax(text) {
70        return Err(diagnostic);
71    }
72    parse_json(text)
73}
74
75/// Reads a profile-conforming JSON document as an [`IRFile`], with the decoder's warnings.
76pub fn read_ir_file(text: &str) -> Result<(IRFile, Vec<Warning>), DiagnosticError> {
77    let value = read(text).map_err(DiagnosticError)?;
78    crate::ir::v4::decode_ir_file_with_warnings(&value)
79}
80
81/// Writes an [`IRFile`] as canonical profile JSON, with no trailing newline.
82///
83/// The value tree is built under [`TypeEncoding::Compact`], which is the canonical spelling of a
84/// type expression (decision 0005): a reference with no arguments and no attributes is
85/// `morphir/SDK:basics#int`, not an expanded wrapper. The thread-local defaults to `Expanded`, so
86/// a canonical writer has to say so — mirroring [`crate::ir::yaml::write_ir_file`].
87pub fn write_ir_file(file: &IRFile) -> String {
88    let value = with_type_encoding(TypeEncoding::Compact, || serde_json::to_value(file))
89        .expect("an IRFile serialises");
90    write_canonical(&value)
91}
92
93/// Writes a value in the JSON profile's canonical text form.
94///
95/// The profile's writer is not `serde_json::to_string`: a non-empty object is padded inside its
96/// braces and its members separated by `, `, while an array is not padded. The driver compares
97/// canonicals as strings (kit README, "What the driver does with a case"), so this is part of
98/// the contract rather than a style.
99pub fn write_canonical(value: &Json) -> String {
100    match value {
101        Json::Null => "null".to_string(),
102        Json::Bool(true) => "true".to_string(),
103        Json::Bool(false) => "false".to_string(),
104        // What `arbitrary_precision` buys is that a number *parsed from text* keeps the lexeme
105        // it was written with, which is what a `DocumentLiteral` payload needs. `Literal::Float`
106        // now carries its lexeme too, so `1.0e2` comes back out as `1.0e2` rather than `100.0`.
107        Json::Number(number) => number.to_string(),
108        Json::String(_) => serde_json::to_string(value).expect("a string always serializes"),
109        Json::Array(elements) if elements.is_empty() => "[]".to_string(),
110        Json::Array(elements) => format!(
111            "[{}]",
112            elements
113                .iter()
114                .map(write_canonical)
115                .collect::<Vec<_>>()
116                .join(", ")
117        ),
118        Json::Object(members) if members.is_empty() => "{}".to_string(),
119        Json::Object(members) => format!(
120            "{{ {} }}",
121            members
122                .iter()
123                .map(|(key, member)| format!(
124                    "{}: {}",
125                    serde_json::to_string(key).expect("a member name always serializes"),
126                    write_canonical(member)
127                ))
128                .collect::<Vec<_>>()
129                .join(", ")
130        ),
131    }
132}
133
134// =============================================================================
135// Parsing
136// =============================================================================
137
138/// Parses the input as JSON, with the ceiling this reader states rather than serde_json's own.
139///
140/// `disable_recursion_limit` needs the `unbounded_depth` feature; without it serde_json stops at
141/// its own default of 128, which would report `invalid_json` for a document the profile admits.
142/// The depth that matters is [`MAX_DEPTH`], and [`probe_syntax`] has already enforced it.
143fn parse_json(text: &str) -> Result<Json, Diagnostic> {
144    let mut deserializer = serde_json::Deserializer::from_str(text);
145    deserializer.disable_recursion_limit();
146    let value = Json::deserialize(&mut deserializer).map_err(invalid_json)?;
147    deserializer.end().map_err(invalid_json)?;
148    Ok(value)
149}
150
151fn invalid_json(error: serde_json::Error) -> Diagnostic {
152    let mut diagnostic = Diagnostic::syntax(DiagnosticCode::InvalidJson, "/", error.to_string());
153    diagnostic.line = u32::try_from(error.line()).ok();
154    diagnostic.column = u32::try_from(error.column()).ok();
155    diagnostic
156}
157
158// =============================================================================
159// Duplicate members and nesting
160// =============================================================================
161
162/// The two syntactic rules a `serde_json::Value` cannot carry: no repeated object member, and no
163/// more than [`MAX_DEPTH`] nested containers.
164///
165/// `Value` keeps one entry per key, so the second `"a"` in `{"a":1,"a":2}` is gone by the time a
166/// value exists, and serde_json's own recursion limit fails before this reader's ceiling is
167/// reached. The probe below walks the token stream instead, carrying the JSON pointer of where
168/// it is, and stops at the first thing it finds: `duplicate_member` at the second occurrence, or
169/// `nesting_too_deep` at the container that crossed the ceiling.
170fn probe_syntax(text: &str) -> Option<Diagnostic> {
171    let mut deserializer = serde_json::Deserializer::from_str(text);
172    deserializer.disable_recursion_limit();
173    match (Probe {
174        cursor: String::new(),
175        depth: 0,
176    })
177    .deserialize(&mut deserializer)
178    {
179        Ok(()) => None,
180        // A syntax error is not this probe's to report: `parse_json` reports it with the line
181        // and column serde_json gives, as `invalid_json`.
182        Err(error) => Diagnostic::from_serde_error(&error),
183    }
184}
185
186/// One position in the token stream: the JSON pointer of the value about to be read and how
187/// many containers are already open around it.
188///
189/// The cursor is built the way the reference reader builds it: empty at the root, then
190/// `<parent>/<member or index>` with the member name written out as it appears. A diagnostic at
191/// the root reports `/` (see [`cursor_or_root`]).
192///
193/// This is a [`DeserializeSeed`] rather than a [`Deserialize`] because the cursor and the depth
194/// have to travel *into* each member, and a `Deserialize` impl is handed nothing but the
195/// deserializer.
196struct Probe {
197    cursor: String,
198    depth: usize,
199}
200
201/// serde_json's `arbitrary_precision` feature carries a number through `deserialize_any` as a
202/// one-member map under this reserved key, so the probe would otherwise count every number as a
203/// container and read its lexeme as a member name.
204///
205/// The key alone does not make a map the token: a document literal is free to spell a member this
206/// way. The whole shape does — exactly this one member, holding a string — and [`Probe::visit_map`]
207/// checks the shape before it takes a map for a number. A map that is only shaped like the token
208/// is indistinguishable from one at this layer, because it is exactly what serde_json emits for a
209/// number, but anything else is walked like the ordinary object it is.
210const NUMBER_TOKEN: &str = "$serde_json::private::Number";
211
212impl<'de> DeserializeSeed<'de> for Probe {
213    type Value = ();
214
215    fn deserialize<D>(self, deserializer: D) -> Result<(), D::Error>
216    where
217        D: serde::Deserializer<'de>,
218    {
219        deserializer.deserialize_any(self)
220    }
221}
222
223impl<'de> Visitor<'de> for Probe {
224    type Value = ();
225
226    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
227        formatter.write_str("any JSON value")
228    }
229
230    fn visit_map<A>(self, mut map: A) -> Result<(), A::Error>
231    where
232        A: MapAccess<'de>,
233    {
234        // The reserved-number check comes before the depth guard, not inside the loop: a number
235        // is a scalar the profile counts at no depth at all, and charging it a nesting level
236        // would make the ceiling depend on whether the innermost value happened to be a number.
237        let Some(first) = map.next_key::<String>()? else {
238            return self.enter::<A::Error>().map(|_| ());
239        };
240
241        let mut seen: HashSet<String> = HashSet::new();
242        let depth;
243        let mut key;
244
245        if first == NUMBER_TOKEN {
246            // Only the token's whole shape is the token. The value decides the first half of it,
247            // and reading it also walks it when it turns out to belong to a user object, so the
248            // level that object owes is charged there rather than here.
249            match map.next_value_seed(NumberTokenValue { outer: &self })? {
250                TokenValue::Lexeme => match map.next_key::<String>()? {
251                    // Exactly one member, holding a string: serde_json's number token.
252                    None => return Ok(()),
253                    // A user object whose first member is spelled like the token and holds a
254                    // string. The string carried nothing to walk, so only the level is still
255                    // owed, and the rest of the members are read like any other object's.
256                    Some(next) => {
257                        depth = self.enter::<A::Error>()?;
258                        seen.insert(first);
259                        key = next;
260                    }
261                },
262                TokenValue::Walked(walked) => {
263                    depth = walked;
264                    seen.insert(first);
265                    match map.next_key::<String>()? {
266                        Some(next) => key = next,
267                        None => return Ok(()),
268                    }
269                }
270            }
271        } else {
272            depth = self.enter::<A::Error>()?;
273            key = first;
274        }
275
276        loop {
277            // The member name goes in raw, not JSON-Pointer-escaped: the reference reader
278            // (`packages/ir/src/codec/json/value.ts`) builds the cursor this way and the kit
279            // README makes that reader the convention a binding mirrors.
280            let cursor = format!("{}/{}", self.cursor, key);
281            if !seen.insert(key.clone()) {
282                return Err(carry(Diagnostic::syntax(
283                    DiagnosticCode::DuplicateMember,
284                    cursor,
285                    format!("duplicate member \"{key}\""),
286                )));
287            }
288            map.next_value_seed(Probe { cursor, depth })?;
289            match map.next_key::<String>()? {
290                Some(next) => key = next,
291                None => return Ok(()),
292            }
293        }
294    }
295
296    fn visit_seq<A>(self, mut seq: A) -> Result<(), A::Error>
297    where
298        A: SeqAccess<'de>,
299    {
300        let depth = self.enter::<A::Error>()?;
301        let mut index = 0usize;
302        while seq
303            .next_element_seed(Probe {
304                cursor: format!("{}/{index}", self.cursor),
305                depth,
306            })?
307            .is_some()
308        {
309            index += 1;
310        }
311        Ok(())
312    }
313
314    fn visit_bool<E: serde::de::Error>(self, _value: bool) -> Result<(), E> {
315        Ok(())
316    }
317
318    fn visit_i64<E: serde::de::Error>(self, _value: i64) -> Result<(), E> {
319        Ok(())
320    }
321
322    fn visit_u64<E: serde::de::Error>(self, _value: u64) -> Result<(), E> {
323        Ok(())
324    }
325
326    fn visit_f64<E: serde::de::Error>(self, _value: f64) -> Result<(), E> {
327        Ok(())
328    }
329
330    fn visit_str<E: serde::de::Error>(self, _value: &str) -> Result<(), E> {
331        Ok(())
332    }
333
334    fn visit_unit<E: serde::de::Error>(self) -> Result<(), E> {
335        Ok(())
336    }
337
338    fn visit_none<E: serde::de::Error>(self) -> Result<(), E> {
339        Ok(())
340    }
341}
342
343/// What the value under a [`NUMBER_TOKEN`] key turned out to be.
344enum TokenValue {
345    /// A string, which is what serde_json puts a number's lexeme in.
346    Lexeme,
347    /// Anything else, so the map holding it is a user object. The value has already been walked,
348    /// and the level that object owes has already been charged; this is the depth it was charged.
349    Walked(usize),
350}
351
352/// Reads the value under a [`NUMBER_TOKEN`] key and says which of the two it was.
353///
354/// It cannot just look, because a value read is a value consumed: whatever this finds has to be
355/// walked here or not at all. So the two answers are "a string, nothing to walk" and "walked it,
356/// here is the depth I charged the object for".
357struct NumberTokenValue<'probe> {
358    /// The map the key was read from, at its own position — not yet entered.
359    outer: &'probe Probe,
360}
361
362impl<'probe> NumberTokenValue<'probe> {
363    /// The probe for the value, with the level the object owes charged.
364    fn walker<E: serde::de::Error>(&self) -> Result<Probe, E> {
365        Ok(Probe {
366            cursor: format!("{}/{NUMBER_TOKEN}", self.outer.cursor),
367            depth: self.outer.enter::<E>()?,
368        })
369    }
370}
371
372impl<'de, 'probe> DeserializeSeed<'de> for NumberTokenValue<'probe> {
373    type Value = TokenValue;
374
375    fn deserialize<D>(self, deserializer: D) -> Result<TokenValue, D::Error>
376    where
377        D: serde::Deserializer<'de>,
378    {
379        deserializer.deserialize_any(self)
380    }
381}
382
383impl<'de, 'probe> Visitor<'de> for NumberTokenValue<'probe> {
384    type Value = TokenValue;
385
386    fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
387        formatter.write_str("any JSON value")
388    }
389
390    fn visit_str<E: serde::de::Error>(self, _value: &str) -> Result<TokenValue, E> {
391        Ok(TokenValue::Lexeme)
392    }
393
394    fn visit_map<A>(self, map: A) -> Result<TokenValue, A::Error>
395    where
396        A: MapAccess<'de>,
397    {
398        let walker = self.walker::<A::Error>()?;
399        let depth = walker.depth;
400        walker.visit_map(map)?;
401        Ok(TokenValue::Walked(depth))
402    }
403
404    fn visit_seq<A>(self, seq: A) -> Result<TokenValue, A::Error>
405    where
406        A: SeqAccess<'de>,
407    {
408        let walker = self.walker::<A::Error>()?;
409        let depth = walker.depth;
410        walker.visit_seq(seq)?;
411        Ok(TokenValue::Walked(depth))
412    }
413
414    fn visit_bool<E: serde::de::Error>(self, _value: bool) -> Result<TokenValue, E> {
415        self.walker::<E>()
416            .map(|walker| TokenValue::Walked(walker.depth))
417    }
418
419    fn visit_i64<E: serde::de::Error>(self, _value: i64) -> Result<TokenValue, E> {
420        self.walker::<E>()
421            .map(|walker| TokenValue::Walked(walker.depth))
422    }
423
424    fn visit_u64<E: serde::de::Error>(self, _value: u64) -> Result<TokenValue, E> {
425        self.walker::<E>()
426            .map(|walker| TokenValue::Walked(walker.depth))
427    }
428
429    fn visit_f64<E: serde::de::Error>(self, _value: f64) -> Result<TokenValue, E> {
430        self.walker::<E>()
431            .map(|walker| TokenValue::Walked(walker.depth))
432    }
433
434    fn visit_unit<E: serde::de::Error>(self) -> Result<TokenValue, E> {
435        self.walker::<E>()
436            .map(|walker| TokenValue::Walked(walker.depth))
437    }
438
439    fn visit_none<E: serde::de::Error>(self) -> Result<TokenValue, E> {
440        self.walker::<E>()
441            .map(|walker| TokenValue::Walked(walker.depth))
442    }
443}
444
445impl Probe {
446    /// Opens the container at this position, refusing the one that crosses the ceiling.
447    fn enter<E: serde::de::Error>(&self) -> Result<usize, E> {
448        let depth = self.depth + 1;
449        if depth > MAX_DEPTH {
450            return Err(carry(Diagnostic::syntax(
451                DiagnosticCode::NestingTooDeep,
452                cursor_or_root(&self.cursor),
453                format!("nesting deeper than {MAX_DEPTH} is not accepted"),
454            )));
455        }
456        Ok(depth)
457    }
458}
459
460/// A cursor as a diagnostic reports it: the root is the whole document, spelled `/`.
461fn cursor_or_root(cursor: &str) -> &str {
462    if cursor.is_empty() { "/" } else { cursor }
463}
464
465fn carry<E: serde::de::Error>(diagnostic: Diagnostic) -> E {
466    E::custom(DiagnosticError(diagnostic))
467}