Skip to main content

doge_runtime/stdlib/
dson.rs

1//! `dson` — parse and emit DSON, Doge Serialized Object Notation
2//! (<https://dogeon.xyz/>): JSON's shape wearing doge-speak. Objects are
3//! `such … wow` with `is` between key and value, arrays are `so … many`, the
4//! literals are `yes`/`no`/`empty`, and every number is written in octal. The
5//! value mapping and catchable-error contract match the [`json`](super::json)
6//! codec — object → Dict, array → List, the rest → Str/Int/Float/Bool/none — so
7//! the two are interchangeable but for their surface syntax.
8
9use std::fmt::Write;
10
11use crate::error::{DogeError, DogeResult};
12use crate::ordered_map::OrderedMap;
13use crate::stdlib::serialize::{escape_str, too_deep, unsupported, MAX_DEPTH};
14use crate::stdlib::str_arg;
15use crate::value::Value;
16
17/// The longest exact base-8 fraction a finite `f64` can have: a subnormal's
18/// fraction is a dyadic rational with up to 1074 bits, and each octal digit
19/// carries three bits, so the expansion terminates within `ceil(1074 / 3)`
20/// digits. Used only as a safety bound — a correct expansion always stops sooner.
21const OCTAL_FRACTION_LIMIT: usize = 358;
22
23/// `dson.parse(text)` — the value a DSON document denotes. Leading and trailing
24/// whitespace is ignored; anything after the top-level value is an error.
25pub fn dson_parse(text: &Value) -> DogeResult {
26    let text = str_arg("dson", "parse", text)?;
27    let chars: Vec<char> = text.chars().collect();
28    let (toks, offs) = lex(&chars)?;
29    let mut p = Parser {
30        toks,
31        offs,
32        end: chars.len(),
33        pos: 0,
34    };
35    let value = p.value(0)?;
36    if p.pos != p.toks.len() {
37        return Err(p.error("expected end of input"));
38    }
39    Ok(value)
40}
41
42/// `dson.emit(value)` — a DSON document for `value`. Dict/List/Str/Int/Float/Bool/
43/// none serialize; any other type, or a non-finite Float, is a catchable error.
44pub fn dson_emit(value: &Value) -> DogeResult {
45    let mut out = String::new();
46    emit(value, &mut out, 0)?;
47    Ok(Value::str(out))
48}
49
50fn dson_unicode(out: &mut String, cp: u32) {
51    let _ = write!(out, "\\u{cp:06o}");
52}
53
54fn emit(value: &Value, out: &mut String, depth: usize) -> Result<(), DogeError> {
55    if depth >= MAX_DEPTH {
56        return Err(too_deep("dson", "emit"));
57    }
58    match value {
59        Value::None => out.push_str("empty"),
60        Value::Bool(b) => out.push_str(if *b { "yes" } else { "no" }),
61        Value::Int(n) => emit_int(*n, out),
62        Value::Float(x) => emit_float(*x, out)?,
63        Value::Str(s) => escape_str(s, out, dson_unicode),
64        Value::List(items) => {
65            let items = items.borrow();
66            if items.is_empty() {
67                out.push_str("so many");
68            } else {
69                out.push_str("so ");
70                for (i, item) in items.iter().enumerate() {
71                    if i > 0 {
72                        out.push_str(" and ");
73                    }
74                    emit(item, out, depth + 1)?;
75                }
76                out.push_str(" many");
77            }
78        }
79        Value::Dict(entries) => {
80            let entries = entries.borrow();
81            if entries.is_empty() {
82                out.push_str("such wow");
83            } else {
84                out.push_str("such ");
85                for (i, (key, val)) in entries.iter().enumerate() {
86                    if i > 0 {
87                        out.push_str(", ");
88                    }
89                    escape_str(key, out, dson_unicode);
90                    out.push_str(" is ");
91                    emit(val, out, depth + 1)?;
92                }
93                out.push_str(" wow");
94            }
95        }
96        other => return Err(unsupported("dson", other)),
97    }
98    Ok(())
99}
100
101/// An Int as signed octal (`-` then the magnitude's octal digits).
102fn emit_int(n: i64, out: &mut String) {
103    if n < 0 {
104        let _ = write!(out, "-{:o}", n.unsigned_abs());
105    } else {
106        let _ = write!(out, "{n:o}");
107    }
108}
109
110/// A finite Float as its exact octal expansion, always with a fractional part
111/// (`0.4` for 0.5) so it re-parses as a Float rather than an Int. NaN/±infinity
112/// have no DSON form and are a catchable error.
113fn emit_float(x: f64, out: &mut String) -> Result<(), DogeError> {
114    if !x.is_finite() {
115        return Err(DogeError::value_error(
116            "dson.emit cannot serialize a Float that is not finite",
117        ));
118    }
119    if x.is_sign_negative() && x != 0.0 {
120        out.push('-');
121    }
122    let mut v = x.abs();
123    let int_part = v.trunc();
124    v -= int_part;
125
126    // Integer part in octal, most-significant digit first.
127    if int_part < 1.0 {
128        out.push('0');
129    } else {
130        let mut digits = Vec::new();
131        let mut ip = int_part;
132        while ip >= 1.0 {
133            digits.push(octal_digit((ip % 8.0) as u32));
134            ip = (ip / 8.0).floor();
135        }
136        out.extend(digits.iter().rev());
137    }
138
139    // Fractional part: multiply by 8 and take the whole part each step. Every
140    // finite f64 fraction is a dyadic rational and 8 = 2^3, so this terminates.
141    out.push('.');
142    if v == 0.0 {
143        out.push('0');
144        return Ok(());
145    }
146    let mut produced = 0;
147    while v != 0.0 && produced < OCTAL_FRACTION_LIMIT {
148        v *= 8.0;
149        let digit = v.floor();
150        out.push(octal_digit(digit as u32));
151        v -= digit;
152        produced += 1;
153    }
154    Ok(())
155}
156
157fn octal_digit(d: u32) -> char {
158    char::from_digit(d, 8).unwrap_or('0')
159}
160
161/// A DSON token. Numbers are resolved to a `Value` at lex time (octal is a
162/// lexical concern); the structural words and separators carry no payload.
163#[derive(Debug, Clone)]
164enum Tok {
165    Such,
166    Wow,
167    So,
168    Many,
169    Is,
170    And,
171    Also,
172    Yes,
173    No,
174    Empty,
175    /// Any of the pair separators `,` `.` `!` `?`.
176    Sep,
177    Str(String),
178    Num(Value),
179}
180
181fn lex(chars: &[char]) -> DogeResult<(Vec<Tok>, Vec<usize>)> {
182    let mut lexer = Lexer { chars, pos: 0 };
183    let mut toks = Vec::new();
184    let mut offs = Vec::new();
185    loop {
186        lexer.skip_ws();
187        let off = lexer.pos;
188        let Some(c) = lexer.peek() else { break };
189        let tok = match c {
190            '"' => Tok::Str(lexer.string()?),
191            '-' => lexer.number()?,
192            c if c.is_ascii_digit() => lexer.number()?,
193            ',' | '.' | '!' | '?' => {
194                lexer.pos += 1;
195                Tok::Sep
196            }
197            c if c.is_ascii_alphabetic() => lexer.word()?,
198            _ => return Err(lexer.error("unexpected character")),
199        };
200        toks.push(tok);
201        offs.push(off);
202    }
203    Ok((toks, offs))
204}
205
206struct Lexer<'a> {
207    chars: &'a [char],
208    pos: usize,
209}
210
211impl Lexer<'_> {
212    fn error(&self, what: &str) -> DogeError {
213        DogeError::value_error(format!(
214            "dson.parse: much invalid. {what} at offset {}",
215            self.pos
216        ))
217    }
218
219    fn peek(&self) -> Option<char> {
220        self.chars.get(self.pos).copied()
221    }
222
223    fn peek_at(&self, ahead: usize) -> Option<char> {
224        self.chars.get(self.pos + ahead).copied()
225    }
226
227    fn skip_ws(&mut self) {
228        while matches!(self.peek(), Some(' ' | '\t' | '\n' | '\r')) {
229            self.pos += 1;
230        }
231    }
232
233    fn starts_with(&self, word: &str) -> bool {
234        word.chars()
235            .enumerate()
236            .all(|(i, ch)| self.peek_at(i) == Some(ch))
237    }
238
239    fn word(&mut self) -> DogeResult<Tok> {
240        let start = self.pos;
241        while matches!(self.peek(), Some(c) if c.is_ascii_alphabetic()) {
242            self.pos += 1;
243        }
244        let word: String = self.chars[start..self.pos].iter().collect();
245        match word.as_str() {
246            "such" => Ok(Tok::Such),
247            "wow" => Ok(Tok::Wow),
248            "so" => Ok(Tok::So),
249            "many" => Ok(Tok::Many),
250            "is" => Ok(Tok::Is),
251            "and" => Ok(Tok::And),
252            "also" => Ok(Tok::Also),
253            "yes" => Ok(Tok::Yes),
254            "no" => Ok(Tok::No),
255            "empty" => Ok(Tok::Empty),
256            _ => Err(DogeError::value_error(format!(
257                "dson.parse: much invalid. unexpected word '{word}' at offset {start}"
258            ))),
259        }
260    }
261
262    fn string(&mut self) -> DogeResult<String> {
263        self.pos += 1; // consume opening '"'
264        let mut s = String::new();
265        loop {
266            match self.peek() {
267                None => return Err(self.error("unterminated string")),
268                Some('"') => {
269                    self.pos += 1;
270                    return Ok(s);
271                }
272                Some('\\') => {
273                    self.pos += 1;
274                    self.escape(&mut s)?;
275                }
276                Some(c) if (c as u32) < 0x20 => {
277                    return Err(self.error("control character in a string"))
278                }
279                Some(c) => {
280                    s.push(c);
281                    self.pos += 1;
282                }
283            }
284        }
285    }
286
287    fn escape(&mut self, s: &mut String) -> DogeResult<()> {
288        match self.peek() {
289            Some('"') => s.push('"'),
290            Some('\\') => s.push('\\'),
291            Some('/') => s.push('/'),
292            Some('b') => s.push('\u{08}'),
293            Some('f') => s.push('\u{0c}'),
294            Some('n') => s.push('\n'),
295            Some('r') => s.push('\r'),
296            Some('t') => s.push('\t'),
297            Some('u') => {
298                self.pos += 1;
299                return self.unicode_escape(s);
300            }
301            _ => return Err(self.error("invalid escape")),
302        }
303        self.pos += 1;
304        Ok(())
305    }
306
307    /// A `\uOOOOOO` escape — six octal digits naming one code point (the leading
308    /// `\u` already consumed).
309    fn unicode_escape(&mut self, s: &mut String) -> DogeResult<()> {
310        let mut cp = 0u32;
311        for _ in 0..6 {
312            match self.peek().and_then(|c| c.to_digit(8)) {
313                Some(d) => {
314                    cp = cp * 8 + d;
315                    self.pos += 1;
316                }
317                None => return Err(self.error("expected six octal digits after \\u")),
318            }
319        }
320        match char::from_u32(cp) {
321            Some(c) => {
322                s.push(c);
323                Ok(())
324            }
325            None => Err(self.error("invalid code point in a \\u escape")),
326        }
327    }
328
329    /// A DSON number in octal: `-?` octal digits, an optional `.`-fraction, and an
330    /// optional `very`/`VERY` exponent (meaning × 8^n). Integral, fraction-free,
331    /// exponent-free, and in `i64` range → Int; anything else → Float.
332    fn number(&mut self) -> DogeResult<Tok> {
333        let neg = self.peek() == Some('-');
334        if neg {
335            self.pos += 1;
336        }
337        let mut int_digits = Vec::new();
338        self.octal_digits(&mut int_digits);
339        if int_digits.is_empty() {
340            return Err(self.error("expected an octal digit"));
341        }
342        if matches!(self.peek(), Some('8' | '9')) {
343            return Err(self.error("not an octal digit (DSON numbers are base 8)"));
344        }
345
346        let mut is_float = false;
347        let mut frac_digits = Vec::new();
348        if self.peek() == Some('.') && matches!(self.peek_at(1), Some('0'..='7')) {
349            is_float = true;
350            self.pos += 1; // consume '.'
351            self.octal_digits(&mut frac_digits);
352        }
353
354        let mut exponent = 0i32;
355        if self.starts_with("very") || self.starts_with("VERY") {
356            is_float = true;
357            self.pos += 4;
358            let esign = match self.peek() {
359                Some('+') => {
360                    self.pos += 1;
361                    1
362                }
363                Some('-') => {
364                    self.pos += 1;
365                    -1
366                }
367                _ => 1,
368            };
369            let mut exp_digits = Vec::new();
370            self.octal_digits(&mut exp_digits);
371            if exp_digits.is_empty() {
372                return Err(self.error("expected an octal digit in the exponent"));
373            }
374            let magnitude = exp_digits.iter().fold(0i32, |acc, &d| acc * 8 + d as i32);
375            exponent = esign * magnitude;
376        }
377
378        if !is_float {
379            if let Some(n) = octal_to_i64(&int_digits, neg) {
380                return Ok(Tok::Num(Value::Int(n)));
381            }
382        }
383        Ok(Tok::Num(Value::Float(octal_to_f64(
384            &int_digits,
385            &frac_digits,
386            exponent,
387            neg,
388        ))))
389    }
390
391    fn octal_digits(&mut self, out: &mut Vec<u32>) {
392        while let Some(c) = self.peek() {
393            match c.to_digit(8) {
394                Some(d) => {
395                    out.push(d);
396                    self.pos += 1;
397                }
398                None => break,
399            }
400        }
401    }
402}
403
404/// Fold octal digits into a signed `i64`, or `None` if they overflow its range.
405fn octal_to_i64(digits: &[u32], neg: bool) -> Option<i64> {
406    let mut acc: i128 = 0;
407    for &d in digits {
408        acc = acc.checked_mul(8)?.checked_add(d as i128)?;
409    }
410    let signed = if neg { -acc } else { acc };
411    i64::try_from(signed).ok()
412}
413
414/// Combine octal integer digits, octal fraction digits, and a base-8 exponent
415/// into an `f64`.
416fn octal_to_f64(int_digits: &[u32], frac_digits: &[u32], exponent: i32, neg: bool) -> f64 {
417    let mut mag = int_digits
418        .iter()
419        .fold(0.0f64, |acc, &d| acc * 8.0 + d as f64);
420    if !frac_digits.is_empty() {
421        let frac = frac_digits
422            .iter()
423            .fold(0.0f64, |acc, &d| acc * 8.0 + d as f64);
424        mag += frac / 8f64.powi(frac_digits.len() as i32);
425    }
426    if exponent != 0 {
427        mag *= 8f64.powi(exponent);
428    }
429    if neg {
430        -mag
431    } else {
432        mag
433    }
434}
435
436struct Parser {
437    toks: Vec<Tok>,
438    offs: Vec<usize>,
439    end: usize,
440    pos: usize,
441}
442
443impl Parser {
444    fn error(&self, what: &str) -> DogeError {
445        let off = self.offs.get(self.pos).copied().unwrap_or(self.end);
446        DogeError::value_error(format!("dson.parse: much invalid. {what} at offset {off}"))
447    }
448
449    fn peek(&self) -> Option<&Tok> {
450        self.toks.get(self.pos)
451    }
452
453    fn value(&mut self, depth: usize) -> DogeResult {
454        if depth >= MAX_DEPTH {
455            return Err(too_deep("dson", "parse"));
456        }
457        match self.peek() {
458            Some(Tok::Such) => self.object(depth),
459            Some(Tok::So) => self.array(depth),
460            Some(Tok::Str(s)) => {
461                let s = s.clone();
462                self.pos += 1;
463                Ok(Value::str(s))
464            }
465            Some(Tok::Num(v)) => {
466                let v = v.clone();
467                self.pos += 1;
468                Ok(v)
469            }
470            Some(Tok::Yes) => {
471                self.pos += 1;
472                Ok(Value::Bool(true))
473            }
474            Some(Tok::No) => {
475                self.pos += 1;
476                Ok(Value::Bool(false))
477            }
478            Some(Tok::Empty) => {
479                self.pos += 1;
480                Ok(Value::None)
481            }
482            _ => Err(self.error("expected a value")),
483        }
484    }
485
486    fn object(&mut self, depth: usize) -> DogeResult {
487        self.pos += 1; // consume 'such'
488        let mut map = OrderedMap::new();
489        if matches!(self.peek(), Some(Tok::Wow)) {
490            self.pos += 1;
491            return Ok(Value::dict(map));
492        }
493        loop {
494            let key = match self.peek() {
495                Some(Tok::Str(s)) => {
496                    let s = s.clone();
497                    self.pos += 1;
498                    s
499                }
500                _ => return Err(self.error("expected a string key")),
501            };
502            if !matches!(self.peek(), Some(Tok::Is)) {
503                return Err(self.error("expected 'is'"));
504            }
505            self.pos += 1;
506            let val = self.value(depth + 1)?;
507            map.insert(key, val);
508            match self.peek() {
509                Some(Tok::Sep) => self.pos += 1,
510                Some(Tok::Wow) => {
511                    self.pos += 1;
512                    return Ok(Value::dict(map));
513                }
514                _ => return Err(self.error("expected a separator or 'wow'")),
515            }
516        }
517    }
518
519    fn array(&mut self, depth: usize) -> DogeResult {
520        self.pos += 1; // consume 'so'
521        let mut items = Vec::new();
522        if matches!(self.peek(), Some(Tok::Many)) {
523            self.pos += 1;
524            return Ok(Value::list(items));
525        }
526        loop {
527            items.push(self.value(depth + 1)?);
528            match self.peek() {
529                Some(Tok::And | Tok::Also) => self.pos += 1,
530                Some(Tok::Many) => {
531                    self.pos += 1;
532                    return Ok(Value::list(items));
533                }
534                _ => return Err(self.error("expected 'and', 'also', or 'many'")),
535            }
536        }
537    }
538}
539
540#[cfg(test)]
541mod tests {
542    use super::*;
543    use crate::error::ErrorKind;
544
545    fn parse(text: &str) -> DogeResult {
546        dson_parse(&Value::str(text))
547    }
548
549    fn emit_str(v: &Value) -> String {
550        match dson_emit(v).unwrap() {
551            Value::Str(s) => s.to_string(),
552            other => panic!("expected a Str, got {other:?}"),
553        }
554    }
555
556    #[test]
557    fn literals_map_to_doge_values() {
558        assert!(matches!(parse("yes").unwrap(), Value::Bool(true)));
559        assert!(matches!(parse("no").unwrap(), Value::Bool(false)));
560        assert!(matches!(parse("empty").unwrap(), Value::None));
561    }
562
563    #[test]
564    fn numbers_are_octal() {
565        assert!(matches!(parse("17620").unwrap(), Value::Int(8080)));
566        assert!(matches!(parse("-12").unwrap(), Value::Int(-10)));
567        // 4very2 = 4 * 8^2 = 256.
568        assert!(matches!(parse("4very2").unwrap(), Value::Float(x) if x == 256.0));
569        // 1very-1 = 1 * 8^-1 = 0.125.
570        assert!(matches!(parse("1very-1").unwrap(), Value::Float(x) if x == 0.125));
571    }
572
573    #[test]
574    fn octal_fractions_round_trip() {
575        // 0.5 == 4/8 == octal 0.4; a plain integer emits without a point.
576        assert!(matches!(parse("0.4").unwrap(), Value::Float(x) if x == 0.5));
577        assert_eq!(emit_str(&Value::Float(0.5)), "0.4");
578        assert_eq!(emit_str(&Value::Float(2.5)), "2.4");
579        assert_eq!(emit_str(&Value::Int(8080)), "17620");
580    }
581
582    #[test]
583    fn a_whole_float_keeps_its_point_so_it_stays_a_float() {
584        assert_eq!(emit_str(&Value::Float(3.0)), "3.0");
585        assert!(matches!(parse("3.0").unwrap(), Value::Float(x) if x == 3.0));
586    }
587
588    #[test]
589    fn all_pair_separators_and_array_joiners_are_accepted() {
590        let d =
591            parse("such \"a\" is 1, \"b\" is 2. \"c\" is 3! \"d\" is 4? \"e\" is 5 wow").unwrap();
592        assert_eq!(
593            emit_str(&d),
594            "such \"a\" is 1, \"b\" is 2, \"c\" is 3, \"d\" is 4, \"e\" is 5 wow"
595        );
596        let a = parse("so 1 and 2 also 3 many").unwrap();
597        assert_eq!(emit_str(&a), "so 1 and 2 and 3 many");
598    }
599
600    #[test]
601    fn empty_containers_round_trip() {
602        assert_eq!(emit_str(&parse("such wow").unwrap()), "such wow");
603        assert_eq!(emit_str(&parse("so many").unwrap()), "so many");
604    }
605
606    #[test]
607    fn nested_structure_round_trips() {
608        let src = "such \"name\" is \"kabosu\", \"tags\" is so \"doge\" and \"shibe\" many, \"good\" is yes wow";
609        assert_eq!(emit_str(&parse(src).unwrap()), src);
610    }
611
612    #[test]
613    fn six_octal_digit_unicode_escape_decodes() {
614        // U+1F415 DOG == octal 372025.
615        assert!(matches!(parse("\"\\u372025\"").unwrap(), Value::Str(s) if &*s == "🐕"));
616    }
617
618    #[test]
619    fn a_non_octal_digit_is_a_catchable_error() {
620        assert_eq!(parse("18").unwrap_err().kind, ErrorKind::ValueError);
621    }
622
623    #[test]
624    fn trailing_garbage_is_a_catchable_error() {
625        assert_eq!(
626            parse("so 1 many wow").unwrap_err().kind,
627            ErrorKind::ValueError
628        );
629    }
630
631    #[test]
632    fn deep_nesting_is_a_catchable_error_not_a_stack_overflow() {
633        let deep = "so ".repeat(MAX_DEPTH + 5);
634        assert_eq!(parse(&deep).unwrap_err().kind, ErrorKind::ValueError);
635    }
636
637    #[test]
638    fn a_non_finite_float_cannot_be_emitted() {
639        assert_eq!(
640            dson_emit(&Value::Float(f64::INFINITY)).unwrap_err().kind,
641            ErrorKind::ValueError
642        );
643    }
644
645    #[test]
646    fn an_unsupported_type_is_a_catchable_type_error() {
647        let sock = Value::function(0, "f", vec![]);
648        assert_eq!(dson_emit(&sock).unwrap_err().kind, ErrorKind::TypeError);
649    }
650
651    #[test]
652    fn a_non_str_argument_to_parse_is_a_type_error() {
653        assert_eq!(
654            dson_parse(&Value::Int(1)).unwrap_err().kind,
655            ErrorKind::TypeError
656        );
657    }
658}