use serde::{Deserialize, Serialize};
use std::path::Path;
use tatara_lisp_derive::TataraDomain as DeriveTataraDomain;
use crate::ast::Sexp;
use crate::error::{LispError, Result};
use crate::macro_expand::Expander;
use crate::reader::read;
#[derive(DeriveTataraDomain, Clone, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[tatara(keyword = "defcompiler")]
pub struct CompilerSpec {
pub name: String,
#[serde(default = "default_dialect")]
pub dialect: String,
#[serde(default)]
pub macros: Vec<String>,
#[serde(default)]
pub domains: Vec<String>,
#[serde(default = "default_optimization")]
pub optimization: String,
#[serde(default)]
pub description: Option<String>,
}
fn default_dialect() -> String {
"standard".into()
}
fn default_optimization() -> String {
"tree-walk".into()
}
#[derive(Clone)]
pub struct RealizedCompiler {
pub spec: CompilerSpec,
preloaded: Expander,
}
impl RealizedCompiler {
pub fn compile(&self, src: &str) -> Result<Vec<Sexp>> {
let forms = read(src)?;
let mut exp = self.preloaded.clone();
exp.expand_program(forms)
}
pub fn expand(&self, form: &Sexp) -> Result<Sexp> {
self.preloaded.expand(form)
}
pub fn macro_count(&self) -> usize {
self.preloaded.len()
}
}
pub fn realize_in_memory(spec: CompilerSpec) -> Result<RealizedCompiler> {
let mut preloaded = Expander::new();
for macro_src in &spec.macros {
let forms = read(macro_src)?;
let _expanded = preloaded.expand_program(forms)?;
}
Ok(RealizedCompiler { spec, preloaded })
}
pub fn realize_to_disk(spec: &CompilerSpec, path: impl AsRef<Path>) -> Result<()> {
let json = serde_json::to_string_pretty(spec).map_err(|e| LispError::Compile {
form: "realize_to_disk".into(),
message: format!("serialize: {e}"),
})?;
std::fs::write(path, json).map_err(|e| LispError::Compile {
form: "realize_to_disk".into(),
message: format!("write: {e}"),
})
}
pub fn load_from_disk(path: impl AsRef<Path>) -> Result<RealizedCompiler> {
let json = std::fs::read_to_string(path).map_err(|e| LispError::Compile {
form: "load_from_disk".into(),
message: format!("read: {e}"),
})?;
let spec: CompilerSpec = serde_json::from_str(&json).map_err(|e| LispError::Compile {
form: "load_from_disk".into(),
message: format!("deserialize: {e}"),
})?;
realize_in_memory(spec)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::domain::TataraDomain;
#[test]
fn defcompiler_form_compiles_to_spec() {
let forms = read(
r#"(defcompiler
:name "my-fast-lisp"
:dialect "standard"
:macros ("(defmacro when (c x) `(if ,c ,x))")
:domains ("defmonitor" "defalertpolicy")
:optimization "tree-walk"
:description "opinionated compiler for alerting")"#,
)
.unwrap();
let spec = CompilerSpec::compile_from_sexp(&forms[0]).unwrap();
assert_eq!(spec.name, "my-fast-lisp");
assert_eq!(spec.dialect, "standard");
assert_eq!(spec.macros.len(), 1);
assert_eq!(
spec.domains,
vec!["defmonitor".to_string(), "defalertpolicy".into()]
);
}
#[test]
fn realize_in_memory_preloads_macros() {
let spec = CompilerSpec {
name: "demo".into(),
dialect: "standard".into(),
macros: vec![
"(defmacro when (c x) `(if ,c ,x))".into(),
"(defmacro unless (c x) `(if ,c () ,x))".into(),
],
domains: vec![],
optimization: "tree-walk".into(),
description: None,
};
let compiler = realize_in_memory(spec).unwrap();
assert_eq!(compiler.macro_count(), 2);
}
#[test]
fn realized_compiler_expands_user_source() {
let spec = CompilerSpec {
name: "demo".into(),
dialect: "standard".into(),
macros: vec!["(defmacro when (c x) `(if ,c ,x))".into()],
domains: vec![],
optimization: "tree-walk".into(),
description: None,
};
let compiler = realize_in_memory(spec).unwrap();
let expanded = compiler.compile("(when #t (foo))").unwrap();
assert_eq!(expanded.len(), 1);
let list = expanded[0].as_list().unwrap();
assert_eq!(list[0].as_symbol(), Some("if"));
assert_eq!(list[1], Sexp::boolean(true));
}
#[test]
fn nested_macro_expands_through_preloaded() {
let spec = CompilerSpec {
name: "demo".into(),
dialect: "standard".into(),
macros: vec!["(defmacro when (c x) `(if ,c ,x))".into()],
domains: vec![],
optimization: "tree-walk".into(),
description: None,
};
let compiler = realize_in_memory(spec).unwrap();
let expanded = compiler
.compile("(defmacro unless (c x) `(when (not ,c) ,x)) (unless #f (foo))")
.unwrap();
let final_form = expanded.last().unwrap().as_list().unwrap();
assert_eq!(final_form[0].as_symbol(), Some("if"));
}
#[test]
fn realize_to_disk_and_load_round_trips() {
let tmp = std::env::temp_dir().join(format!("tatara-compiler-{}.json", std::process::id()));
let spec = CompilerSpec {
name: "disk-test".into(),
dialect: "standard".into(),
macros: vec!["(defmacro id (x) `,x)".into()],
domains: vec!["defmonitor".into()],
optimization: "tree-walk".into(),
description: Some("persistence smoke test".into()),
};
realize_to_disk(&spec, &tmp).unwrap();
let compiler = load_from_disk(&tmp).unwrap();
assert_eq!(compiler.spec.name, "disk-test");
assert_eq!(compiler.macro_count(), 1);
let out = compiler.compile("(id 42)").unwrap();
assert_eq!(out[0], Sexp::int(42));
let _ = std::fs::remove_file(&tmp);
}
#[test]
fn empty_compiler_expands_nothing_but_reads_source() {
let spec = CompilerSpec {
name: "empty".into(),
dialect: "standard".into(),
macros: vec![],
domains: vec![],
optimization: "tree-walk".into(),
description: None,
};
let compiler = realize_in_memory(spec).unwrap();
assert_eq!(compiler.macro_count(), 0);
let out = compiler.compile("(foo bar)").unwrap();
assert_eq!(out.len(), 1);
}
#[test]
fn self_bootstrapping_compiler_generates_another_compiler() {
let base = realize_in_memory(CompilerSpec {
name: "base".into(),
dialect: "standard".into(),
macros: vec![],
domains: vec![],
optimization: "tree-walk".into(),
description: None,
})
.unwrap();
let source_of_child = r#"(defcompiler
:name "child"
:dialect "standard"
:macros ("(defmacro twice (x) `(list ,x ,x))")
:optimization "tree-walk")"#;
let forms = base.compile(source_of_child).unwrap();
assert_eq!(forms.len(), 1);
let child_spec = CompilerSpec::compile_from_sexp(&forms[0]).unwrap();
let child = realize_in_memory(child_spec).unwrap();
assert_eq!(child.macro_count(), 1);
let final_form = child.compile("(twice hello)").unwrap();
let list = final_form[0].as_list().unwrap();
assert_eq!(list[0].as_symbol(), Some("list"));
assert_eq!(list[1].as_symbol(), Some("hello"));
assert_eq!(list[2].as_symbol(), Some("hello"));
}
}