use std::fmt::Write;
#[derive(Debug, Clone)]
pub enum SExp {
Symbol(String),
String(String),
Keyword(String),
Int(i64),
Bool(bool),
List(Vec<SExp>),
Map(Vec<(SExp, SExp)>),
Vector(Vec<SExp>),
}
impl SExp {
pub fn sym(s: impl Into<String>) -> Self { Self::Symbol(s.into()) }
pub fn str(s: impl Into<String>) -> Self { Self::String(s.into()) }
pub fn kw(s: impl Into<String>) -> Self { Self::Keyword(s.into()) }
pub fn int(n: i64) -> Self { Self::Int(n) }
pub fn b(v: bool) -> Self { Self::Bool(v) }
pub fn list(items: impl IntoIterator<Item = SExp>) -> Self {
Self::List(items.into_iter().collect())
}
pub fn vec_(items: impl IntoIterator<Item = SExp>) -> Self {
Self::Vector(items.into_iter().collect())
}
pub fn map_(pairs: impl IntoIterator<Item = (SExp, SExp)>) -> Self {
Self::Map(pairs.into_iter().collect())
}
}
fn escape_string(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str(r"\\"),
'\n' => out.push_str("\\n"),
c => out.push(c),
}
}
out.push('"');
out
}
fn render_at(e: &SExp, indent: usize, out: &mut String) {
let pad = " ".repeat(indent);
match e {
SExp::Symbol(s) => out.push_str(s),
SExp::String(s) => out.push_str(&escape_string(s)),
SExp::Keyword(k) => {
out.push(':');
out.push_str(k);
}
SExp::Int(n) => { let _ = write!(out, "{n}"); }
SExp::Bool(true) => out.push_str("true"),
SExp::Bool(false) => out.push_str("false"),
SExp::List(items) if items.is_empty() => out.push_str("()"),
SExp::List(items) if is_inline_simple(items) => {
out.push('(');
for (i, item) in items.iter().enumerate() {
if i > 0 { out.push(' '); }
render_at(item, 0, out);
}
out.push(')');
}
SExp::List(items) => {
out.push('(');
if let Some(first) = items.first() {
render_at(first, 0, out);
}
for item in items.iter().skip(1) {
out.push('\n');
let _ = write!(out, "{pad} ");
render_at(item, indent + 1, out);
}
out.push(')');
}
SExp::Vector(items) if items.is_empty() => out.push_str("[]"),
SExp::Vector(items) => {
out.push('[');
for (i, item) in items.iter().enumerate() {
if i > 0 { out.push(' '); }
render_at(item, indent, out);
}
out.push(']');
}
SExp::Map(pairs) if pairs.is_empty() => out.push_str("{}"),
SExp::Map(pairs) => {
out.push('{');
for (i, (k, v)) in pairs.iter().enumerate() {
if i > 0 {
out.push('\n');
let _ = write!(out, "{pad} ");
}
render_at(k, indent + 1, out);
out.push(' ');
render_at(v, indent + 1, out);
}
out.push('}');
}
}
}
fn is_inline_simple(items: &[SExp]) -> bool {
items.len() <= 4
&& items.iter().all(|i| matches!(i,
SExp::Symbol(_) | SExp::String(_) | SExp::Keyword(_) | SExp::Int(_) | SExp::Bool(_)
))
}
pub fn render_forms(forms: &[SExp]) -> String {
let mut out = String::new();
for f in forms {
render_at(f, 0, &mut out);
out.push('\n');
}
out
}
#[derive(Debug, Clone, Default)]
pub struct Forms(pub Vec<SExp>);
impl Forms {
pub fn new() -> Self { Self::default() }
pub fn push(&mut self, form: SExp) -> &mut Self { self.0.push(form); self }
pub fn from(items: impl IntoIterator<Item = SExp>) -> Self {
Self(items.into_iter().collect())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn keyword_renders_with_colon() {
let mut out = String::new();
render_at(&SExp::kw("name"), 0, &mut out);
assert_eq!(out, ":name");
}
#[test]
fn string_escapes_quote() {
assert_eq!(escape_string(r#"a"b"#), r#""a\"b""#);
}
#[test]
fn inline_simple_list() {
let e = SExp::list([SExp::sym("name"), SExp::sym("widget")]);
let mut out = String::new();
render_at(&e, 0, &mut out);
assert_eq!(out, "(name widget)");
}
#[test]
fn multi_line_complex_list() {
let e = SExp::list([
SExp::sym("package"),
SExp::list([SExp::sym("name"), SExp::sym("widget")]),
SExp::list([SExp::sym("version"), SExp::sym("1.0")]),
]);
let mut out = String::new();
render_at(&e, 0, &mut out);
assert!(out.starts_with("(package"));
assert!(out.contains("(name widget)"));
}
}