locrian 0.2.1

A simple embeddable functional programming language.
Documentation
use std::{collections::HashMap, error::Error, fmt::Display};

use crate::{
    parser::ExprValue,
    stdlib::{self, quote::call},
};

pub type EvalFn = fn(Vec<EvalResult>) -> EvalResult;

#[derive(Debug, PartialEq, Clone)]
pub struct EvalContext<'a> {
    pub vars: HashMap<String, ExprValue<'a>>,
    pub fns: HashMap<String, EvalFn>,
}

impl<'a> Default for EvalContext<'a> {
    fn default() -> Self {
        Self::new()
    }
}

impl<'a> EvalContext<'a> {
    pub fn new() -> Self {
        Self {
            vars: HashMap::new(),
            fns: HashMap::new(),
        }
    }
    pub fn with_var<N: Into<String>>(&self, name: N, value: ExprValue<'a>) -> Self {
        let mut cloned = self.clone();
        cloned.vars.insert(name.into(), value);
        cloned
    }
    pub fn with_fn<N: Into<String>>(&self, name: N, value: EvalFn) -> Self {
        let mut cloned = self.clone();
        cloned.fns.insert(name.into(), value);
        cloned
    }

    pub fn merge(&self, other: Self) -> Self {
        let mut cloned = self.clone();
        for fun in other.fns {
            cloned.fns.insert(fun.0, fun.1);
        }
        for var in other.vars {
            cloned.vars.insert(var.0, var.1);
        }
        cloned
    }
}

#[derive(Debug, Clone)]
pub struct NoSuchIdentError {
    pub ident: String,
}

impl Error for NoSuchIdentError {}
impl Display for NoSuchIdentError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_fmt(format_args!("No such identifier: {}", self.ident))
    }
}

#[derive(PartialEq, Debug, Clone)]
pub enum EvalResult<'a> {
    String(String),
    Array(Vec<EvalResult<'a>>),
    Object(Vec<(String, EvalResult<'a>)>),
    Number(f64),
    Boolean(bool),
    Quoted(Box<ExprValue<'a>>, EvalContext<'a>),
    None,
}

#[derive(Clone, Debug)]
enum Callable<'a> {
    Fn(EvalFn),
    Quote(EvalResult<'a>),
}

impl<'a> From<EvalResult<'a>> for ExprValue<'a> {
    fn from(val: EvalResult<'a>) -> Self {
        match val {
            EvalResult::String(s) => ExprValue::String(s),
            EvalResult::Array(eval_results) => {
                ExprValue::Array(eval_results.iter().map(|r| r.clone().into()).collect())
            }
            EvalResult::Object(items) => ExprValue::Object(
                items
                    .iter()
                    .map(|(k, v)| (k.clone(), v.clone().into()))
                    .collect(),
            ),
            EvalResult::Number(f) => ExprValue::Number(f),
            EvalResult::Boolean(b) => ExprValue::Boolean(b),
            EvalResult::Quoted(expr_value, ctx) => ExprValue::QuoteWithCtx(expr_value, ctx),
            EvalResult::None => ExprValue::Null,
        }
    }
}

impl<'a> Display for EvalResult<'a> {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::String(s) => f.write_str(s),
            EvalResult::Array(v) => {
                f.write_str("[")?;
                for item in v {
                    item.fmt(f)?;

                    if item != v.last().unwrap() {
                        f.write_str(", ")?;
                    }
                }
                f.write_str("]")
            }
            EvalResult::Object(items) => {
                f.write_str("{")?;
                for item in items {
                    f.write_fmt(format_args!("{}: {}", item.0, item.1))?;
                }
                f.write_str("}")
            }
            EvalResult::Number(n) => f.write_fmt(format_args!("{n}")),
            EvalResult::Boolean(b) => f.write_fmt(format_args!("{b}")),
            EvalResult::Quoted(q, _) => f.write_fmt(format_args!("'{q:?}")),
            EvalResult::None => f.write_str("null"),
        }
    }
}

