Skip to main content

corium_query/
edn.rs

1//! Minimal EDN reader/printer for the query boundary.
2//!
3//! The engine carries this small self-contained reader covering the subset
4//! used by queries, transaction forms, and the conformance corpus:
5//! collections, keywords, symbols, strings, numbers, booleans, `nil`, sets,
6//! comments, `#_` discards, and tagged elements. Text arriving through a
7//! cljrs boundary is read by `cljrs-reader` instead and bridged onto this
8//! representation (`corium_cljrs::convert::read_edn`, M5), keeping one EDN
9//! implementation at the boundary while the engine core stays dependency-free.
10
11use std::fmt;
12
13use corium_core::{Keyword, TotalF64};
14use thiserror::Error;
15
16/// An EDN value with total ordering (maps/sets are normalized sorted).
17#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd)]
18pub enum Edn {
19    /// `nil`.
20    Nil,
21    /// Boolean.
22    Bool(bool),
23    /// Integer.
24    Long(i64),
25    /// Floating point (total order).
26    Double(TotalF64),
27    /// String.
28    Str(String),
29    /// Keyword.
30    Keyword(Keyword),
31    /// Symbol, printed verbatim.
32    Symbol(String),
33    /// List `( … )`.
34    List(Vec<Edn>),
35    /// Vector `[ … ]`.
36    Vector(Vec<Edn>),
37    /// Map `{ … }` as sorted key/value pairs.
38    Map(Vec<(Edn, Edn)>),
39    /// Set `#{ … }` as sorted, deduplicated elements.
40    Set(Vec<Edn>),
41    /// Tagged element `#tag value`.
42    Tagged(String, Box<Edn>),
43}
44
45impl Edn {
46    /// Builds a symbol.
47    #[must_use]
48    pub fn symbol(text: &str) -> Self {
49        Self::Symbol(text.to_owned())
50    }
51
52    /// Builds a keyword from `"ns/name"` or `"name"` text (no leading colon).
53    #[must_use]
54    pub fn keyword(text: &str) -> Self {
55        Self::Keyword(Keyword::parse(text))
56    }
57
58    /// Returns the symbol text if this is a symbol.
59    #[must_use]
60    pub fn as_symbol(&self) -> Option<&str> {
61        match self {
62            Self::Symbol(s) => Some(s),
63            _ => None,
64        }
65    }
66
67    /// Returns the keyword if this is a keyword.
68    #[must_use]
69    pub const fn as_keyword(&self) -> Option<&Keyword> {
70        match self {
71            Self::Keyword(k) => Some(k),
72            _ => None,
73        }
74    }
75
76    /// Returns the elements if this is a list or vector.
77    #[must_use]
78    pub fn as_seq(&self) -> Option<&[Edn]> {
79        match self {
80            Self::List(items) | Self::Vector(items) => Some(items),
81            _ => None,
82        }
83    }
84
85    /// Looks up a map value by key.
86    #[must_use]
87    pub fn get(&self, key: &Self) -> Option<&Self> {
88        match self {
89            Self::Map(pairs) => pairs.iter().find(|(k, _)| k == key).map(|(_, v)| v),
90            _ => None,
91        }
92    }
93}
94
95/// EDN read failure.
96#[derive(Debug, Error, Eq, PartialEq)]
97#[error("EDN parse error at offset {offset}: {message}")]
98pub struct EdnError {
99    /// Byte offset of the failure.
100    pub offset: usize,
101    /// Failure description.
102    pub message: String,
103}
104
105/// Parses exactly one EDN form from `input` (trailing whitespace allowed).
106///
107/// # Errors
108/// Returns [`EdnError`] on malformed input or trailing data.
109pub fn read_one(input: &str) -> Result<Edn, EdnError> {
110    let mut reader = Reader::new(input);
111    let form = reader.read_form()?;
112    reader.skip_ws();
113    if reader.peek().is_some() {
114        return Err(reader.error("trailing data after form"));
115    }
116    Ok(form)
117}
118
119/// Parses every top-level EDN form in `input`.
120///
121/// # Errors
122/// Returns [`EdnError`] on malformed input.
123pub fn read_all(input: &str) -> Result<Vec<Edn>, EdnError> {
124    let mut reader = Reader::new(input);
125    let mut forms = Vec::new();
126    loop {
127        reader.skip_ws();
128        if reader.peek().is_none() {
129            return Ok(forms);
130        }
131        forms.push(reader.read_form()?);
132    }
133}
134
135struct Reader<'a> {
136    bytes: &'a [u8],
137    pos: usize,
138}
139
140const DELIMITERS: &[u8] = b"()[]{}\"; \t\r\n,";
141
142impl<'a> Reader<'a> {
143    fn new(input: &'a str) -> Self {
144        Self {
145            bytes: input.as_bytes(),
146            pos: 0,
147        }
148    }
149
150    fn error(&self, message: &str) -> EdnError {
151        EdnError {
152            offset: self.pos,
153            message: message.to_owned(),
154        }
155    }
156
157    fn peek(&self) -> Option<u8> {
158        self.bytes.get(self.pos).copied()
159    }
160
161    fn bump(&mut self) -> Option<u8> {
162        let b = self.peek()?;
163        self.pos += 1;
164        Some(b)
165    }
166
167    fn skip_ws(&mut self) {
168        while let Some(b) = self.peek() {
169            match b {
170                b' ' | b'\t' | b'\r' | b'\n' | b',' => {
171                    self.pos += 1;
172                }
173                b';' => {
174                    while self.peek().is_some_and(|b| b != b'\n') {
175                        self.pos += 1;
176                    }
177                }
178                _ => return,
179            }
180        }
181    }
182
183    fn read_form(&mut self) -> Result<Edn, EdnError> {
184        self.skip_ws();
185        match self.peek().ok_or_else(|| self.error("unexpected end"))? {
186            b'(' => {
187                self.pos += 1;
188                Ok(Edn::List(self.read_until(b')')?))
189            }
190            b'[' => {
191                self.pos += 1;
192                Ok(Edn::Vector(self.read_until(b']')?))
193            }
194            b'{' => {
195                self.pos += 1;
196                let items = self.read_until(b'}')?;
197                if items.len() % 2 != 0 {
198                    return Err(self.error("map requires an even number of forms"));
199                }
200                let mut pairs: Vec<(Edn, Edn)> = Vec::new();
201                let mut iter = items.into_iter();
202                while let (Some(k), Some(v)) = (iter.next(), iter.next()) {
203                    pairs.push((k, v));
204                }
205                pairs.sort_by(|left, right| left.0.cmp(&right.0));
206                Ok(Edn::Map(pairs))
207            }
208            b'"' => self.read_string(),
209            b'#' => self.read_dispatch(),
210            _ => self.read_atom(),
211        }
212    }
213
214    fn read_until(&mut self, close: u8) -> Result<Vec<Edn>, EdnError> {
215        let mut items = Vec::new();
216        loop {
217            self.skip_ws();
218            match self.peek() {
219                None => return Err(self.error("unterminated collection")),
220                Some(b) if b == close => {
221                    self.pos += 1;
222                    return Ok(items);
223                }
224                Some(_) => items.push(self.read_form()?),
225            }
226        }
227    }
228
229    fn read_dispatch(&mut self) -> Result<Edn, EdnError> {
230        self.pos += 1;
231        match self.peek() {
232            Some(b'{') => {
233                self.pos += 1;
234                let mut items = self.read_until(b'}')?;
235                items.sort();
236                items.dedup();
237                Ok(Edn::Set(items))
238            }
239            Some(b'_') => {
240                self.pos += 1;
241                let _discarded = self.read_form()?;
242                self.read_form()
243            }
244            _ => {
245                let tag = self.read_token()?;
246                if tag.is_empty() {
247                    return Err(self.error("empty dispatch tag"));
248                }
249                let value = self.read_form()?;
250                Ok(Edn::Tagged(tag, Box::new(value)))
251            }
252        }
253    }
254
255    fn read_string(&mut self) -> Result<Edn, EdnError> {
256        self.pos += 1;
257        let mut out = String::new();
258        loop {
259            match self
260                .bump()
261                .ok_or_else(|| self.error("unterminated string"))?
262            {
263                b'"' => return Ok(Edn::Str(out)),
264                b'\\' => {
265                    let escape = self
266                        .bump()
267                        .ok_or_else(|| self.error("unterminated escape"))?;
268                    out.push(match escape {
269                        b'n' => '\n',
270                        b't' => '\t',
271                        b'r' => '\r',
272                        b'"' => '"',
273                        b'\\' => '\\',
274                        _ => return Err(self.error("unknown string escape")),
275                    });
276                }
277                b => {
278                    // Re-decode multi-byte UTF-8 starting at this byte.
279                    let start = self.pos - 1;
280                    let mut end = self.pos;
281                    while end < self.bytes.len() && (self.bytes[end] & 0xC0) == 0x80 {
282                        end += 1;
283                    }
284                    if b < 0x80 {
285                        out.push(char::from(b));
286                    } else {
287                        let text = std::str::from_utf8(&self.bytes[start..end])
288                            .map_err(|_| self.error("invalid UTF-8"))?;
289                        out.push_str(text);
290                        self.pos = end;
291                    }
292                }
293            }
294        }
295    }
296
297    fn read_token(&mut self) -> Result<String, EdnError> {
298        let start = self.pos;
299        while self.peek().is_some_and(|b| !DELIMITERS.contains(&b)) {
300            self.pos += 1;
301        }
302        std::str::from_utf8(&self.bytes[start..self.pos])
303            .map(str::to_owned)
304            .map_err(|_| self.error("invalid UTF-8 token"))
305    }
306
307    fn read_atom(&mut self) -> Result<Edn, EdnError> {
308        let token = self.read_token()?;
309        if token.is_empty() {
310            return Err(self.error("unexpected character"));
311        }
312        if let Some(name) = token.strip_prefix(':') {
313            if name.is_empty() {
314                return Err(self.error("empty keyword"));
315            }
316            return Ok(Edn::keyword(name));
317        }
318        match token.as_str() {
319            "nil" => return Ok(Edn::Nil),
320            "true" => return Ok(Edn::Bool(true)),
321            "false" => return Ok(Edn::Bool(false)),
322            _ => {}
323        }
324        let numeric_start = token.starts_with(|c: char| c.is_ascii_digit())
325            || (token.len() > 1
326                && (token.starts_with('-') || token.starts_with('+'))
327                && token[1..].starts_with(|c: char| c.is_ascii_digit()));
328        if numeric_start {
329            if token.contains('.') || token.contains('e') || token.contains('E') {
330                return token
331                    .parse::<f64>()
332                    .map(|v| Edn::Double(TotalF64(v)))
333                    .map_err(|_| self.error("malformed float"));
334            }
335            return token
336                .parse::<i64>()
337                .map(Edn::Long)
338                .map_err(|_| self.error("malformed integer"));
339        }
340        Ok(Edn::Symbol(token))
341    }
342}
343
344impl fmt::Display for Edn {
345    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
346        fn write_seq(f: &mut fmt::Formatter<'_>, items: &[Edn]) -> fmt::Result {
347            for (i, item) in items.iter().enumerate() {
348                if i > 0 {
349                    write!(f, " ")?;
350                }
351                write!(f, "{item}")?;
352            }
353            Ok(())
354        }
355        match self {
356            Self::Nil => write!(f, "nil"),
357            Self::Bool(v) => write!(f, "{v}"),
358            Self::Long(v) => write!(f, "{v}"),
359            Self::Double(v) => write!(f, "{:?}", v.0),
360            Self::Str(v) => write!(f, "{v:?}"),
361            Self::Keyword(v) => write!(f, "{v}"),
362            Self::Symbol(v) => write!(f, "{v}"),
363            Self::List(items) => {
364                write!(f, "(")?;
365                write_seq(f, items)?;
366                write!(f, ")")
367            }
368            Self::Vector(items) => {
369                write!(f, "[")?;
370                write_seq(f, items)?;
371                write!(f, "]")
372            }
373            Self::Set(items) => {
374                write!(f, "#{{")?;
375                write_seq(f, items)?;
376                write!(f, "}}")
377            }
378            Self::Map(pairs) => {
379                write!(f, "{{")?;
380                for (i, (k, v)) in pairs.iter().enumerate() {
381                    if i > 0 {
382                        write!(f, ", ")?;
383                    }
384                    write!(f, "{k} {v}")?;
385                }
386                write!(f, "}}")
387            }
388            Self::Tagged(tag, value) => write!(f, "#{tag} {value}"),
389        }
390    }
391}
392
393#[cfg(test)]
394mod tests {
395    use super::*;
396
397    #[test]
398    fn reads_query_shapes() {
399        let form = read_one(
400            "{:find [?e (count ?x)] ; comment\n :where [[?e :person/name \"A\"] [(< ?x 3)]]}",
401        )
402        .expect("parse");
403        assert!(form.get(&Edn::keyword("find")).is_some());
404        assert!(form.to_string().contains(":person/name"));
405    }
406
407    #[test]
408    fn reads_sets_tags_and_discards() {
409        assert_eq!(
410            read_one("#{3 1 2 1}").expect("set"),
411            Edn::Set(vec![Edn::Long(1), Edn::Long(2), Edn::Long(3)])
412        );
413        assert_eq!(
414            read_one("#tempid \"a\"").expect("tag"),
415            Edn::Tagged("tempid".into(), Box::new(Edn::Str("a".into())))
416        );
417        assert_eq!(
418            read_one("[#_ 1 2]").expect("discard"),
419            Edn::Vector(vec![Edn::Long(2)])
420        );
421    }
422
423    #[test]
424    fn reads_numbers_and_negative_symbols() {
425        assert_eq!(read_one("-42").expect("int"), Edn::Long(-42));
426        assert_eq!(read_one("1.5").expect("float"), Edn::Double(TotalF64(1.5)));
427        assert_eq!(read_one("-").expect("minus"), Edn::symbol("-"));
428        assert_eq!(read_one("?e").expect("var"), Edn::symbol("?e"));
429    }
430}