use std::fmt::Write;
#[derive(Debug, Clone)]
pub enum Expr {
String(String),
Int(i64),
Ident(String),
Dot(String),
Struct(Vec<(String, Expr)>),
Tuple(Vec<Expr>),
Call { receiver: Option<Box<Expr>>, name: String, args: Vec<Expr> },
}
impl Expr {
pub fn s(v: impl Into<String>) -> Self { Self::String(v.into()) }
pub fn i(v: i64) -> Self { Self::Int(v) }
pub fn ident(v: impl Into<String>) -> Self { Self::Ident(v.into()) }
pub fn dot(member: impl Into<String>) -> Self { Self::Dot(member.into()) }
pub fn strct(fields: Vec<(String, Expr)>) -> Self { Self::Struct(fields) }
pub fn tup(items: impl IntoIterator<Item = Expr>) -> Self {
Self::Tuple(items.into_iter().collect())
}
pub fn call(name: impl Into<String>, args: impl IntoIterator<Item = Expr>) -> Self {
Self::Call { receiver: None, name: name.into(), args: args.into_iter().collect() }
}
pub fn method(receiver: Expr, name: impl Into<String>, args: impl IntoIterator<Item = Expr>) -> Self {
Self::Call {
receiver: Some(Box::new(receiver)),
name: name.into(),
args: args.into_iter().collect(),
}
}
fn render(&self, indent: usize) -> String {
let pad_outer = " ".repeat(indent);
let pad_inner = " ".repeat(indent + 1);
match self {
Self::String(s) => render_zig_string(s),
Self::Int(n) => n.to_string(),
Self::Ident(s) => s.clone(),
Self::Dot(s) => {
let mut o = String::from("."); o.push_str(s); o
}
Self::Struct(fields) if fields.is_empty() => ".{}".into(),
Self::Struct(fields) => {
let mut out = String::from(".{\n");
for (i, (k, v)) in fields.iter().enumerate() {
let _ = write!(out, "{pad_inner}.{k} = {}", v.render(indent + 1));
if i + 1 < fields.len() { out.push(','); }
out.push('\n');
}
let _ = write!(out, "{pad_outer}}}");
out
}
Self::Tuple(items) if items.is_empty() => ".{}".into(),
Self::Tuple(items) => {
let parts: Vec<String> = items.iter().map(|v| v.render(indent + 1)).collect();
let mut s = String::from(".{ ");
s.push_str(&parts.join(", "));
s.push_str(" }");
s
}
Self::Call { receiver, name, args } => {
let mut out = String::new();
if let Some(r) = receiver { out.push_str(&r.render(indent)); out.push('.'); }
out.push_str(name);
out.push('(');
let parts: Vec<String> = args.iter().map(|a| a.render(indent + 1)).collect();
if parts.iter().any(|p| p.contains('\n')) {
out.push('\n');
for (i, p) in parts.iter().enumerate() {
let _ = write!(out, "{pad_inner}{p}");
if i + 1 < parts.len() { out.push(','); }
out.push('\n');
}
let _ = write!(out, "{pad_outer})");
} else {
out.push_str(&parts.join(", "));
out.push(')');
}
out
}
}
}
}
fn render_zig_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(r"\\"),
'"' => out.push_str("\\\""),
'\n' => out.push_str(r"\n"),
'\r' => out.push_str(r"\r"),
'\t' => out.push_str(r"\t"),
c => out.push(c),
}
}
out.push('"');
out
}
#[derive(Debug, Clone)]
pub enum Stmt {
Const { name: String, value: Expr },
Expr(Expr),
Fn { sig: String, body: Vec<Stmt> },
Blank,
Comment(String),
}
#[derive(Debug, Clone, Default)]
pub struct File {
pub stmts: Vec<Stmt>,
}
impl File {
pub fn new() -> Self { Self::default() }
pub fn push(&mut self, s: Stmt) -> &mut Self { self.stmts.push(s); self }
pub fn render(&self) -> String {
let mut out = String::new();
render_at(&self.stmts, 0, &mut out);
out
}
pub fn render_zon(root: &Expr) -> String {
let mut s = root.render(0);
s.push('\n');
s
}
}
fn render_at(stmts: &[Stmt], depth: usize, out: &mut String) {
let pad = " ".repeat(depth);
for s in stmts {
match s {
Stmt::Const { name, value } => {
let _ = writeln!(out, "{pad}const {name} = {};", value.render(depth));
}
Stmt::Expr(e) => { let _ = writeln!(out, "{pad}{};", e.render(depth)); }
Stmt::Fn { sig, body } => {
let _ = writeln!(out, "{pad}{sig} {{");
render_at(body, depth + 1, out);
let _ = writeln!(out, "{pad}}}");
}
Stmt::Blank => out.push('\n'),
Stmt::Comment(c) => { let _ = writeln!(out, "{pad}// {c}"); }
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dot_member_renders_with_leading_dot() {
assert_eq!(Expr::dot("v13").render(0), ".v13");
}
#[test]
fn struct_literal_indents_fields() {
let e = Expr::strct(vec![
("name".into(), Expr::dot("foo")),
("version".into(), Expr::s("1.0")),
]);
let out = e.render(0);
assert!(out.starts_with(".{\n"));
assert!(out.contains(" .name = .foo,"));
assert!(out.contains(" .version = \"1.0\""));
assert!(out.ends_with('}'));
}
#[test]
fn tuple_inline_when_short() {
let e = Expr::tup([Expr::s("a"), Expr::s("b")]);
assert_eq!(e.render(0), ".{ \"a\", \"b\" }");
}
#[test]
fn method_call_with_struct_arg_breaks_multiline() {
let e = Expr::method(Expr::ident("b"), "addExecutable", vec![
Expr::strct(vec![("name".into(), Expr::s("demo"))]),
]);
let out = e.render(0);
assert!(out.contains("b.addExecutable("));
assert!(out.contains(".name = \"demo\""));
}
}