Skip to main content

sim_codec_lisp/
grammar.rs

1//! Lisp codec grammar rendering over the neutral Shape grammar graph.
2
3use sim_codec::encode_string_literal;
4use sim_kernel::{Error, Expr, Result, Symbol};
5use sim_shape::{
6    GrammarDialect, GrammarGraph, GrammarPosition, GrammarRenderer, Production, TerminalAtom,
7};
8
9/// Renders neutral Shape grammars for `codec:lisp`.
10#[derive(Clone, Copy, Debug, PartialEq, Eq)]
11pub struct LispGrammarRenderer {
12    dialect: GrammarDialect,
13}
14
15impl LispGrammarRenderer {
16    /// Builds a renderer for `dialect`.
17    pub fn new(dialect: GrammarDialect) -> Self {
18        Self { dialect }
19    }
20
21    /// Builds an S-expression grammar renderer.
22    pub fn sexpr() -> Self {
23        Self::new(GrammarDialect::SExpr)
24    }
25
26    /// Builds a Lisp-shaped GBNF renderer.
27    pub fn gbnf() -> Self {
28        Self::new(GrammarDialect::Gbnf)
29    }
30}
31
32impl GrammarRenderer for LispGrammarRenderer {
33    fn codec_symbol(&self) -> Symbol {
34        Symbol::qualified("codec", "lisp")
35    }
36
37    fn dialect(&self) -> GrammarDialect {
38        self.dialect
39    }
40
41    fn render(&self, graph: &GrammarGraph, position: GrammarPosition) -> Result<String> {
42        match self.dialect {
43            GrammarDialect::SExpr => render_lisp_sexpr_graph(graph, position),
44            GrammarDialect::Gbnf => render_lisp_gbnf_graph(graph, position),
45            unsupported => Err(grammar_error(format!(
46                "codec/lisp does not support {unsupported:?} grammar dialect"
47            ))),
48        }
49    }
50}
51
52fn render_lisp_sexpr_graph(graph: &GrammarGraph, position: GrammarPosition) -> Result<String> {
53    let mut forms = vec![
54        format!("(codec {})", Symbol::qualified("codec", "lisp")),
55        format!("(position {})", position_name(position)),
56        format!("(decode-target {})", lisp_decode_target(position)),
57        format!("(root {})", render_lisp_sexpr(&graph.root)?),
58    ];
59    for (name, production) in &graph.defs {
60        forms.push(format!("(def {} {})", name, render_lisp_sexpr(production)?));
61    }
62    Ok(format!("(grammar {})", forms.join(" ")))
63}
64
65fn render_lisp_sexpr(production: &Production) -> Result<String> {
66    match production {
67        Production::Terminal(atom) => render_lisp_terminal(atom),
68        Production::Seq(items) => render_wrapped("seq", items.iter().map(render_lisp_sexpr)),
69        Production::Alt(choices) => render_wrapped("alt", choices.iter().map(render_lisp_sexpr)),
70        Production::Repeat { inner, at_least } => Ok(format!(
71            "(repeat {} {})",
72            at_least,
73            render_lisp_sexpr(inner)?
74        )),
75        Production::Call { head, args } => {
76            let mut rendered = Vec::with_capacity(args.len() + 1);
77            rendered.push(render_lisp_sexpr(head)?);
78            for arg in args {
79                rendered.push(render_lisp_sexpr(arg)?);
80            }
81            Ok(format!("({})", rendered.join(" ")))
82        }
83        Production::Ref(name) => Ok(format!("(ref {})", name)),
84    }
85}
86
87fn render_lisp_terminal(atom: &TerminalAtom) -> Result<String> {
88    Ok(match atom {
89        TerminalAtom::Any => "_".to_owned(),
90        TerminalAtom::Nil => "nil".to_owned(),
91        TerminalAtom::Bool => "Bool".to_owned(),
92        TerminalAtom::Number => "Number".to_owned(),
93        TerminalAtom::String => "String".to_owned(),
94        TerminalAtom::List => "List".to_owned(),
95        TerminalAtom::Map => "Map".to_owned(),
96        TerminalAtom::Symbol => "Symbol".to_owned(),
97        TerminalAtom::Exact(expr) => render_exact_lisp(expr)?,
98    })
99}
100
101fn render_lisp_gbnf_graph(graph: &GrammarGraph, position: GrammarPosition) -> Result<String> {
102    let mut lines = vec![
103        format!(
104            "# codec/lisp position={} target={}",
105            position_name(position),
106            lisp_decode_target(position)
107        ),
108        format!("root ::= {}", render_lisp_gbnf(&graph.root)?),
109    ];
110    for (name, production) in &graph.defs {
111        lines.push(format!(
112            "{} ::= {}",
113            rule_name(name),
114            render_lisp_gbnf(production)?
115        ));
116    }
117    Ok(lines.join("\n"))
118}
119
120fn render_lisp_gbnf(production: &Production) -> Result<String> {
121    match production {
122        Production::Terminal(atom) => render_lisp_gbnf_terminal(atom),
123        Production::Seq(items) => {
124            let rendered = items
125                .iter()
126                .map(render_lisp_gbnf)
127                .collect::<Result<Vec<_>>>()?;
128            Ok(format!("({})", rendered.join(" ")))
129        }
130        Production::Alt(choices) => {
131            let rendered = choices
132                .iter()
133                .map(render_lisp_gbnf)
134                .collect::<Result<Vec<_>>>()?;
135            Ok(format!("({})", rendered.join(" | ")))
136        }
137        Production::Repeat { inner, .. } => Ok(format!("({})*", render_lisp_gbnf(inner)?)),
138        Production::Call { head, args } => {
139            let mut rendered = Vec::with_capacity(args.len() + 1);
140            rendered.push(render_lisp_gbnf(head)?);
141            for arg in args {
142                rendered.push(render_lisp_gbnf(arg)?);
143            }
144            Ok(format!("\"(\" {} \")\"", rendered.join(" ")))
145        }
146        Production::Ref(name) => Ok(rule_name(name)),
147    }
148}
149
150fn render_lisp_gbnf_terminal(atom: &TerminalAtom) -> Result<String> {
151    Ok(match atom {
152        TerminalAtom::Any => "sexpr".to_owned(),
153        TerminalAtom::Nil => "\"nil\"".to_owned(),
154        TerminalAtom::Bool => "(\"true\" | \"false\")".to_owned(),
155        TerminalAtom::Number => "number".to_owned(),
156        TerminalAtom::String => "string".to_owned(),
157        TerminalAtom::List => "list".to_owned(),
158        TerminalAtom::Map => "map".to_owned(),
159        TerminalAtom::Symbol => "symbol".to_owned(),
160        TerminalAtom::Exact(expr) => gbnf_literal(&render_exact_lisp(expr)?),
161    })
162}
163
164fn render_wrapped(head: &str, values: impl Iterator<Item = Result<String>>) -> Result<String> {
165    let rendered = values.collect::<Result<Vec<_>>>()?;
166    Ok(format!("({} {})", head, rendered.join(" ")))
167}
168
169fn render_exact_lisp(expr: &Expr) -> Result<String> {
170    Ok(match expr {
171        Expr::Nil => "nil".to_owned(),
172        Expr::Bool(true) => "true".to_owned(),
173        Expr::Bool(false) => "false".to_owned(),
174        Expr::Number(number) => number.canonical.clone(),
175        Expr::String(text) => encode_string_literal(text),
176        Expr::Symbol(symbol) => symbol.to_string(),
177        Expr::List(items) | Expr::Vector(items) => {
178            let items = items
179                .iter()
180                .map(render_exact_lisp)
181                .collect::<Result<Vec<_>>>()?;
182            format!("({})", items.join(" "))
183        }
184        Expr::Map(entries) => {
185            let entries = entries
186                .iter()
187                .map(|(key, value)| {
188                    Ok(format!(
189                        "({} {})",
190                        render_exact_lisp(key)?,
191                        render_exact_lisp(value)?
192                    ))
193                })
194                .collect::<Result<Vec<_>>>()?;
195            format!("(map {})", entries.join(" "))
196        }
197        _ => {
198            return Err(grammar_error(
199                "exact Lisp grammar terminals support data-like expressions",
200            ));
201        }
202    })
203}
204
205fn gbnf_literal(text: &str) -> String {
206    format!("\"{}\"", text.replace('\\', "\\\\").replace('"', "\\\""))
207}
208
209fn rule_name(symbol: &Symbol) -> String {
210    let mut out = String::new();
211    for ch in symbol.to_string().chars() {
212        if ch.is_ascii_alphanumeric() || ch == '-' {
213            out.push(ch);
214        } else {
215            out.push('-');
216        }
217    }
218    if out
219        .chars()
220        .next()
221        .is_none_or(|ch| !ch.is_ascii_alphabetic())
222    {
223        out.insert_str(0, "r-");
224    }
225    out
226}
227
228fn position_name(position: GrammarPosition) -> &'static str {
229    match position {
230        GrammarPosition::Eval => "eval",
231        GrammarPosition::Quote => "quote",
232        GrammarPosition::Data => "data",
233        GrammarPosition::Pattern => "pattern",
234        GrammarPosition::Surface => "surface",
235    }
236}
237
238fn lisp_decode_target(position: GrammarPosition) -> &'static str {
239    match position {
240        GrammarPosition::Eval => "term",
241        GrammarPosition::Quote
242        | GrammarPosition::Data
243        | GrammarPosition::Pattern
244        | GrammarPosition::Surface => "datum",
245    }
246}
247
248fn grammar_error(message: impl Into<String>) -> Error {
249    Error::Eval(format!("codec/lisp grammar renderer: {}", message.into()))
250}
251
252#[cfg(test)]
253mod tests {
254    use std::sync::Arc;
255
256    use sim_kernel::Symbol;
257    use sim_shape::{
258        ExprKind, ExprKindShape, FieldShape, FieldSpec, GrammarDialect, GrammarPosition,
259        GrammarTarget, OneOfShape, Shape, ShapeDefRef, ShapeDefs, shape_grammar,
260    };
261
262    use super::LispGrammarRenderer;
263
264    #[test]
265    fn lisp_sexpr_renders_calls_and_refs() {
266        let grammar = shape_grammar(
267            recursive_node_shape().as_ref(),
268            GrammarTarget {
269                codec: Symbol::qualified("codec", "lisp"),
270                dialect: GrammarDialect::SExpr,
271                position: GrammarPosition::Eval,
272            },
273            &LispGrammarRenderer::sexpr(),
274        )
275        .unwrap();
276
277        assert!(grammar.text.contains("(decode-target term)"));
278        assert!(grammar.text.contains("(shape/fields"));
279        assert!(grammar.text.contains("(ref Node)"));
280        assert!(grammar.text.contains("name"));
281        assert!(grammar.text.contains("next"));
282    }
283
284    #[test]
285    fn lisp_gbnf_uses_named_rules_for_refs() {
286        let grammar = shape_grammar(
287            recursive_node_shape().as_ref(),
288            GrammarTarget {
289                codec: Symbol::qualified("codec", "lisp"),
290                dialect: GrammarDialect::Gbnf,
291                position: GrammarPosition::Quote,
292            },
293            &LispGrammarRenderer::gbnf(),
294        )
295        .unwrap();
296
297        assert!(grammar.text.contains("target=datum"));
298        assert!(grammar.text.contains("Node ::="));
299        assert!(grammar.text.contains("Node"));
300    }
301
302    #[test]
303    fn lisp_renderer_rejects_unsupported_dialect() {
304        let err = shape_grammar(
305            recursive_node_shape().as_ref(),
306            GrammarTarget {
307                codec: Symbol::qualified("codec", "lisp"),
308                dialect: GrammarDialect::JsonSchema,
309                position: GrammarPosition::Data,
310            },
311            &LispGrammarRenderer::new(GrammarDialect::JsonSchema),
312        )
313        .unwrap_err();
314
315        assert!(err.to_string().contains("does not support JsonSchema"));
316    }
317
318    fn recursive_node_shape() -> Arc<dyn Shape> {
319        let node = Symbol::new("Node");
320        Arc::new(ShapeDefs::new(
321            Arc::new(ShapeDefRef::new(node.clone())),
322            vec![(
323                node.clone(),
324                Arc::new(FieldShape::anonymous(vec![
325                    FieldSpec::required(
326                        Symbol::new("name"),
327                        Arc::new(ExprKindShape::new(ExprKind::String)),
328                    ),
329                    FieldSpec::required(
330                        Symbol::new("next"),
331                        Arc::new(OneOfShape::new(vec![
332                            Arc::new(ExprKindShape::new(ExprKind::Nil)),
333                            Arc::new(ShapeDefRef::new(node)),
334                        ])),
335                    ),
336                ])),
337            )],
338        ))
339    }
340}