pub mod alloc;
pub mod collection;
pub mod crypto;
pub mod dispatch;
pub mod docs;
pub mod domain;
#[cfg(feature = "sys")]
pub mod entropy;
pub mod erase;
pub mod errors;
pub mod host_surface;
pub mod hosted;
pub mod inputs;
pub mod json;
pub mod literal;
pub mod messages;
#[cfg(feature = "sys")]
pub mod net;
pub mod nix;
pub mod observe;
pub mod paths;
pub mod pipeline;
pub mod project_rules;
pub mod session;
pub mod spread;
pub mod stdlib;
#[cfg(feature = "sys")]
pub mod sys;
pub mod uses;
pub use erase::{erase_types, to_sexps};
pub use inputs::{declarations, install_input_primitives, Declaration, InputError, Inputs};
pub use pipeline::{
parse, parse_tree_with_depth, parse_with_depth, run, run_with_inputs, Run, RunError,
};
pub use stdlib::install_blue_stdlib;
use tatara_lisp_eval::{install_full_stdlib_with, Interpreter};
pub fn interpreter<H: 'static>(host: &mut H) -> Interpreter<H> {
let mut interp = Interpreter::new();
install_full_stdlib_with(&mut interp, host);
stdlib::install_blue_stdlib(&mut interp);
json::install_json_stdlib(&mut interp);
crypto::install_crypto_stdlib(&mut interp);
nix::install_nix_stdlib(&mut interp);
#[cfg(feature = "sys")]
sys::install_sys_stdlib(&mut interp);
attach_node(&mut interp);
apply_execution_bounds(&mut interp);
interp
}
pub fn attach_node<H: 'static>(interp: &mut Interpreter<H>) {
dispatch::install(interp);
blue_lang_proc::install(interp, blue_lang_proc::Node::new(node_config()));
}
pub fn attach_node_in_frame<H: 'static>(interp: &mut Interpreter<H>, frame: &blue_lang_waku::Waku) {
let mut config = node_config();
let refused: Vec<(&'static str, &'static str)> = blue_lang_waku::Capability::host_effects()
.into_iter()
.filter(|cap| !frame.reach.grants(*cap))
.flat_map(|cap| cap.names().into_iter().map(move |n| (n, cap.label())))
.collect();
config.child = std::sync::Arc::new(move || {
let mut child = child_interpreter();
for (name, label) in &refused {
if child.lookup_global(name).is_none() {
continue;
}
let name: &'static str = name;
let label: &'static str = label;
child.register_fn(name, tatara_lisp_eval::ffi::Arity::Any, move |_: &[tatara_lisp_eval::Value], _: &mut (), span| {
Err(tatara_lisp_eval::EvalError::native_fn(
name,
format!("refused: `{name}` reaches the host ({label}), which this process's frame does not grant"),
span,
))
});
}
child
});
dispatch::install(interp);
blue_lang_proc::install(interp, blue_lang_proc::Node::new(config));
}
fn node_config() -> blue_lang_proc::NodeConfig {
let b = execution_bounds();
let mut config = blue_lang_proc::NodeConfig::new(std::sync::Arc::new(child_interpreter));
config.budget = tatara_lisp_eval::vm::Budget {
fuel: b.max_steps,
max_depth: Some(b.max_call_depth),
quantum: Some(blue_lang_proc::node::DEFAULT_QUANTUM),
};
config.clock = if cfg!(feature = "sys") {
blue_lang_proc::ClockKind::Real
} else {
blue_lang_proc::ClockKind::Virtual
};
config
}
fn child_interpreter() -> Interpreter<()> {
let mut interp = match HOSTLESS_BASE.lock() {
Ok(base) => base.fork(),
Err(_) => interpreter(&mut ()),
};
apply_execution_bounds(&mut interp);
interp
}
static EXECUTION_BOUNDS: std::sync::OnceLock<ExecutionBounds> = std::sync::OnceLock::new();
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ExecutionBounds {
pub max_call_depth: usize,
pub max_steps: Option<usize>,
}
impl ExecutionBounds {
pub const DEFAULT: Self = Self {
max_call_depth: tatara_lisp_eval::vm::DEFAULT_MAX_DEPTH,
max_steps: None,
};
}
pub fn set_execution_bounds(bounds: ExecutionBounds) {
let _ = EXECUTION_BOUNDS.set(bounds);
}
#[must_use]
pub fn execution_bounds() -> ExecutionBounds {
EXECUTION_BOUNDS
.get()
.copied()
.unwrap_or(ExecutionBounds::DEFAULT)
}
fn apply_execution_bounds<H: 'static>(interp: &mut Interpreter<H>) {
let b = execution_bounds();
let _ = interp.set_budget(tatara_lisp_eval::vm::Budget {
fuel: b.max_steps,
max_depth: Some(b.max_call_depth),
quantum: None,
});
}
static HOSTLESS_BASE: std::sync::LazyLock<std::sync::Mutex<Interpreter<()>>> =
std::sync::LazyLock::new(|| std::sync::Mutex::new(interpreter(&mut ())));
pub fn interpreter_hostless() -> Interpreter<()> {
let mut interp = match HOSTLESS_BASE.lock() {
Ok(base) => base.fork(),
Err(_) => interpreter(&mut ()),
};
apply_execution_bounds(&mut interp);
attach_node(&mut interp);
interp
}
#[must_use]
pub fn lower_to_spanned(forms: &[tatara_lisp::Sexp]) -> Vec<tatara_lisp::Spanned> {
forms
.iter()
.map(tatara_lisp::Spanned::from_sexp_synthetic)
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use tatara_lisp_eval::Value;
fn eval(src: &str) -> Value {
let forms = tatara_lisp::read_spanned(src).expect("read");
let mut interp = interpreter_hostless();
interp.eval_program(&forms, &mut ()).expect("eval")
}
#[test]
fn both_layers_are_installed() {
assert!(matches!(eval("(+ 1 2)"), Value::Int(3)));
assert!(matches!(eval("(mod 7 3)"), Value::Int(1)));
assert!(matches!(eval("(inc 41)"), Value::Int(42)));
assert!(matches!(eval("(first (list 9 8))"), Value::Int(9)));
assert!(matches!(eval(r#"(length "héllo")"#), Value::Int(5)));
assert!(matches!(
eval(r#"(json_parse "{\"outcome\":\"ok\"}")"#),
Value::List(_)
));
assert!(matches!(eval(r#"(blake3_hex "")"#), Value::Str(_)));
}
#[test]
fn a_bare_interpreter_lacks_the_stdlib() {
let forms = tatara_lisp::read_spanned("(mod 7 3)").expect("read");
let mut bare = Interpreter::new();
tatara_lisp_eval::install_primitives(&mut bare);
assert!(
bare.eval_program(&forms, &mut ()).is_err(),
"if a bare interpreter already resolved `mod`, this crate would be measuring nothing"
);
}
}