Skip to main content

doge_runtime/stdlib/
json.rs

1//! `json` — parse and emit JSON. `json.parse(text)` turns a JSON document into a
2//! Doge value (object → Dict, array → List, the rest → Str/Int/Float/Bool/none);
3//! `json.emit(value)` turns one back into compact JSON text. Every malformed input
4//! is a catchable `ValueError` with the offset it failed at, and a value that has
5//! no JSON form (an object, function, socket, …) is a catchable `TypeError` —
6//! neither ever panics.
7
8use std::fmt::Write;
9
10use num_bigint::BigInt;
11
12use crate::error::{DogeError, DogeResult};
13use crate::ordered_map::OrderedMap;
14use crate::stdlib::serialize::{escape_str, too_deep, unsupported, MAX_DEPTH};
15use crate::stdlib::str_arg;
16use crate::value::Value;
17
18/// `json.parse(text)` — the value a JSON document denotes. Leading and trailing
19/// whitespace is ignored; anything after the top-level value is an error.
20pub fn json_parse(text: &Value) -> DogeResult {
21    let text = str_arg("json", "parse", text)?;
22    let mut p = Parser {
23        chars: text.chars().collect(),
24        pos: 0,
25    };
26    p.skip_ws();
27    let value = p.value(0)?;
28    p.skip_ws();
29    if p.pos != p.chars.len() {
30        return Err(p.error("expected end of input"));
31    }
32    Ok(value)
33}
34
35/// `json.emit(value)` — a compact JSON document (no insignificant whitespace) for
36/// `value`. Dict/List/Str/Int/Float/Bool/none serialize; any other type, or a
37/// non-finite Float (JSON has no NaN/infinity), is a catchable error.
38pub fn json_emit(value: &Value) -> DogeResult {
39    let mut out = String::new();
40    emit(value, &mut out, 0)?;
41    Ok(Value::str(out))
42}
43
44fn emit(value: &Value, out: &mut String, depth: usize) -> Result<(), DogeError> {
45    if depth >= MAX_DEPTH {
46        return Err(too_deep("json", "emit"));
47    }
48    match value {
49        Value::None => out.push_str("null"),
50        Value::Bool(b) => out.push_str(if *b { "true" } else { "false" }),
51        Value::Int(n) => {
52            let _ = write!(out, "{n}");
53        }
54        // A Decimal emits as a bare JSON number, preserving its exact digits.
55        // JSON has no decimal type, so a round-trip through `json.parse` returns a
56        // Float — the value is exact on the wire, inexact only if re-parsed.
57        Value::Decimal(d) => {
58            let _ = write!(out, "{d}");
59        }
60        Value::Float(x) => {
61            if !x.is_finite() {
62                return Err(DogeError::value_error(
63                    "json.emit cannot serialize a Float that is not finite",
64                ));
65            }
66            // A whole-number Float keeps its decimal point so it re-parses as a
67            // Float, matching how `bark` prints it (3.0, never 3).
68            if x.fract() == 0.0 {
69                let _ = write!(out, "{x:.1}");
70            } else {
71                let _ = write!(out, "{x}");
72            }
73        }
74        Value::Str(s) => escape_str(s, out, |o, cp| {
75            let _ = write!(o, "\\u{cp:04x}");
76        }),
77        Value::List(items) => {
78            out.push('[');
79            for (i, item) in items.borrow().iter().enumerate() {
80                if i > 0 {
81                    out.push(',');
82                }
83                emit(item, out, depth + 1)?;
84            }
85            out.push(']');
86        }
87        Value::Dict(entries) => {
88            out.push('{');
89            for (i, (key, val)) in entries.borrow().iter().enumerate() {
90                if i > 0 {
91                    out.push(',');
92                }
93                escape_str(key, out, |o, cp| {
94                    let _ = write!(o, "\\u{cp:04x}");
95                });
96                out.push(':');
97                emit(val, out, depth + 1)?;
98            }
99            out.push('}');
100        }
101        other => return Err(unsupported("json", other)),
102    }
103    Ok(())
104}
105
106struct Parser {
107    chars: Vec<char>,
108    pos: usize,
109}
110
111impl Parser {
112    fn error(&self, what: &str) -> DogeError {
113        DogeError::value_error(format!(
114            "json.parse: much invalid. {what} at offset {}",
115            self.pos
116        ))
117    }
118
119    fn peek(&self) -> Option<char> {
120        self.chars.get(self.pos).copied()
121    }
122
123    fn skip_ws(&mut self) {
124        while matches!(self.peek(), Some(' ' | '\t' | '\n' | '\r')) {
125            self.pos += 1;
126        }
127    }
128
129    fn value(&mut self, depth: usize) -> DogeResult {
130        if depth >= MAX_DEPTH {
131            return Err(too_deep("json", "parse"));
132        }
133        match self.peek() {
134            Some('{') => self.object(depth),
135            Some('[') => self.array(depth),
136            Some('"') => Ok(Value::str(self.string()?)),
137            Some('t') => self.keyword("true", Value::Bool(true)),
138            Some('f') => self.keyword("false", Value::Bool(false)),
139            Some('n') => self.keyword("null", Value::None),
140            Some(c) if c == '-' || c.is_ascii_digit() => self.number(),
141            Some(_) => Err(self.error("unexpected character")),
142            None => Err(self.error("unexpected end of input")),
143        }
144    }
145
146    fn keyword(&mut self, word: &str, value: Value) -> DogeResult {
147        for expected in word.chars() {
148            if self.peek() != Some(expected) {
149                return Err(self.error("unexpected character"));
150            }
151            self.pos += 1;
152        }
153        Ok(value)
154    }
155
156    fn object(&mut self, depth: usize) -> DogeResult {
157        self.pos += 1; // consume '{'
158        let mut map = OrderedMap::new();
159        self.skip_ws();
160        if self.peek() == Some('}') {
161            self.pos += 1;
162            return Ok(Value::dict(map));
163        }
164        loop {
165            self.skip_ws();
166            if self.peek() != Some('"') {
167                return Err(self.error("expected a string key"));
168            }
169            let key = self.string()?;
170            self.skip_ws();
171            if self.peek() != Some(':') {
172                return Err(self.error("expected ':'"));
173            }
174            self.pos += 1;
175            self.skip_ws();
176            let val = self.value(depth + 1)?;
177            map.insert(key, val);
178            self.skip_ws();
179            match self.peek() {
180                Some(',') => self.pos += 1,
181                Some('}') => {
182                    self.pos += 1;
183                    return Ok(Value::dict(map));
184                }
185                _ => return Err(self.error("expected ',' or '}'")),
186            }
187        }
188    }
189
190    fn array(&mut self, depth: usize) -> DogeResult {
191        self.pos += 1; // consume '['
192        let mut items = Vec::new();
193        self.skip_ws();
194        if self.peek() == Some(']') {
195            self.pos += 1;
196            return Ok(Value::list(items));
197        }
198        loop {
199            self.skip_ws();
200            items.push(self.value(depth + 1)?);
201            self.skip_ws();
202            match self.peek() {
203                Some(',') => self.pos += 1,
204                Some(']') => {
205                    self.pos += 1;
206                    return Ok(Value::list(items));
207                }
208                _ => return Err(self.error("expected ',' or ']'")),
209            }
210        }
211    }
212
213    fn string(&mut self) -> DogeResult<String> {
214        self.pos += 1; // consume opening '"'
215        let mut s = String::new();
216        loop {
217            match self.peek() {
218                None => return Err(self.error("unterminated string")),
219                Some('"') => {
220                    self.pos += 1;
221                    return Ok(s);
222                }
223                Some('\\') => {
224                    self.pos += 1;
225                    self.escape(&mut s)?;
226                }
227                Some(c) if (c as u32) < 0x20 => {
228                    return Err(self.error("control character in a string"))
229                }
230                Some(c) => {
231                    s.push(c);
232                    self.pos += 1;
233                }
234            }
235        }
236    }
237
238    fn escape(&mut self, s: &mut String) -> DogeResult<()> {
239        match self.peek() {
240            Some('"') => s.push('"'),
241            Some('\\') => s.push('\\'),
242            Some('/') => s.push('/'),
243            Some('b') => s.push('\u{08}'),
244            Some('f') => s.push('\u{0c}'),
245            Some('n') => s.push('\n'),
246            Some('r') => s.push('\r'),
247            Some('t') => s.push('\t'),
248            Some('u') => {
249                self.pos += 1;
250                return self.unicode_escape(s);
251            }
252            _ => return Err(self.error("invalid escape")),
253        }
254        self.pos += 1;
255        Ok(())
256    }
257
258    /// A `\uXXXX` escape (the leading `\u` already consumed). A high surrogate is
259    /// paired with a following `\uXXXX` low surrogate into one code point, so
260    /// astral characters written as surrogate pairs decode correctly.
261    fn unicode_escape(&mut self, s: &mut String) -> DogeResult<()> {
262        let hi = self.hex4()?;
263        let cp = if (0xd800..=0xdbff).contains(&hi) {
264            if self.peek() != Some('\\') {
265                return Err(self.error("lone surrogate in a \\u escape"));
266            }
267            self.pos += 1;
268            if self.peek() != Some('u') {
269                return Err(self.error("lone surrogate in a \\u escape"));
270            }
271            self.pos += 1;
272            let lo = self.hex4()?;
273            if !(0xdc00..=0xdfff).contains(&lo) {
274                return Err(self.error("invalid low surrogate in a \\u escape"));
275            }
276            0x10000 + ((hi - 0xd800) << 10) + (lo - 0xdc00)
277        } else if (0xdc00..=0xdfff).contains(&hi) {
278            return Err(self.error("lone surrogate in a \\u escape"));
279        } else {
280            hi
281        };
282        match char::from_u32(cp) {
283            Some(c) => {
284                s.push(c);
285                Ok(())
286            }
287            None => Err(self.error("invalid code point in a \\u escape")),
288        }
289    }
290
291    fn hex4(&mut self) -> DogeResult<u32> {
292        let mut value = 0u32;
293        for _ in 0..4 {
294            match self.peek().and_then(|c| c.to_digit(16)) {
295                Some(d) => {
296                    value = value * 16 + d;
297                    self.pos += 1;
298                }
299                None => return Err(self.error("expected four hex digits after \\u")),
300            }
301        }
302        Ok(value)
303    }
304
305    /// A JSON number. With neither a fraction nor an exponent it is an Int when it
306    /// fits `i64` (otherwise a Float); with either, it is always a Float.
307    fn number(&mut self) -> DogeResult {
308        let start = self.pos;
309        if self.peek() == Some('-') {
310            self.pos += 1;
311        }
312        self.digits()?;
313        let mut is_float = false;
314        if self.peek() == Some('.') {
315            is_float = true;
316            self.pos += 1;
317            self.digits()?;
318        }
319        if matches!(self.peek(), Some('e' | 'E')) {
320            is_float = true;
321            self.pos += 1;
322            if matches!(self.peek(), Some('+' | '-')) {
323                self.pos += 1;
324            }
325            self.digits()?;
326        }
327        let literal: String = self.chars[start..self.pos].iter().collect();
328        if !is_float {
329            if let Ok(n) = literal.parse::<BigInt>() {
330                return Ok(Value::Int(n));
331            }
332        }
333        match literal.parse::<f64>() {
334            Ok(x) => Ok(Value::Float(x)),
335            Err(_) => Err(self.error("invalid number")),
336        }
337    }
338
339    fn digits(&mut self) -> DogeResult<()> {
340        if !matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
341            return Err(self.error("expected a digit"));
342        }
343        while matches!(self.peek(), Some(c) if c.is_ascii_digit()) {
344            self.pos += 1;
345        }
346        Ok(())
347    }
348}
349
350#[cfg(test)]
351mod tests {
352    use super::*;
353    use crate::error::ErrorKind;
354    use crate::ops::values_equal;
355
356    fn parse(text: &str) -> DogeResult {
357        json_parse(&Value::str(text))
358    }
359
360    fn emit_str(v: &Value) -> String {
361        match json_emit(v).unwrap() {
362            Value::Str(s) => s.to_string(),
363            other => panic!("expected a Str, got {other:?}"),
364        }
365    }
366
367    #[test]
368    fn scalars_parse_to_the_right_types() {
369        assert!(matches!(parse("null").unwrap(), Value::None));
370        assert!(matches!(parse("true").unwrap(), Value::Bool(true)));
371        assert!(matches!(parse("false").unwrap(), Value::Bool(false)));
372        assert!(values_equal(&parse("  42 ").unwrap(), &Value::int(42)));
373        assert!(values_equal(&parse("-7").unwrap(), &Value::int(-7)));
374        assert!(matches!(parse("\"wow\"").unwrap(), Value::Str(s) if &*s == "wow"));
375    }
376
377    #[test]
378    fn a_fraction_or_exponent_is_a_float_but_a_bare_integer_is_an_int() {
379        assert!(matches!(parse("3.5").unwrap(), Value::Float(x) if x == 3.5));
380        assert!(matches!(parse("3.0").unwrap(), Value::Float(x) if x == 3.0));
381        assert!(matches!(parse("1e2").unwrap(), Value::Float(x) if x == 100.0));
382        assert!(values_equal(&parse("100").unwrap(), &Value::int(100)));
383    }
384
385    #[test]
386    fn an_integer_past_i64_parses_exactly() {
387        // 2^63 does not fit i64, but Int is arbitrary precision, so it parses
388        // exactly rather than losing precision to a Float.
389        let big = "9223372036854775808";
390        assert_eq!(parse(big).unwrap().to_string(), big);
391    }
392
393    #[test]
394    fn nested_structure_round_trips_through_emit() {
395        let src = "{\"name\":\"kabosu\",\"tags\":[\"doge\",\"shibe\"],\"age\":7,\"good\":true,\"note\":null}";
396        let value = parse(src).unwrap();
397        assert_eq!(emit_str(&value), src);
398    }
399
400    #[test]
401    fn a_surrogate_pair_decodes_to_one_astral_character() {
402        // U+1F415 DOG, written as a UTF-16 surrogate pair.
403        assert!(matches!(parse("\"\\ud83d\\udc15\"").unwrap(), Value::Str(s) if &*s == "🐕"));
404    }
405
406    #[test]
407    fn a_duplicate_key_keeps_the_last_value() {
408        assert_eq!(emit_str(&parse("{\"a\":1,\"a\":2}").unwrap()), "{\"a\":2}");
409    }
410
411    #[test]
412    fn emit_escapes_quotes_and_control_characters() {
413        assert_eq!(
414            emit_str(&Value::str("a\"b\n\t\u{01}")),
415            "\"a\\\"b\\n\\t\\u0001\""
416        );
417    }
418
419    #[test]
420    fn trailing_garbage_is_a_catchable_value_error() {
421        assert_eq!(parse("[1, 2] wow").unwrap_err().kind, ErrorKind::ValueError);
422    }
423
424    #[test]
425    fn deep_nesting_is_a_catchable_error_not_a_stack_overflow() {
426        let deep = "[".repeat(MAX_DEPTH + 5);
427        assert_eq!(parse(&deep).unwrap_err().kind, ErrorKind::ValueError);
428    }
429
430    #[test]
431    fn a_non_finite_float_cannot_be_emitted() {
432        assert_eq!(
433            json_emit(&Value::Float(f64::NAN)).unwrap_err().kind,
434            ErrorKind::ValueError
435        );
436    }
437
438    #[test]
439    fn an_unsupported_type_is_a_catchable_type_error() {
440        let f = Value::function(0, "greet", vec![]);
441        assert_eq!(json_emit(&f).unwrap_err().kind, ErrorKind::TypeError);
442    }
443
444    #[test]
445    fn a_non_str_argument_to_parse_is_a_type_error() {
446        assert_eq!(
447            json_parse(&Value::int(1)).unwrap_err().kind,
448            ErrorKind::TypeError
449        );
450    }
451}