use tatara_lisp::{Atom, Sexp, TataraDomain};
use tatara_lisp_eval::{MapKey, Value};
use crate::pipeline::{run, RunError};
#[derive(Debug, thiserror::Error)]
pub enum DomainError {
#[error("the blue source did not run: {0}")]
Run(#[from] RunError),
#[error("a domain is authored as a map (`{{key: value}}`), but the value was {found}")]
NotAMap { found: &'static str },
#[error("domain keys are labels (`name:`), but a key was {found}")]
KeyNotALabel { found: &'static str },
#[error("the field `{field}` holds {found}, which has no form a domain can read")]
Unrepresentable { field: String, found: &'static str },
#[error("the domain `{keyword}` refused the form: {message}")]
Refused {
keyword: &'static str,
message: String,
},
}
pub fn compile<T: TataraDomain>(src: &str) -> Result<T, DomainError> {
let value = run(src)?.value;
compile_value(&value)
}
pub fn compile_value<T: TataraDomain>(value: &Value) -> Result<T, DomainError> {
let form = domain_form(T::KEYWORD, value)?;
T::compile_from_sexp(&form).map_err(|e| DomainError::Refused {
keyword: T::KEYWORD,
message: e.to_string(),
})
}
pub fn domain_form(keyword: &str, value: &Value) -> Result<Sexp, DomainError> {
let Value::Map(map) = value else {
return Err(DomainError::NotAMap {
found: kind_of(value),
});
};
let mut form = vec![Sexp::Atom(Atom::Symbol(keyword.to_string()))];
form.extend(keyword_args(map)?);
Ok(Sexp::List(form))
}
fn keyword_args(map: &tatara_lisp_eval::Map) -> Result<Vec<Sexp>, DomainError> {
let mut pairs: Vec<(String, &Value)> = map
.iter()
.map(|(k, v)| label_of(k).map(|label| (label, v)))
.collect::<Result<_, _>>()?;
pairs.sort_by(|a, b| a.0.cmp(&b.0));
let mut args = Vec::with_capacity(pairs.len() * 2);
for (label, v) in pairs {
let field = kebab(&label);
let arg = to_sexp(&field, v)?;
args.push(Sexp::Atom(Atom::Keyword(field)));
args.push(arg);
}
Ok(args)
}
fn label_of(key: &MapKey) -> Result<String, DomainError> {
match key {
MapKey::Keyword(k) | MapKey::Symbol(k) => Ok(k.to_string()),
MapKey::Str(_) => Err(DomainError::KeyNotALabel { found: "a string" }),
MapKey::Int(_) => Err(DomainError::KeyNotALabel {
found: "an integer",
}),
MapKey::Float(_) => Err(DomainError::KeyNotALabel { found: "a float" }),
MapKey::Bool(_) => Err(DomainError::KeyNotALabel { found: "a boolean" }),
MapKey::Nil => Err(DomainError::KeyNotALabel { found: "nil" }),
}
}
fn kebab(label: &str) -> String {
label.replace('_', "-")
}
fn to_sexp(field: &str, value: &Value) -> Result<Sexp, DomainError> {
Ok(match value {
Value::Nil => Sexp::Nil,
Value::Bool(b) => Sexp::Atom(Atom::Bool(*b)),
Value::Int(i) => Sexp::Atom(Atom::Int(*i)),
Value::Float(f) => Sexp::Atom(Atom::Float(*f)),
Value::Str(s) => Sexp::Atom(Atom::Str(s.to_string())),
Value::Symbol(s) => Sexp::Atom(Atom::Symbol(s.to_string())),
Value::Keyword(k) => Sexp::Atom(Atom::Keyword(kebab(k))),
Value::List(items) => Sexp::List(
items
.iter()
.map(|v| to_sexp(field, v))
.collect::<Result<_, _>>()?,
),
Value::Map(map) => Sexp::List(keyword_args(map)?),
Value::Sexp(s, _) => s.clone(),
other => {
return Err(DomainError::Unrepresentable {
field: field.to_string(),
found: kind_of(other),
})
}
})
}
fn kind_of(value: &Value) -> &'static str {
match value {
Value::Nil => "nil",
Value::Bool(_) => "a boolean",
Value::Int(_) => "an integer",
Value::Float(_) => "a float",
Value::Str(_) => "a string",
Value::Symbol(_) => "a symbol",
Value::Keyword(_) => "a keyword",
Value::List(_) => "a list",
Value::Map(_) => "a map",
Value::Closure(_) | Value::NativeFn(_) => "a function",
Value::Promise(_) => "a promise",
Value::Error(_) => "an error object",
Value::Sexp(..) => "a quoted form",
_ => "a host value",
}
}