1use 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
15pub 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
430pub 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}