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}