use std::sync::Arc;
use tatara_lisp_eval::{Interpreter, Value};
use crate::pipeline::{builtin_names, eval_prepared, prepare, RunError};
use crate::uses::{Entry, Loader};
pub struct Hosted<H: 'static> {
interp: Interpreter<H>,
entry: blue_lang_check::Namespace,
}
pub fn load_hosted<H: 'static>(
entry: Entry<'_>,
loader: &dyn Loader,
host: &mut H,
install: impl FnOnce(&mut Interpreter<H>),
) -> Result<Hosted<H>, RunError> {
let mut interp = crate::interpreter(host);
install(&mut interp);
let prepared = prepare(entry, loader, None, &builtin_names(&interp))?;
eval_prepared(&mut interp, &prepared, host)?;
let entry = prepared.entry_namespace();
Ok(Hosted { interp, entry })
}
impl<H: 'static> Hosted<H> {
fn lookup(&self, name: &str) -> Option<Value> {
match name.split_once("::") {
Some((blue_lang_syntax::BUILTIN_QUALIFIER, bare)) => self.interp.lookup_global(bare),
Some((pkg, bare)) => self.interp.lookup_global(&blue_lang_check::names::key(
&blue_lang_check::Namespace::Bidama(pkg.to_owned()),
bare,
)),
None => self
.interp
.lookup_global(&blue_lang_check::names::key(&self.entry, name))
.or_else(|| self.interp.lookup_global(name)),
}
}
#[must_use]
pub fn defines(&self, name: &str) -> bool {
matches!(
self.lookup(name),
Some(Value::Closure(_) | Value::NativeFn(_))
)
}
pub fn call(&mut self, name: &str, args: Vec<Value>, host: &mut H) -> Result<Value, RunError> {
let callee = match self.lookup(name) {
Some(v @ (Value::Closure(_) | Value::NativeFn(_))) => v,
Some(other) => {
return Err(RunError::Eval(Box::new(crate::observe::Fault::from(
format!("{name} is not a function (it is {})", kind(&other)),
))))
}
None => {
return Err(RunError::Eval(Box::new(
format!("no function named {name}").into(),
)))
}
};
self.interp
.apply_external_value(&callee, args, host, tatara_lisp::Span::synthetic())
.map_err(|e| RunError::Eval(Box::new(format!("{name}: {}", e.short_message()).into())))
}
}
#[derive(Debug, thiserror::Error)]
pub enum FramedError {
#[error("{0}")]
Run(#[from] RunError),
#[error("it names {} outside its frame: {}", if .names.len() == 1 { "a name" } else { "names" }, .names.join(", "))]
Escapes { names: Vec<String> },
}
#[derive(Debug, thiserror::Error)]
pub enum CallError {
#[error("no function named {0}")]
Undefined(String),
#[error("{name} is not a function (it is {kind})")]
NotAFunction { name: String, kind: &'static str },
#[error("{name}: {message}")]
Exhausted { name: String, message: String },
#[error("{name}: {message}")]
Raised { name: String, message: String },
}
pub fn load_hosted_in_frame<H: 'static>(
entry: Entry<'_>,
loader: &dyn Loader,
host: &mut H,
install: impl FnOnce(&mut Interpreter<H>),
permits: &dyn Fn(&str) -> bool,
budget: tatara_lisp_eval::vm::Budget,
) -> Result<Hosted<H>, FramedError> {
let mut interp = crate::interpreter(host);
install(&mut interp);
let prepared = prepare(entry, loader, None, &builtin_names(&interp))?;
let escapes = blue_lang_waku::free_names(prepared.erased(), permits);
if !escapes.is_empty() {
return Err(FramedError::Escapes {
names: escapes.into_iter().collect(),
});
}
for name in interp.reserved_head_names() {
if !permits(&name) && interp.lookup_global(&name).is_some() {
let refused = name.clone();
interp.register_fn(
name,
tatara_lisp_eval::Arity::Any,
move |_: &[Value], _: &mut H, span| {
Err(tatara_lisp_eval::EvalError::native_fn(
refused.to_string(),
"outside this program's frame",
span,
))
},
);
}
}
interp
.set_budget(budget)
.map_err(|e| RunError::Eval(Box::new(e.to_string().into())))?;
eval_prepared(&mut interp, &prepared, host)?;
let entry = prepared.entry_namespace();
Ok(Hosted { interp, entry })
}
impl<H: 'static> Hosted<H> {
pub fn call_budgeted(
&mut self,
name: &str,
args: Vec<Value>,
host: &mut H,
budget: tatara_lisp_eval::vm::Budget,
) -> Result<Value, CallError> {
let callee = self.callee(name)?;
if let Err(e) = self.interp.set_budget(budget) {
return Err(CallError::Raised {
name: name.to_string(),
message: e.to_string(),
});
}
self.apply(name, &callee, args, host)
}
pub fn try_call(
&mut self,
name: &str,
args: Vec<Value>,
host: &mut H,
) -> Result<Value, CallError> {
let callee = self.callee(name)?;
self.apply(name, &callee, args, host)
}
fn callee(&self, name: &str) -> Result<Value, CallError> {
match self.lookup(name) {
Some(v @ (Value::Closure(_) | Value::NativeFn(_))) => Ok(v),
Some(other) => Err(CallError::NotAFunction {
name: name.to_string(),
kind: kind(&other),
}),
None => Err(CallError::Undefined(name.to_string())),
}
}
fn apply(
&mut self,
name: &str,
callee: &Value,
args: Vec<Value>,
host: &mut H,
) -> Result<Value, CallError> {
self.interp
.apply_external_value(callee, args, host, tatara_lisp::Span::synthetic())
.map_err(|e| match e {
tatara_lisp_eval::EvalError::BudgetExceeded { .. } => CallError::Exhausted {
name: name.to_string(),
message: e.short_message(),
},
other => CallError::Raised {
name: name.to_string(),
message: other.short_message(),
},
})
}
}
#[must_use]
pub fn text(s: &str) -> Value {
Value::Str(Arc::from(s))
}
fn kind(v: &Value) -> &'static str {
match v {
Value::Nil => "nil",
Value::Bool(_) => "a boolean",
Value::Int(_) | Value::Float(_) => "a number",
Value::Str(_) => "a string",
Value::List(_) => "a list",
Value::Map(_) => "a map",
_ => "a value",
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::uses::NoLoader;
use tatara_lisp_eval::Arity;
#[derive(Default)]
struct Notes(Vec<String>);
fn install(interp: &mut Interpreter<Notes>) {
interp.register_fn(
"note",
Arity::Exact(1),
|args: &[Value], h: &mut Notes, _span| {
if let Value::Str(s) = &args[0] {
h.0.push(s.to_string());
}
Ok(Value::Nil)
},
);
}
fn load(src: &str, host: &mut Notes) -> Result<Hosted<Notes>, RunError> {
load_hosted(Entry::anonymous(src), &NoLoader, host, install)
}
fn as_text(v: &Value) -> String {
match v {
Value::Str(s) => s.to_string(),
other => format!("{other:?}"),
}
}
#[test]
fn an_empty_program_defines_nothing() {
let mut host = Notes::default();
let mut p = load("", &mut host).expect("an empty program loads");
assert!(!p.defines("run"));
let err = p.call("run", vec![], &mut host).unwrap_err().to_string();
assert!(err.contains("no function named run"), "{err}");
}
#[test]
fn a_call_returns_the_function_value_for_its_arguments() {
let mut host = Notes::default();
let src = "def greet(who)\n \"hello #{who}\"\nend\n";
let mut p = load(src, &mut host).unwrap();
assert!(p.defines("greet"));
let v = p.call("greet", vec![text("blue")], &mut host).unwrap();
assert_eq!(as_text(&v), "hello blue");
let again = p.call("greet", vec![text("blue")], &mut host).unwrap();
assert_eq!(as_text(&again), as_text(&v));
}
#[test]
fn host_primitives_reach_the_host_state() {
let mut host = Notes::default();
let src = "def run(x)\n note(\"saw #{x}\")\n note(\"done\")\n size_of(x)\nend\ndef size_of(s)\n length(s)\nend\n";
let mut p = load(src, &mut host).unwrap();
let v = p.call("run", vec![text("abcd")], &mut host).unwrap();
assert!(matches!(v, Value::Int(4)), "{v:?}");
assert_eq!(host.0, vec!["saw abcd".to_owned(), "done".to_owned()]);
}
#[test]
fn a_type_error_stops_the_load_before_anything_runs() {
let mut host = Notes::default();
let src = "note(\"top level ran\")\ndef f(x: Int) -> Int\n \"no\"\nend\n";
let err = load(src, &mut host).err().expect("refused").to_string();
assert!(err.contains("error[B0003]"), "{err}");
assert!(
host.0.is_empty(),
"no top-level form may run before the check passes"
);
}
#[test]
fn a_qualified_name_calls_a_bidamas_definition_and_a_bare_one_does_not() {
let mut host = Notes::default();
let loader = crate::uses::tests::MemLoader(
[("tama", "def exports()\n \"tama rows\"\nend\n")]
.into_iter()
.collect(),
);
let mut p = load_hosted(
Entry::anonymous("use(\"tama\")\n"),
&loader,
&mut host,
install,
)
.unwrap();
assert!(p.defines("tama::exports"));
let v = p.call("tama::exports", vec![], &mut host).unwrap();
assert_eq!(as_text(&v), "tama rows");
assert!(!p.defines("exports"));
assert!(!p.defines("other::exports"));
assert!(p.defines("blue::length"));
let err = p
.call("tama::absent", vec![], &mut host)
.unwrap_err()
.to_string();
assert!(err.contains("no function named tama::absent"), "{err}");
}
#[test]
fn a_raise_and_a_non_function_are_typed_errors_naming_the_function() {
let mut host = Notes::default();
let src = "LIMIT = 3\ndef boom()\n throw(error(\"bad\"))\nend\n";
let mut p = load(src, &mut host).unwrap();
let raised = p.call("boom", vec![], &mut host).unwrap_err().to_string();
assert!(raised.contains("boom"), "{raised}");
let not_fn = p.call("LIMIT", vec![], &mut host).unwrap_err().to_string();
assert!(not_fn.contains("not a function"), "{not_fn}");
}
}