Skip to main content

sim_codec_lisp/implementation/
encode.rs

1//! Encoding for the Lisp codec: renders any `Expr` (or located expression tree)
2//! back to s-expression text, respecting its output position and quote forms.
3
4use sim_codec::{
5    DecodeLimits, Decoder, Encoder, Input, Output, ReadCx, TreeEncoder, encode_string_literal,
6};
7use sim_kernel::{
8    EncodePosition, Expr, LocatedExprTree, NumberLiteral, ObjectEncoding, Origin, QuoteMode,
9    ReadConstructEncodePolicy, Result, Symbol, Trivia, Value, WriteCx,
10};
11
12use super::decode::LispProcMacroDecoder;
13use super::forms::{may_be_number_literal, parse_logic_var, parse_symbol};
14
15/// Lisp encoder that renders expressions back to s-expression text.
16///
17/// Implements the codec's [`Encoder`] and [`TreeEncoder`] roles: it serializes
18/// any [`Expr`] (or a [`LocatedExprTree`], preserving trivia) to Lisp text,
19/// honoring quote forms and the requested output position.
20pub struct LispProcMacroEncoder;
21
22impl Encoder for LispProcMacroEncoder {
23    fn encode(&self, cx: &mut WriteCx<'_>, expr: &Expr) -> Result<Output> {
24        Ok(Output::Text(encode_lisp_expr(cx, expr)?))
25    }
26}
27
28impl TreeEncoder for LispProcMacroEncoder {
29    fn encode_tree(&self, cx: &mut WriteCx<'_>, expr: &LocatedExprTree) -> Result<Output> {
30        Ok(Output::Text(encode_lisp_tree(cx, expr)?))
31    }
32}
33
34fn encode_lisp_expr(cx: &mut WriteCx<'_>, expr: &Expr) -> Result<String> {
35    match expr {
36        Expr::Nil => Ok("nil".to_owned()),
37        Expr::Bool(true) => Ok("true".to_owned()),
38        Expr::Bool(false) => Ok("false".to_owned()),
39        Expr::Number(number) => encode_number_lisp(cx, number),
40        Expr::Symbol(symbol) => encode_symbol_lisp(cx, symbol),
41        Expr::Local(symbol) => encode_local_lisp(cx, symbol),
42        Expr::String(value) => Ok(encode_string_literal(value)),
43        Expr::Bytes(bytes) => Ok(encode_byte_string_literal(bytes)),
44        Expr::List(items) => encode_seq(cx, "(", ")", items),
45        Expr::Vector(items) => encode_seq(cx, "[", "]", items),
46        Expr::Map(entries) => encode_map(cx, entries),
47        Expr::Set(items) => encode_escape_named(cx, "expr:set", items),
48        Expr::Call { operator, args } => match cx.options.position {
49            EncodePosition::Eval => {
50                let mut items = Vec::with_capacity(args.len() + 1);
51                items.push((**operator).clone());
52                items.extend(args.iter().cloned());
53                encode_seq(cx, "(", ")", &items)
54            }
55            _ => {
56                let mut items = Vec::with_capacity(args.len() + 1);
57                items.push(operator.as_ref().clone());
58                items.extend(args.iter().cloned());
59                encode_escape_named(cx, "expr:call", &items)
60            }
61        },
62        Expr::Infix {
63            operator,
64            left,
65            right,
66        } => encode_escape_named(
67            cx,
68            "expr:infix",
69            &[
70                Expr::String(operator.to_string()),
71                left.as_ref().clone(),
72                right.as_ref().clone(),
73            ],
74        ),
75        Expr::Prefix { operator, arg } => encode_escape_named(
76            cx,
77            "expr:prefix",
78            &[Expr::String(operator.to_string()), arg.as_ref().clone()],
79        ),
80        Expr::Postfix { operator, arg } => encode_escape_named(
81            cx,
82            "expr:postfix",
83            &[Expr::String(operator.to_string()), arg.as_ref().clone()],
84        ),
85        Expr::Block(items) => encode_seq(cx, "{", "}", items),
86        Expr::Quote { mode, expr } => encode_quote_lisp(cx, *mode, expr),
87        Expr::Annotated { expr, annotations } => {
88            let mut items = Vec::with_capacity(annotations.len() + 2);
89            items.push(Expr::Symbol(Symbol::qualified("expr", "annotated")));
90            items.push(expr.as_ref().clone());
91            items.extend(annotations.iter().map(|(symbol, value)| {
92                Expr::List(vec![Expr::Symbol(symbol.clone()), value.clone()])
93            }));
94            encode_seq(cx, "(", ")", &items)
95        }
96        Expr::Extension { tag, payload } => encode_escape_named(
97            cx,
98            "expr:extension",
99            &[Expr::Symbol(tag.clone()), payload.as_ref().clone()],
100        ),
101    }
102}
103
104fn encode_number_lisp(cx: &mut WriteCx<'_>, number: &NumberLiteral) -> Result<String> {
105    if plain_number_round_trips(cx, number) {
106        return Ok(number.canonical.clone());
107    }
108    encode_number_escape_lisp(cx, number)
109}
110
111fn encode_number_escape_lisp(cx: &mut WriteCx<'_>, number: &NumberLiteral) -> Result<String> {
112    encode_escape_named(
113        cx,
114        "expr:number",
115        &[
116            Expr::Symbol(number.domain.clone()),
117            Expr::String(number.canonical.clone()),
118        ],
119    )
120}
121
122fn plain_number_round_trips(cx: &mut WriteCx<'_>, number: &NumberLiteral) -> bool {
123    let mut read_cx = ReadCx {
124        cx: cx.cx,
125        codec: cx.codec,
126        read_policy: Default::default(),
127        limits: DecodeLimits::default(),
128    };
129    LispProcMacroDecoder
130        .decode(&mut read_cx, Input::Text(number.canonical.clone()))
131        .is_ok_and(|expr| expr == Expr::Number(number.clone()))
132}
133
134fn encode_symbol_lisp(cx: &mut WriteCx<'_>, symbol: &Symbol) -> Result<String> {
135    let text = symbol.to_string();
136    if plain_symbol_round_trips(cx, symbol, &text)? {
137        return Ok(text);
138    }
139    encode_symbol_escape_lisp(cx, symbol)
140}
141
142fn encode_symbol_escape_lisp(cx: &mut WriteCx<'_>, symbol: &Symbol) -> Result<String> {
143    let namespace = match &symbol.namespace {
144        Some(namespace) => Expr::String(namespace.to_string()),
145        None => Expr::Nil,
146    };
147    encode_escape_named(
148        cx,
149        "expr:symbol",
150        &[namespace, Expr::String(symbol.name.to_string())],
151    )
152}
153
154fn encode_local_lisp(cx: &mut WriteCx<'_>, symbol: &Symbol) -> Result<String> {
155    let namespace = match &symbol.namespace {
156        Some(namespace) => Expr::String(namespace.to_string()),
157        None => Expr::Nil,
158    };
159    encode_escape_named(
160        cx,
161        "expr:local",
162        &[namespace, Expr::String(symbol.name.to_string())],
163    )
164}
165
166fn plain_symbol_round_trips(cx: &mut WriteCx<'_>, symbol: &Symbol, text: &str) -> Result<bool> {
167    if matches!(text, "nil" | "true" | "false") || parse_logic_var(text).is_some() {
168        return Ok(false);
169    }
170    if !text.chars().all(|ch| {
171        ch.is_ascii_alphanumeric() || matches!(ch, '_' | '/' | ':' | '?' | '!' | '-' | '+' | '.')
172    }) {
173        return Ok(false);
174    }
175    if has_punctuation_digit_boundary(text) {
176        return Ok(false);
177    }
178    if may_be_number_literal(text) && cx.cx.parse_number_literal(text)?.is_some() {
179        return Ok(false);
180    }
181    Ok(parse_symbol(text) == *symbol)
182}
183
184fn starts_with_joining_punctuation(text: &str) -> bool {
185    text.chars()
186        .next()
187        .is_some_and(|ch| matches!(ch, '/' | ':' | '?' | '!' | '-' | '+' | '.'))
188}
189
190fn ends_with_joining_punctuation(text: &str) -> bool {
191    text.chars()
192        .last()
193        .is_some_and(|ch| matches!(ch, '/' | ':' | '?' | '!' | '-' | '+' | '.'))
194}
195
196fn has_punctuation_digit_boundary(text: &str) -> bool {
197    let mut previous = None;
198    for ch in text.chars() {
199        if ch.is_ascii_digit()
200            && previous.is_some_and(|prev| matches!(prev, '/' | ':' | '?' | '!' | '-' | '+' | '.'))
201        {
202            return true;
203        }
204        previous = Some(ch);
205    }
206    false
207}
208
209fn encode_lisp_tree(cx: &mut WriteCx<'_>, tree: &LocatedExprTree) -> Result<String> {
210    let prefix = encode_trivia(&tree.origin);
211    let body = match &tree.expr {
212        Expr::List(_) => encode_tree_seq(cx, "(", ")", &tree.children)?,
213        Expr::Vector(_) => encode_tree_seq(cx, "[", "]", &tree.children)?,
214        Expr::Block(_) => encode_tree_seq(cx, "{", "}", &tree.children)?,
215        Expr::Quote { mode, .. } if tree.children.len() == 1 => {
216            let name = match mode {
217                QuoteMode::Quote => "quote",
218                QuoteMode::QuasiQuote => "quasiquote",
219                QuoteMode::Unquote => "unquote",
220                QuoteMode::Splice => "splice",
221                QuoteMode::Syntax => "syntax",
222            };
223            format!(
224                "({name} {})",
225                encode_quote_tree_operand_lisp(cx, &tree.children[0])?
226            )
227        }
228        _ => encode_lisp_expr(cx, &tree.expr)?,
229    };
230    Ok(format!("{prefix}{body}"))
231}
232
233fn encode_tree_seq(
234    cx: &mut WriteCx<'_>,
235    start: &str,
236    end: &str,
237    items: &[LocatedExprTree],
238) -> Result<String> {
239    let inner = items
240        .iter()
241        .enumerate()
242        .map(|(index, item)| {
243            let previous = index.checked_sub(1).and_then(|prev| items.get(prev));
244            let next = items.get(index + 1);
245            encode_tree_seq_item(cx, item, previous, next)
246        })
247        .collect::<Result<Vec<_>>>()?
248        .join(" ");
249    Ok(format!("{start}{inner}{end}"))
250}
251
252fn encode_seq(cx: &mut WriteCx<'_>, start: &str, end: &str, items: &[Expr]) -> Result<String> {
253    let inner = items
254        .iter()
255        .enumerate()
256        .map(|(index, item)| {
257            let previous = index.checked_sub(1).and_then(|prev| items.get(prev));
258            let next = items.get(index + 1);
259            encode_seq_item(cx, item, previous, next)
260        })
261        .collect::<Result<Vec<_>>>()?
262        .join(" ");
263    Ok(format!("{start}{inner}{end}"))
264}
265
266fn encode_tree_seq_item(
267    cx: &mut WriteCx<'_>,
268    item: &LocatedExprTree,
269    previous: Option<&LocatedExprTree>,
270    next: Option<&LocatedExprTree>,
271) -> Result<String> {
272    if let Expr::Symbol(symbol) = &item.expr
273        && symbol_needs_sequence_escape(
274            symbol,
275            previous.map(|tree| &tree.expr),
276            next.map(|tree| &tree.expr),
277        )
278    {
279        return Ok(format!(
280            "{}{}",
281            encode_trivia(&item.origin),
282            encode_symbol_escape_lisp(cx, symbol)?
283        ));
284    }
285    encode_lisp_tree(cx, item)
286}
287
288fn encode_seq_item(
289    cx: &mut WriteCx<'_>,
290    item: &Expr,
291    previous: Option<&Expr>,
292    next: Option<&Expr>,
293) -> Result<String> {
294    if let Expr::Symbol(symbol) = item
295        && symbol_needs_sequence_escape(symbol, previous, next)
296    {
297        return encode_symbol_escape_lisp(cx, symbol);
298    }
299    encode_lisp_expr(cx, item)
300}
301
302fn symbol_needs_sequence_escape(
303    symbol: &Symbol,
304    previous: Option<&Expr>,
305    next: Option<&Expr>,
306) -> bool {
307    let text = symbol.to_string();
308    has_punctuation_digit_boundary(&text)
309        || (starts_with_joining_punctuation(&text) && matches!(previous, Some(Expr::Symbol(_))))
310        || (ends_with_joining_punctuation(&text) && matches!(next, Some(Expr::Symbol(_))))
311}
312
313fn encode_map(cx: &mut WriteCx<'_>, entries: &[(Expr, Expr)]) -> Result<String> {
314    let mut sorted = entries.to_vec();
315    sorted.sort_by_key(|(key, value)| (key.canonical_key(), value.canonical_key()));
316    let items = sorted
317        .iter()
318        .map(|(key, value)| {
319            Ok(format!(
320                "[{} {}]",
321                encode_map_key_expr(cx, key)?,
322                encode_map_value_expr(cx, value)?
323            ))
324        })
325        .collect::<Result<Vec<_>>>()?
326        .join(" ");
327    Ok(format!("(expr:map {items})"))
328}
329
330fn encode_map_key_expr(cx: &mut WriteCx<'_>, expr: &Expr) -> Result<String> {
331    if let Expr::Symbol(symbol) = expr {
332        let text = symbol.to_string();
333        if ends_with_joining_punctuation(&text) || has_punctuation_digit_boundary(&text) {
334            return encode_symbol_escape_lisp(cx, symbol);
335        }
336    }
337    encode_lisp_expr(cx, expr)
338}
339
340fn encode_map_value_expr(cx: &mut WriteCx<'_>, expr: &Expr) -> Result<String> {
341    if let Expr::Symbol(symbol) = expr {
342        let text = symbol.to_string();
343        if starts_with_joining_punctuation(&text) || has_punctuation_digit_boundary(&text) {
344            return encode_symbol_escape_lisp(cx, symbol);
345        }
346    }
347    encode_lisp_expr(cx, expr)
348}
349
350fn encode_escape_named(cx: &mut WriteCx<'_>, name: &str, args: &[Expr]) -> Result<String> {
351    let mut items = Vec::with_capacity(args.len() + 1);
352    items.push(Expr::Symbol(parse_symbol(name)));
353    items.extend(args.iter().cloned());
354    encode_seq(cx, "(", ")", &items)
355}
356
357fn encode_quote_lisp(cx: &mut WriteCx<'_>, mode: QuoteMode, expr: &Expr) -> Result<String> {
358    let name = match mode {
359        QuoteMode::Quote => "quote",
360        QuoteMode::QuasiQuote => "quasiquote",
361        QuoteMode::Unquote => "unquote",
362        QuoteMode::Splice => "splice",
363        QuoteMode::Syntax => "syntax",
364    };
365
366    let mut nested = cx.with_position(EncodePosition::Quote);
367    let inner = encode_quote_operand_lisp(&mut nested, expr)?;
368    Ok(format!("({name} {inner})"))
369}
370
371fn encode_quote_operand_lisp(cx: &mut WriteCx<'_>, expr: &Expr) -> Result<String> {
372    if let Expr::Symbol(symbol) = expr {
373        let text = symbol.to_string();
374        if starts_with_joining_punctuation(&text) || has_punctuation_digit_boundary(&text) {
375            return encode_symbol_escape_lisp(cx, symbol);
376        }
377    }
378    encode_lisp_expr(cx, expr)
379}
380
381fn encode_quote_tree_operand_lisp(cx: &mut WriteCx<'_>, tree: &LocatedExprTree) -> Result<String> {
382    if let Expr::Symbol(symbol) = &tree.expr {
383        let text = symbol.to_string();
384        if starts_with_joining_punctuation(&text) || has_punctuation_digit_boundary(&text) {
385            return Ok(format!(
386                "{}{}",
387                encode_trivia(&tree.origin),
388                encode_symbol_escape_lisp(cx, symbol)?
389            ));
390        }
391    }
392    encode_lisp_tree(cx, tree)
393}
394
395fn encode_byte_string_literal(bytes: &[u8]) -> String {
396    let mut out = String::from("b\"");
397    for byte in bytes {
398        match byte {
399            b'\n' => out.push_str("\\n"),
400            b'\r' => out.push_str("\\r"),
401            b'\t' => out.push_str("\\t"),
402            b'\\' => out.push_str("\\\\"),
403            b'"' => out.push_str("\\\""),
404            0x20..=0x7e => out.push(*byte as char),
405            other => out.push_str(&format!("\\x{other:02x}")),
406        }
407    }
408    out.push('"');
409    out
410}
411
412fn encode_trivia(origin: &Option<Origin>) -> String {
413    origin
414        .as_ref()
415        .map(|origin| {
416            origin
417                .trivia
418                .iter()
419                .map(|item| match item {
420                    Trivia::Whitespace(text)
421                    | Trivia::LineComment(text)
422                    | Trivia::BlockComment(text) => text.clone(),
423                })
424                .collect::<Vec<_>>()
425                .join("")
426        })
427        .unwrap_or_default()
428}
429
430/// Encodes a runtime [`Value`] to Lisp text via its object encoder, rendering
431/// constructor, tagged-data, and opaque object encodings as s-expression forms.
432pub fn encode_object_lisp(cx: &mut WriteCx<'_>, value: Value) -> Result<String> {
433    let encoder = value
434        .object()
435        .as_object_encoder()
436        .ok_or_else(|| sim_kernel::Error::Eval("value has no object encoder".to_owned()))?;
437    match encoder.object_encoding(cx.cx)? {
438        ObjectEncoding::Constructor { class, args } => encode_constructor_lisp(cx, class, args),
439        ObjectEncoding::TaggedData { tag, fields } => {
440            let mut items = vec![
441                Expr::Symbol(Symbol::qualified("object", "tagged")),
442                Expr::Symbol(tag),
443            ];
444            items.extend(
445                fields
446                    .into_iter()
447                    .map(|(name, value)| Expr::List(vec![Expr::Symbol(name), value])),
448            );
449            encode_seq(cx, "(", ")", &items)
450        }
451        ObjectEncoding::Opaque { class, stable_id } => encode_seq(
452            cx,
453            "(",
454            ")",
455            &[
456                Expr::Symbol(Symbol::qualified("object", "opaque")),
457                Expr::Symbol(class),
458                Expr::String(stable_id),
459            ],
460        ),
461    }
462}
463
464fn encode_constructor_lisp(cx: &mut WriteCx<'_>, class: Symbol, args: Vec<Expr>) -> Result<String> {
465    let mut items = Vec::with_capacity(args.len() + 1);
466    items.push(Expr::Symbol(class.clone()));
467    items.extend(args);
468    let inner = items
469        .iter()
470        .map(|item| encode_lisp_expr(cx, item))
471        .collect::<Result<Vec<_>>>()?
472        .join(" ");
473    match cx.options.position {
474        EncodePosition::Eval => Ok(format!("({inner})")),
475        EncodePosition::Quote
476            if matches!(cx.options.read_construct, ReadConstructEncodePolicy::Allow) =>
477        {
478            Ok(format!("#({inner})"))
479        }
480        _ => Ok(format!("(object {inner})")),
481    }
482}