Skip to main content

sim_codec/implementation/
domain_form.rs

1//! A generic `#(...)` domain-form parser and formatter.
2//!
3//! Several domain codecs (`music-shapes`, `sound-shapes`, ...) hand-rolled the
4//! same `#(Name key=value value [list,...])` reader. This is that grammar, with
5//! no domain knowledge:
6//!
7//! ```text
8//! form  = "#(" name item* ")"
9//! item  = key "=" value | value
10//! value = form | "[" value ("," value)* "]" | string | atom
11//! ```
12//!
13//! Output is ASCII. A domain crate parses with [`parse_domain_form`], reads
14//! fields with [`DomainForm::atom`]/[`string`](DomainForm::string)/
15//! [`list`](DomainForm::list), and (optionally) renders with
16//! [`format_domain_form`].
17
18use sim_kernel::{Expr, Symbol};
19
20/// A parsed domain-form value.
21#[derive(Clone, Debug, PartialEq)]
22pub enum DomainValue {
23    /// A nested `#(...)` form.
24    Form(DomainForm),
25    /// A `[...]` list.
26    List(Vec<DomainValue>),
27    /// A `"..."` string.
28    String(String),
29    /// A bare atom (number, identifier, `4/4`, ...).
30    Atom(String),
31}
32
33/// A parsed `#(name ...)` form: a name, keyed fields, and positional values.
34#[derive(Clone, Debug, PartialEq)]
35pub struct DomainForm {
36    /// The form name.
37    pub name: String,
38    /// Keyed `key=value` fields, in order.
39    pub fields: Vec<(String, DomainValue)>,
40    /// Positional (un-keyed) values, in order.
41    pub positional: Vec<DomainValue>,
42}
43
44impl DomainForm {
45    /// The value of keyed field `key`, if present.
46    pub fn field(&self, key: &str) -> Option<&DomainValue> {
47        self.fields
48            .iter()
49            .find_map(|(name, value)| (name == key).then_some(value))
50    }
51
52    /// The atom string of keyed field `key`.
53    pub fn atom(&self, key: &str) -> Result<&str, DomainFormError> {
54        match self.field(key) {
55            Some(DomainValue::Atom(value)) => Ok(value),
56            Some(_) => Err(DomainFormError::WrongFieldKind(key.to_owned())),
57            None => Err(DomainFormError::MissingField(key.to_owned())),
58        }
59    }
60
61    /// The string of keyed field `key`.
62    pub fn string(&self, key: &str) -> Result<&str, DomainFormError> {
63        match self.field(key) {
64            Some(DomainValue::String(value)) => Ok(value),
65            Some(_) => Err(DomainFormError::WrongFieldKind(key.to_owned())),
66            None => Err(DomainFormError::MissingField(key.to_owned())),
67        }
68    }
69
70    /// The list items of keyed field `key`.
71    pub fn list(&self, key: &str) -> Result<&[DomainValue], DomainFormError> {
72        match self.field(key) {
73            Some(DomainValue::List(items)) => Ok(items),
74            Some(_) => Err(DomainFormError::WrongFieldKind(key.to_owned())),
75            None => Err(DomainFormError::MissingField(key.to_owned())),
76        }
77    }
78
79    /// The nested form of keyed field `key`.
80    pub fn form(&self, key: &str) -> Result<&DomainForm, DomainFormError> {
81        match self.field(key) {
82            Some(DomainValue::Form(value)) => Ok(value),
83            Some(_) => Err(DomainFormError::WrongFieldKind(key.to_owned())),
84            None => Err(DomainFormError::MissingField(key.to_owned())),
85        }
86    }
87
88    /// The atom or string text of keyed field `key`.
89    ///
90    /// Accepts either a bare atom or a quoted string, returning the underlying
91    /// text in both cases (the reverse of the `#(...)` writer, which renders a
92    /// symbol as an atom and a string with quotes).
93    pub fn field_atom_or_string(&self, name: &str) -> Result<&str, DomainFormError> {
94        match self.field(name) {
95            Some(DomainValue::Atom(value) | DomainValue::String(value)) => Ok(value),
96            Some(_) => Err(DomainFormError::WrongFieldKind(name.to_owned())),
97            None => Err(DomainFormError::MissingField(name.to_owned())),
98        }
99    }
100
101    /// The rendered [`render_text`](DomainValue::render_text) form of keyed
102    /// field `name`.
103    ///
104    /// Errors with [`MissingField`](DomainFormError::MissingField) when the
105    /// field is absent; any present value renders (an atom to its text, a
106    /// string to its quoted form, a list or nested form to its `#(...)`/`[...]`
107    /// text).
108    pub fn field_text(&self, name: &str) -> Result<String, DomainFormError> {
109        match self.field(name) {
110            Some(value) => Ok(value.render_text()),
111            None => Err(DomainFormError::MissingField(name.to_owned())),
112        }
113    }
114
115    /// Project this parsed form to an [`Expr::Map`] for shape validation.
116    ///
117    /// The map always contains `form` with the form name. Positional values are
118    /// carried as `args` only when present. Keyed fields keep their field names
119    /// as symbol keys, and values are projected with [`DomainValue::to_expr`].
120    pub fn to_expr_map(&self) -> Expr {
121        let mut entries = vec![(
122            Expr::Symbol(Symbol::new("form")),
123            Expr::String(self.name.clone()),
124        )];
125
126        if !self.positional.is_empty() {
127            entries.push((
128                Expr::Symbol(Symbol::new("args")),
129                Expr::List(self.positional.iter().map(DomainValue::to_expr).collect()),
130            ));
131        }
132
133        entries.extend(
134            self.fields
135                .iter()
136                .map(|(key, value)| (Expr::Symbol(Symbol::new(key.clone())), value.to_expr())),
137        );
138
139        Expr::Map(entries)
140    }
141}
142
143impl DomainValue {
144    /// The nested form, if this value is a `#(...)` form.
145    ///
146    /// Errors with [`WrongValueKind`](DomainFormError::WrongValueKind) for a
147    /// list, string, or atom.
148    pub fn as_form(&self) -> Result<&DomainForm, DomainFormError> {
149        match self {
150            DomainValue::Form(form) => Ok(form),
151            _ => Err(DomainFormError::WrongValueKind),
152        }
153    }
154
155    /// The text of this value when it is a bare atom or a quoted string.
156    ///
157    /// Errors with [`WrongValueKind`](DomainFormError::WrongValueKind) for a
158    /// list or nested form.
159    pub fn atom_or_string(&self) -> Result<&str, DomainFormError> {
160        match self {
161            DomainValue::Atom(value) | DomainValue::String(value) => Ok(value),
162            _ => Err(DomainFormError::WrongValueKind),
163        }
164    }
165
166    /// Renders this value back to its ASCII `#(...)` source text: an atom to
167    /// its literal text, a string to its escaped quoted form, a list to
168    /// `[a,b,...]`, and a nested form via [`format_domain_form`].
169    ///
170    /// Round-trips through [`parse_domain_form`] for a top-level form value.
171    pub fn render_text(&self) -> String {
172        match self {
173            DomainValue::Form(form) => format_domain_form(form),
174            DomainValue::List(items) => format!(
175                "[{}]",
176                items
177                    .iter()
178                    .map(DomainValue::render_text)
179                    .collect::<Vec<_>>()
180                    .join(",")
181            ),
182            DomainValue::String(value) => {
183                format!("\"{}\"", value.replace('\\', "\\\\").replace('"', "\\\""))
184            }
185            DomainValue::Atom(value) => value.clone(),
186        }
187    }
188
189    /// Project this domain-form value to an expression map/list/string tree.
190    ///
191    /// Atoms become strings so their original text is preserved rather than
192    /// interpreted as a symbol or number.
193    pub fn to_expr(&self) -> Expr {
194        match self {
195            DomainValue::Form(form) => form.to_expr_map(),
196            DomainValue::List(items) => {
197                Expr::List(items.iter().map(DomainValue::to_expr).collect())
198            }
199            DomainValue::String(value) | DomainValue::Atom(value) => Expr::String(value.clone()),
200        }
201    }
202}
203
204/// A domain-form parse or access error.
205#[derive(Clone, Debug, PartialEq, Eq)]
206pub enum DomainFormError {
207    /// Input did not start with `#(`.
208    ExpectedForm,
209    /// Input ended mid-form.
210    UnexpectedEof,
211    /// An invalid character was found where a token was expected.
212    InvalidToken,
213    /// A form repeated a field key.
214    DuplicateField(String),
215    /// Extra input followed the top-level form.
216    TrailingInput,
217    /// A required field was missing.
218    MissingField(String),
219    /// A field had the wrong value kind.
220    WrongFieldKind(String),
221    /// A value had the wrong kind for a keyless accessor (not the atom,
222    /// string, or form that was expected).
223    WrongValueKind,
224}
225
226/// Parse a top-level `#(...)` domain form.
227///
228/// # Examples
229///
230/// ```
231/// use sim_codec::{parse_domain_form, DomainValue};
232///
233/// let form = parse_domain_form("#(Note dur=4/4 60 64)").unwrap();
234/// assert_eq!(form.name, "Note");
235/// assert_eq!(form.atom("dur").unwrap(), "4/4");
236/// assert_eq!(
237///     form.positional,
238///     vec![DomainValue::Atom("60".into()), DomainValue::Atom("64".into())],
239/// );
240/// ```
241pub fn parse_domain_form(input: &str) -> Result<DomainForm, DomainFormError> {
242    let mut parser = Parser { input, index: 0 };
243    parser.skip_ws();
244    if !parser.consume_str("#(") {
245        return Err(DomainFormError::ExpectedForm);
246    }
247    let form = parser.parse_form_body()?;
248    parser.skip_ws();
249    if parser.index != parser.input.len() {
250        return Err(DomainFormError::TrailingInput);
251    }
252    Ok(form)
253}
254
255/// Render a domain form as an ASCII `#(...)` string. Round-trips through
256/// [`parse_domain_form`].
257///
258/// # Examples
259///
260/// ```
261/// use sim_codec::{format_domain_form, parse_domain_form};
262///
263/// let source = "#(Note dur=4/4 pitches=[60,64])";
264/// let form = parse_domain_form(source).unwrap();
265/// let rendered = format_domain_form(&form);
266/// assert_eq!(parse_domain_form(&rendered).unwrap(), form);
267/// ```
268pub fn format_domain_form(form: &DomainForm) -> String {
269    let mut out = String::from("#(");
270    out.push_str(&form.name);
271    for value in &form.positional {
272        out.push(' ');
273        format_value(value, &mut out);
274    }
275    for (key, value) in &form.fields {
276        out.push(' ');
277        out.push_str(key);
278        out.push('=');
279        format_value(value, &mut out);
280    }
281    out.push(')');
282    out
283}
284
285fn format_value(value: &DomainValue, out: &mut String) {
286    match value {
287        DomainValue::Form(form) => out.push_str(&format_domain_form(form)),
288        DomainValue::List(items) => {
289            out.push('[');
290            for (index, item) in items.iter().enumerate() {
291                if index > 0 {
292                    out.push(',');
293                }
294                format_value(item, out);
295            }
296            out.push(']');
297        }
298        DomainValue::String(text) => {
299            out.push('"');
300            for ch in text.chars() {
301                if ch == '\\' || ch == '"' {
302                    out.push('\\');
303                }
304                out.push(ch);
305            }
306            out.push('"');
307        }
308        DomainValue::Atom(text) => out.push_str(text),
309    }
310}
311
312struct Parser<'a> {
313    input: &'a str,
314    index: usize,
315}
316
317impl Parser<'_> {
318    fn parse_form_body(&mut self) -> Result<DomainForm, DomainFormError> {
319        let name = self.parse_ident()?;
320        let mut fields: Vec<(String, DomainValue)> = Vec::new();
321        let mut positional = Vec::new();
322        loop {
323            self.skip_ws();
324            if self.consume_char(')') {
325                break;
326            }
327            match self.peek_char() {
328                Some('#') | Some('[') | Some('"') => positional.push(self.parse_value()?),
329                _ => {
330                    let atom = self.parse_atom()?;
331                    if self.consume_char('=') {
332                        if fields.iter().any(|(key, _)| key == &atom) {
333                            return Err(DomainFormError::DuplicateField(atom));
334                        }
335                        fields.push((atom, self.parse_value()?));
336                    } else {
337                        positional.push(DomainValue::Atom(atom));
338                    }
339                }
340            }
341        }
342        Ok(DomainForm {
343            name,
344            fields,
345            positional,
346        })
347    }
348
349    fn parse_value(&mut self) -> Result<DomainValue, DomainFormError> {
350        self.skip_ws();
351        if self.consume_str("#(") {
352            return self.parse_form_body().map(DomainValue::Form);
353        }
354        if self.consume_char('[') {
355            return self.parse_list();
356        }
357        if self.peek_char() == Some('"') {
358            return self.parse_string().map(DomainValue::String);
359        }
360        self.parse_atom().map(DomainValue::Atom)
361    }
362
363    fn parse_list(&mut self) -> Result<DomainValue, DomainFormError> {
364        let mut items = Vec::new();
365        loop {
366            self.skip_ws();
367            if self.consume_char(']') {
368                break;
369            }
370            items.push(self.parse_value()?);
371            self.skip_ws();
372            if self.consume_char(',') {
373                continue;
374            }
375            self.expect_char(']')?;
376            break;
377        }
378        Ok(DomainValue::List(items))
379    }
380
381    fn parse_string(&mut self) -> Result<String, DomainFormError> {
382        self.expect_char('"')?;
383        let mut out = String::new();
384        while let Some(ch) = self.next_char() {
385            match ch {
386                '"' => return Ok(out),
387                '\\' => out.push(self.next_char().ok_or(DomainFormError::UnexpectedEof)?),
388                other => out.push(other),
389            }
390        }
391        Err(DomainFormError::UnexpectedEof)
392    }
393
394    fn parse_atom(&mut self) -> Result<String, DomainFormError> {
395        let start = self.index;
396        while let Some(ch) = self.peek_char() {
397            if ch.is_whitespace() || [',', ')', ']', '='].contains(&ch) {
398                break;
399            }
400            self.index += ch.len_utf8();
401        }
402        if self.index == start {
403            return Err(DomainFormError::UnexpectedEof);
404        }
405        Ok(self.input[start..self.index].to_owned())
406    }
407
408    fn parse_ident(&mut self) -> Result<String, DomainFormError> {
409        let atom = self.parse_atom()?;
410        if atom
411            .chars()
412            .all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '-')
413        {
414            Ok(atom)
415        } else {
416            Err(DomainFormError::InvalidToken)
417        }
418    }
419
420    fn expect_char(&mut self, expected: char) -> Result<(), DomainFormError> {
421        match self.next_char() {
422            Some(ch) if ch == expected => Ok(()),
423            Some(_) => Err(DomainFormError::InvalidToken),
424            None => Err(DomainFormError::UnexpectedEof),
425        }
426    }
427
428    fn consume_char(&mut self, expected: char) -> bool {
429        if self.peek_char() == Some(expected) {
430            self.index += expected.len_utf8();
431            true
432        } else {
433            false
434        }
435    }
436
437    fn consume_str(&mut self, expected: &str) -> bool {
438        if self.input[self.index..].starts_with(expected) {
439            self.index += expected.len();
440            true
441        } else {
442            false
443        }
444    }
445
446    fn skip_ws(&mut self) {
447        while let Some(ch) = self.peek_char() {
448            if ch.is_whitespace() {
449                self.index += ch.len_utf8();
450            } else {
451                break;
452            }
453        }
454    }
455
456    fn peek_char(&self) -> Option<char> {
457        self.input[self.index..].chars().next()
458    }
459
460    fn next_char(&mut self) -> Option<char> {
461        let ch = self.peek_char()?;
462        self.index += ch.len_utf8();
463        Some(ch)
464    }
465}
466
467#[cfg(test)]
468mod tests {
469    use super::*;
470
471    #[test]
472    fn parses_keyed_form_with_list_and_nested_form() {
473        let form = parse_domain_form("#(Note dur=4/4 pitch=C4 tags=[a,b] inner=#(Rest dur=1/4))")
474            .expect("parse");
475        assert_eq!(form.name, "Note");
476        assert_eq!(form.atom("dur").unwrap(), "4/4");
477        assert_eq!(form.atom("pitch").unwrap(), "C4");
478        assert_eq!(
479            form.list("tags").unwrap(),
480            &[
481                DomainValue::Atom("a".to_owned()),
482                DomainValue::Atom("b".to_owned())
483            ]
484        );
485        assert_eq!(form.form("inner").unwrap().name, "Rest");
486    }
487
488    #[test]
489    fn parses_positional_values() {
490        let form = parse_domain_form("#(Chord 60 64 67)").expect("parse");
491        assert_eq!(form.positional.len(), 3);
492        assert!(form.fields.is_empty());
493    }
494
495    #[test]
496    fn round_trips_through_format() {
497        let source = "#(Note dur=4/4 sym=\"a\\\"b\" pitches=[60,64])";
498        let form = parse_domain_form(source).expect("parse");
499        let rendered = format_domain_form(&form);
500        assert_eq!(parse_domain_form(&rendered).unwrap(), form);
501    }
502
503    #[test]
504    fn missing_close_paren_is_unexpected_eof() {
505        assert_eq!(
506            parse_domain_form("#(Note dur=4/4"),
507            Err(DomainFormError::UnexpectedEof)
508        );
509    }
510
511    #[test]
512    fn duplicate_field_is_rejected() {
513        assert_eq!(
514            parse_domain_form("#(Note dur=4/4 dur=1/2)"),
515            Err(DomainFormError::DuplicateField("dur".to_owned()))
516        );
517    }
518
519    #[test]
520    fn trailing_input_is_rejected() {
521        assert_eq!(
522            parse_domain_form("#(Note dur=4/4) extra"),
523            Err(DomainFormError::TrailingInput)
524        );
525    }
526
527    #[test]
528    fn non_form_input_is_rejected() {
529        assert_eq!(
530            parse_domain_form("Note"),
531            Err(DomainFormError::ExpectedForm)
532        );
533    }
534
535    #[test]
536    fn escaped_quotes_round_trip() {
537        let form = parse_domain_form("#(S v=\"he said \\\"hi\\\"\")").expect("parse");
538        assert_eq!(form.string("v").unwrap(), "he said \"hi\"");
539    }
540
541    #[test]
542    fn field_atom_or_string_accepts_both_kinds() {
543        let form = parse_domain_form("#(N a=bare s=\"quoted\")").expect("parse");
544        assert_eq!(form.field_atom_or_string("a").unwrap(), "bare");
545        assert_eq!(form.field_atom_or_string("s").unwrap(), "quoted");
546    }
547
548    #[test]
549    fn field_atom_or_string_rejects_list_and_missing() {
550        let form = parse_domain_form("#(N tags=[a,b])").expect("parse");
551        assert_eq!(
552            form.field_atom_or_string("tags"),
553            Err(DomainFormError::WrongFieldKind("tags".to_owned()))
554        );
555        assert_eq!(
556            form.field_atom_or_string("nope"),
557            Err(DomainFormError::MissingField("nope".to_owned()))
558        );
559    }
560
561    #[test]
562    fn field_text_renders_every_value_kind() {
563        let form = parse_domain_form("#(N a=bare s=\"q\\\"x\" tags=[a,b] inner=#(Rest dur=1/4))")
564            .expect("parse");
565        assert_eq!(form.field_text("a").unwrap(), "bare");
566        assert_eq!(form.field_text("s").unwrap(), "\"q\\\"x\"");
567        assert_eq!(form.field_text("tags").unwrap(), "[a,b]");
568        assert_eq!(form.field_text("inner").unwrap(), "#(Rest dur=1/4)");
569        assert_eq!(
570            form.field_text("nope"),
571            Err(DomainFormError::MissingField("nope".to_owned()))
572        );
573    }
574
575    #[test]
576    fn value_as_form_and_atom_or_string() {
577        let form = parse_domain_form("#(N inner=#(Rest dur=1/4) a=bare s=\"q\")").expect("parse");
578        assert_eq!(form.field("inner").unwrap().as_form().unwrap().name, "Rest");
579        assert_eq!(
580            form.field("a").unwrap().as_form(),
581            Err(DomainFormError::WrongValueKind)
582        );
583        assert_eq!(form.field("a").unwrap().atom_or_string().unwrap(), "bare");
584        assert_eq!(form.field("s").unwrap().atom_or_string().unwrap(), "q");
585        assert_eq!(
586            form.field("inner").unwrap().atom_or_string(),
587            Err(DomainFormError::WrongValueKind)
588        );
589    }
590
591    #[test]
592    fn render_text_round_trips_a_form_value() {
593        let source = "#(Note dur=4/4 sym=\"a\\\"b\" pitches=[60,64])";
594        let form = parse_domain_form(source).expect("parse");
595        let value = DomainValue::Form(form.clone());
596        let rendered = value.render_text();
597        assert_eq!(parse_domain_form(&rendered).unwrap(), form);
598    }
599
600    #[test]
601    fn render_text_formats_list_and_string_and_atom() {
602        assert_eq!(DomainValue::Atom("60".to_owned()).render_text(), "60");
603        assert_eq!(
604            DomainValue::String("a\"b\\c".to_owned()).render_text(),
605            "\"a\\\"b\\\\c\""
606        );
607        let list = DomainValue::List(vec![
608            DomainValue::Atom("a".to_owned()),
609            DomainValue::String("b".to_owned()),
610        ]);
611        assert_eq!(list.render_text(), "[a,\"b\"]");
612    }
613}