Skip to main content

blue_lang_runtime/
hosted.rs

1//! Embedding blue in a Rust host: load a program into an interpreter the host
2//! built, then call its definitions by name.
3//!
4//! [`crate::pipeline::run_in_surface`] evaluates with a hostless interpreter
5//! and returns the last value, which suits `blue run` and nothing that wants a
6//! program to be a library of entry points. A host (arnes's slash commands and
7//! tools are the first) needs three things that path cannot give: its own
8//! primitives, closures that reach its state through `&mut H`, and a call into
9//! one named function with arguments.
10//!
11//! The stages before evaluation are [`crate::pipeline::prepare`], the same
12//! function `run_in_surface` uses, so a hosted program is checked and erased
13//! exactly as a run one is. Only the interpreter differs: it is built by
14//! [`crate::interpreter`] with the host's type, and the host installs its
15//! primitives on it before the program's top-level forms run.
16
17use std::sync::Arc;
18
19use tatara_lisp_eval::{Interpreter, Value};
20
21use crate::pipeline::{eval_prepared, prepare, RunError};
22use crate::uses::{Entry, Loader};
23
24/// A loaded blue program whose definitions a host can call.
25pub struct Hosted<H: 'static> {
26    interp: Interpreter<H>,
27}
28
29/// Parse, check and erase `entry`, build a blue interpreter over `H`, let
30/// `install` register the host's primitives on it, then evaluate the
31/// program's top-level forms (its `def`s) with `host`.
32///
33/// # Errors
34///
35/// Every [`RunError`] the pipeline has: parse, import, type and runtime
36/// errors, the last two rendered `file:line:col: message`.
37pub fn load_hosted<H: 'static>(
38    entry: Entry<'_>,
39    loader: &dyn Loader,
40    host: &mut H,
41    install: impl FnOnce(&mut Interpreter<H>),
42) -> Result<Hosted<H>, RunError> {
43    let prepared = prepare(entry, loader, None)?;
44    let mut interp = crate::interpreter(host);
45    install(&mut interp);
46    eval_prepared(&mut interp, &prepared, host)?;
47    Ok(Hosted { interp })
48}
49
50impl<H: 'static> Hosted<H> {
51    /// Whether the program defines a callable named `name`.
52    #[must_use]
53    pub fn defines(&self, name: &str) -> bool {
54        matches!(
55            self.interp.lookup_global(name),
56            Some(Value::Closure(_) | Value::NativeFn(_))
57        )
58    }
59
60    /// Call the definition `name` with `args`.
61    ///
62    /// # Errors
63    ///
64    /// [`RunError::Eval`] naming the function when it is not defined or is not
65    /// callable, or with the message it raised.
66    pub fn call(&mut self, name: &str, args: Vec<Value>, host: &mut H) -> Result<Value, RunError> {
67        let callee = match self.interp.lookup_global(name) {
68            Some(v @ (Value::Closure(_) | Value::NativeFn(_))) => v,
69            Some(other) => {
70                return Err(RunError::Eval(format!(
71                    "{name} is not a function (it is {})",
72                    kind(&other)
73                )))
74            }
75            None => return Err(RunError::Eval(format!("no function named {name}"))),
76        };
77        self.interp
78            .apply_external_value(&callee, args, host, tatara_lisp::Span::synthetic())
79            .map_err(|e| RunError::Eval(format!("{name}: {}", e.short_message())))
80    }
81}
82
83/// A string argument, the common case for a host passing text in.
84#[must_use]
85pub fn text(s: &str) -> Value {
86    Value::Str(Arc::from(s))
87}
88
89fn kind(v: &Value) -> &'static str {
90    match v {
91        Value::Nil => "nil",
92        Value::Bool(_) => "a boolean",
93        Value::Int(_) | Value::Float(_) => "a number",
94        Value::Str(_) => "a string",
95        Value::List(_) => "a list",
96        Value::Map(_) => "a map",
97        _ => "a value",
98    }
99}
100
101#[cfg(test)]
102mod tests {
103    use super::*;
104    use crate::uses::NoLoader;
105    use tatara_lisp_eval::Arity;
106
107    /// What a host sees of the program: the notes it was sent.
108    #[derive(Default)]
109    struct Notes(Vec<String>);
110
111    fn install(interp: &mut Interpreter<Notes>) {
112        interp.register_fn(
113            "note",
114            Arity::Exact(1),
115            |args: &[Value], h: &mut Notes, _span| {
116                if let Value::Str(s) = &args[0] {
117                    h.0.push(s.to_string());
118                }
119                Ok(Value::Nil)
120            },
121        );
122    }
123
124    fn load(src: &str, host: &mut Notes) -> Result<Hosted<Notes>, RunError> {
125        load_hosted(Entry::anonymous(src), &NoLoader, host, install)
126    }
127
128    fn as_text(v: &Value) -> String {
129        match v {
130            Value::Str(s) => s.to_string(),
131            other => format!("{other:?}"),
132        }
133    }
134
135    #[test]
136    fn an_empty_program_defines_nothing() {
137        let mut host = Notes::default();
138        let mut p = load("", &mut host).expect("an empty program loads");
139        assert!(!p.defines("run"));
140        let err = p.call("run", vec![], &mut host).unwrap_err().to_string();
141        assert!(err.contains("no function named run"), "{err}");
142    }
143
144    #[test]
145    fn a_call_returns_the_function_value_for_its_arguments() {
146        let mut host = Notes::default();
147        let src = "def greet(who)\n  \"hello #{who}\"\nend\n";
148        let mut p = load(src, &mut host).unwrap();
149        assert!(p.defines("greet"));
150        let v = p.call("greet", vec![text("blue")], &mut host).unwrap();
151        assert_eq!(as_text(&v), "hello blue");
152        // Identity: two calls with the same argument agree.
153        let again = p.call("greet", vec![text("blue")], &mut host).unwrap();
154        assert_eq!(as_text(&again), as_text(&v));
155    }
156
157    #[test]
158    fn host_primitives_reach_the_host_state() {
159        let mut host = Notes::default();
160        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";
161        let mut p = load(src, &mut host).unwrap();
162        let v = p.call("run", vec![text("abcd")], &mut host).unwrap();
163        assert!(matches!(v, Value::Int(4)), "{v:?}");
164        assert_eq!(host.0, vec!["saw abcd".to_owned(), "done".to_owned()]);
165    }
166
167    #[test]
168    fn a_type_error_stops_the_load_before_anything_runs() {
169        let mut host = Notes::default();
170        let src = "note(\"top level ran\")\ndef f(x: Int) -> Int\n  \"no\"\nend\n";
171        let err = load(src, &mut host).err().expect("refused").to_string();
172        assert!(err.contains("type error"), "{err}");
173        assert!(
174            host.0.is_empty(),
175            "no top-level form may run before the check passes"
176        );
177    }
178
179    #[test]
180    fn a_raise_and_a_non_function_are_typed_errors_naming_the_function() {
181        let mut host = Notes::default();
182        let src = "LIMIT = 3\ndef boom()\n  raise(\"bad\")\nend\n";
183        let mut p = load(src, &mut host).unwrap();
184        let raised = p.call("boom", vec![], &mut host).unwrap_err().to_string();
185        assert!(raised.contains("boom"), "{raised}");
186        let not_fn = p.call("LIMIT", vec![], &mut host).unwrap_err().to_string();
187        assert!(not_fn.contains("not a function"), "{not_fn}");
188    }
189}