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