fn resolve_identifier<'a, R>(
    i: String,
    map: &'a HashMap<String, R>,
    pkgs: Option<&ExprValue<'a>>,
    ctx: &EvalContext<'a>,
) -> Result<&'a R, NoSuchIdentError> {
    map.get(&i)
        .or_else(|| {
            pkgs.and_then(|pkgs| {
                if let Ok(EvalResult::Array(arr)) = eval(pkgs.clone(), ctx.clone()) {
                    arr.iter()
                        .map(|pkg| {
                            if let EvalResult::String(s) = pkg {
                                map.get(&format!("{}:{}", s, i))
                            } else {
                                None
                            }
                        })
                        .find(|o| o.is_some())
                } else {
                    None
                }
            })
            .flatten()
        })
        .ok_or(NoSuchIdentError {
            ident: i.to_string(),
        })
}

pub fn eval<'a>(
    value: ExprValue<'a>,
    ctx: EvalContext<'a>,
) -> Result<EvalResult<'a>, NoSuchIdentError> {
    Ok(match value {
        ExprValue::String(s) => EvalResult::String(s.to_string()),
        ExprValue::Array(a) => EvalResult::Array(
            a.iter()
                .map(|v| eval(v.clone(), ctx.clone()))
                .collect::<Result<Vec<_>, _>>()?,
        ),
        ExprValue::Object(items) => EvalResult::Object(
            items
                .iter()
                .map(|(key, val)| Ok((key.to_string(), eval(val.clone(), ctx.clone())?)))
                .collect::<Result<Vec<_>, _>>()?,
        ),
        ExprValue::Number(n) => EvalResult::Number(n),
        ExprValue::Boolean(b) => EvalResult::Boolean(b),
        ExprValue::Ident(i) => eval(
            resolve_identifier(i.to_string(), &ctx.vars, ctx.vars.get("$use"), &ctx)?.clone(),
            ctx,
        )?,
        ExprValue::FnCall(name, expr_values) => {
            let c = resolve_identifier(name.to_string(), &ctx.fns, ctx.vars.get("$use"), &ctx)
                .map(|f| Callable::Fn(f.clone()))
                .or_else(|e| {
                    let ident_v =
                        resolve_identifier(name.to_string(), &ctx.vars, ctx.vars.get("$use"), &ctx);
                    if let Ok(ExprValue::QuoteWithCtx(q, ctx)) = ident_v {
                        Ok(Callable::Quote(EvalResult::Quoted(q.clone(), ctx.clone())))
                    } else if let Ok(ExprValue::Quote(q)) = ident_v {
                        Ok(Callable::Quote(EvalResult::Quoted(q.clone(), ctx.clone())))
                    } else {
                        Err(e)
                    }
                })?;
            let args = if let EvalResult::Array(args) = eval(expr_values.as_ref().clone(), ctx)? {
                args
            } else {
                vec![]
            };

            match c {
                Callable::Fn(f) => f(args),
                Callable::Quote(q) => call(vec![q, EvalResult::Array(args)]),
            }
        }
        ExprValue::Quote(q) => EvalResult::Quoted(q, ctx.clone()),
        ExprValue::QuoteWithCtx(q, cx) => EvalResult::Quoted(q, cx),
        ExprValue::Null => EvalResult::None,
    })
}

#[cfg(test)]
pub(crate) fn test<'a>(args: Vec<EvalResult>) -> EvalResult {
    if let Some(EvalResult::String(str)) = args.first()
        && let Some(EvalResult::Number(n)) = args.get(1)
    {
        EvalResult::String(format!("{str} {n}"))
    } else {
        EvalResult::None
    }
}

#[cfg(test)]
mod tests {
    use crate::eval::test;
    use std::collections::HashMap;

    use crate::{
        eval::{EvalContext, EvalFn, EvalResult, eval},
        parser::ExprValue,
    };

    #[test]
    fn eval_tree_test() {
        let mut fns = HashMap::new();
        fns.insert("test".to_string(), test as EvalFn);
        let mut vars = HashMap::new();
        vars.insert("hello".to_string(), ExprValue::String("world".into()));
        assert_eq!(
            eval(
                ExprValue::FnCall(
                    "test",
                    Box::new(ExprValue::Array(vec![
                        ExprValue::Ident("hello"),
                        ExprValue::Number(3.14)
                    ]))
                ),
                EvalContext { fns, vars }
            )
            .unwrap(),
            EvalResult::String("world 3.14".to_string())
        );
    }
}