pub mod alloc;
pub mod collection;
pub mod crypto;
pub mod dispatch;
pub mod doc_sources;
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;
#[cfg(feature = "sys")]
pub mod provide;
pub mod receipt;
pub mod session;
pub mod spread;
pub mod stdlib;
#[cfg(feature = "sys")]
pub mod sys;
pub mod timings;
pub mod uses;
pub mod xml;
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;
pub use blue_lang_proc::{Frame, HostIo, Identities, Receipt};
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);
xml::install_xml_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) {
attach_node_held(interp, &Hold::from(frame.clone()));
}
pub fn attach_node_held<H: 'static>(interp: &mut Interpreter<H>, hold: &Hold) {
attach_node_accounted(interp, Some(hold), &Account::default());
}
pub fn attach_node_accounted<H: 'static>(
interp: &mut Interpreter<H>,
hold: Option<&Hold>,
account: &Account,
) {
let mut config = node_config();
config.host_io = account.host_io.clone();
config.identities = account.identities.clone();
if let Some(hold) = hold {
config.frame = Some(hold.frame());
config.receipts = account.receipts.clone().map(receipt::sink);
let held = hold.clone();
let io = account.host_io.clone();
config.child = std::sync::Arc::new(move || {
let mut child = child_interpreter();
held.bind(&mut child, "this process's frame", &io);
child
});
}
dispatch::install(interp);
blue_lang_proc::install(interp, blue_lang_proc::Node::new(config));
}
#[derive(Clone, Default)]
pub struct Account {
pub host_io: HostIo,
pub identities: Identities,
pub identify: Option<Identify>,
pub receipts: Option<std::path::PathBuf>,
}
pub type Identified =
Box<dyn FnOnce() -> std::collections::BTreeMap<String, String> + Send + 'static>;
pub type Identify = fn(
&uses::ResolvedProgram,
std::sync::Arc<Vec<tatara_lisp::Spanned>>,
Option<String>,
) -> Identified;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Hold {
pub frame: blue_lang_waku::Waku,
pub provisions: blue_lang_waku::Provisions,
}
impl From<blue_lang_waku::Waku> for Hold {
fn from(frame: blue_lang_waku::Waku) -> Self {
Self {
frame,
provisions: blue_lang_waku::Provisions::default(),
}
}
}
#[must_use]
pub fn provided_name(word: &str) -> String {
format!("%provided/{word}")
}
impl Hold {
#[must_use]
pub fn withheld(&self) -> Vec<blue_lang_waku::Capability> {
let reach = self.provisions.widen(&self.frame.reach);
blue_lang_waku::Capability::host_effects()
.into_iter()
.filter(|cap| !reach.grants(*cap))
.collect()
}
#[must_use]
pub fn granted(&self) -> Vec<blue_lang_waku::Capability> {
blue_lang_waku::Capability::host_effects()
.into_iter()
.filter(|cap| {
self.frame.reach.grants(*cap)
&& self.provisions.get(*cap).is_none_or(|p| p.scope.is_some())
})
.collect()
}
#[must_use]
pub fn withheld_host_names(&self) -> Vec<(&'static str, blue_lang_waku::Capability)> {
names_of(self.withheld())
}
#[must_use]
pub fn granted_host_names(&self) -> Vec<(&'static str, blue_lang_waku::Capability)> {
names_of(self.granted())
}
#[must_use]
pub fn frame(&self) -> Frame {
Frame {
granted: self.granted(),
provided: self.provisions.iter().cloned().collect(),
withheld: self.withheld(),
}
}
#[must_use]
pub fn frame_summary(&self, host_io: u64) -> serde_json::Value {
self.frame().block(host_io)
}
pub fn bind<H: 'static>(&self, interp: &mut Interpreter<H>, who: &'static str, io: &HostIo) {
refuse_host_names(interp, &self.withheld_host_names(), who);
#[cfg(feature = "sys")]
provide::provide(interp, &self.provisions);
count_host_names(interp, &self.granted_host_names(), io);
}
pub fn rebind_scoped(
&self,
program: &uses::ResolvedProgram,
forms: &mut [tatara_lisp::Spanned],
) -> Vec<(&'static str, blue_lang_waku::Capability)> {
let served: Vec<(
blue_lang_waku::Capability,
std::collections::BTreeSet<String>,
)> = self
.provisions
.scoped()
.map(|(cap, named)| (cap, program.reached_from(named)))
.collect();
let serves = |package: Option<&str>, cap: blue_lang_waku::Capability| {
package.is_some_and(|p| served.iter().any(|(c, s)| *c == cap && s.contains(p)))
};
if !served.is_empty() {
uses::rename_written(program, forms, &|package, name| {
let cap = blue_lang_waku::Provision::capability_of(name)?;
serves(package, cap).then(|| provided_name(name))
});
}
let entry = program
.file(uses::ResolvedProgram::ENTRY)
.and_then(|f| f.package.as_deref());
self.withheld_host_names()
.into_iter()
.filter(|(_, cap)| !serves(entry, *cap))
.collect()
}
}
fn names_of(
caps: Vec<blue_lang_waku::Capability>,
) -> Vec<(&'static str, blue_lang_waku::Capability)> {
caps.into_iter()
.flat_map(|cap| cap.names().into_iter().map(move |n| (n, cap)))
.collect()
}
fn count_host_names<H: 'static>(
interp: &mut Interpreter<H>,
granted: &[(&'static str, blue_lang_waku::Capability)],
io: &HostIo,
) {
for &(name, _) in granted {
let Some(original @ tatara_lisp_eval::Value::NativeFn(_)) = interp.lookup_global(name)
else {
continue;
};
let tatara_lisp_eval::Value::NativeFn(nf) = &original else {
continue;
};
let arity = nf.arity;
let io = io.clone();
let shim = format!("%io:{name}");
interp.register_higher_order_fn(
shim.clone(),
tatara_lisp_eval::ffi::Arity::Any,
move |args: &[tatara_lisp_eval::Value],
host: &mut H,
caller: &tatara_lisp_eval::ffi::Caller<H>,
span| {
if arity.check(args.len()).is_ok() {
io.tally();
}
caller.apply_value(&original, args.to_vec(), host, span)
},
);
if let Some(v) = interp.lookup_global(&shim) {
interp.define_global(name, v);
}
}
}
#[must_use]
pub fn ungranted_host_names(
frame: &blue_lang_waku::Waku,
) -> Vec<(&'static str, blue_lang_waku::Capability)> {
Hold::from(frame.clone()).withheld_host_names()
}
pub const BREACH: &str = "capability-breach";
pub const BREACH_EXIT: u8 = 77;
#[must_use]
pub fn breach(
name: &str,
cap: blue_lang_waku::Capability,
who: &str,
at: tatara_lisp::Span,
) -> tatara_lisp_eval::EvalError {
tatara_lisp_eval::EvalError::fatal(
BREACH,
format!(
"`{name}` reaches the host ({}), which {who} does not grant",
cap.label()
),
at,
)
}
#[must_use]
pub fn is_breach(e: &tatara_lisp_eval::EvalError) -> bool {
matches!(e, tatara_lisp_eval::EvalError::Fatal { kind, .. } if &**kind == BREACH)
}
pub fn refuse_host_names<H: 'static>(
interp: &mut Interpreter<H>,
refused: &[(&'static str, blue_lang_waku::Capability)],
who: &'static str,
) {
refuse_host_names_noting(interp, refused, who, |_, _, _| {});
}
pub fn refuse_host_names_noting<H: 'static>(
interp: &mut Interpreter<H>,
refused: &[(&'static str, blue_lang_waku::Capability)],
who: &'static str,
note: fn(&mut H, blue_lang_waku::Capability, &'static str),
) {
for &(name, cap) in refused {
if interp.lookup_global(name).is_none() {
continue;
}
interp.register_fn(
name,
tatara_lisp_eval::ffi::Arity::Any,
move |_: &[tatara_lisp_eval::Value], host: &mut H, span| {
note(host, cap, name);
Err(breach(name, cap, who, span))
},
);
}
}
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(timings::stage(timings::Stage::Runtime, || {
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(_)));
}
#[cfg(feature = "sys")]
mod held {
use super::super::*;
use blue_lang_waku::{Capability, Provided, Provision, Provisions};
use tatara_lisp_eval::Value;
fn held(host: &[Capability], provided: &[Provided]) -> (Hold, Interpreter<()>, HostIo) {
let mut provisions = Provisions::default();
for p in provided {
provisions.add(p.clone()).expect("one provider each");
}
let hold = Hold {
frame: session::frame_granting(host),
provisions,
};
let mut interp = interpreter_hostless();
let io = HostIo::new();
hold.bind(&mut interp, "this test's frame", &io);
(hold, interp, io)
}
fn call(interp: &mut Interpreter<()>, src: &str) -> Result<Value, String> {
let forms = tatara_lisp::read_spanned(src).expect("read");
interp
.eval_program(&forms, &mut ())
.map_err(|e| e.to_string())
}
#[test]
fn a_pure_hold_shims_no_name_so_nothing_can_tally() {
let (hold, mut interp, io) = held(&[], &[]);
assert!(hold.granted_host_names().is_empty());
for (name, _) in hold.withheld_host_names() {
assert!(
interp.lookup_global(&format!("%io:{name}")).is_none(),
"`{name}` is withheld, so it has no counting shim"
);
}
assert!(call(&mut interp, "(getenv \"HOME\")").is_err());
assert!(call(&mut interp, "(now_ms)").is_err());
assert_eq!(io.total(), 0);
}
#[test]
fn a_granted_name_tallies_each_call_and_a_provided_or_withheld_one_none() {
let (hold, mut interp, io) = held(
&[Capability::Environment],
&[Provision::DeterministicStepped {
at_ms: 10,
step_ms: 1,
}
.into()],
);
assert_eq!(hold.granted(), [Capability::Environment]);
call(&mut interp, "(getenv \"HOME\")").expect("granted");
call(&mut interp, "(argv)").expect("granted");
assert_eq!(io.total(), 2, "two calls through granted names");
assert_eq!(
call(&mut interp, "(now_ms)").expect("provided").to_string(),
"10"
);
assert!(call(&mut interp, "(read_file \"/etc/hosts\")").is_err());
assert_eq!(io.total(), 2, "a provider and a breach reach no host");
let wrong = call(&mut interp, "(getenv)").expect_err("arity");
assert!(
wrong.contains("getenv"),
"the original names itself: {wrong}"
);
assert_eq!(
io.total(),
2,
"a call refused for its arity reached no host"
);
}
#[test]
fn an_unrestricted_hold_shims_every_host_name_but_a_provided_one() {
let hold = Hold {
frame: blue_lang_waku::Waku::top(),
provisions: {
let mut p = Provisions::default();
p.add(Provision::NullSink).expect("one");
p
},
};
assert!(hold.withheld().is_empty());
assert!(!hold.granted().contains(&Capability::FileSystem));
assert_eq!(hold.granted().len(), Capability::host_effects().len() - 1);
let f = hold.frame();
assert_eq!(f.provided, [Provided::from(Provision::NullSink)]);
assert_eq!(f.granted, hold.granted());
}
#[test]
fn a_scoped_provider_binds_its_alias_uncounted_and_leaves_the_host_name_counted() {
let scoped = Provided {
provision: Provision::NullSink,
scope: Some(std::collections::BTreeSet::from(["kazu".to_string()])),
};
let (hold, mut interp, io) =
held(&[Capability::FileSystem], std::slice::from_ref(&scoped));
assert_eq!(hold.granted(), [Capability::FileSystem]);
assert_eq!(hold.frame().provided, [scoped]);
let alias = provided_name("read_file");
assert!(
interp.lookup_global(&alias).is_some(),
"the scoped alias is bound"
);
let _ = call(&mut interp, &format!("({alias} \"/etc/hosts\")"));
assert_eq!(io.total(), 0, "the provider reaches no host");
call(&mut interp, "(read_file \"/etc/hosts\")").expect("granted");
assert_eq!(io.total(), 1, "code outside the scope reaches the host");
}
#[test]
fn the_frame_summary_is_exactly_granted_withheld_and_host_io() {
let (hold, _, _) = held(&[Capability::Clock], &[]);
assert_eq!(
blue_lang_proc::receipt::canonical(&hold.frame_summary(4)),
r#"{"granted":["clock"],"host_io":4,"withheld":["process","filesystem","environment","network","entropy"]}"#
);
}
}
#[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"
);
}
}