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::{builtin_names, 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    /// The entry file's namespace: its definitions run under this
28    /// namespace's keys (`%root/f` for a script), and a host names them bare.
29    entry: blue_lang_check::Namespace,
30}
31
32/// Parse, check and erase `entry`, build a blue interpreter over `H`, let
33/// `install` register the host's primitives on it, then evaluate the
34/// program's top-level forms (its `def`s) with `host`.
35///
36/// # Errors
37///
38/// Every [`RunError`] the pipeline has: parse, import, type and runtime
39/// errors, the last two rendered `file:line:col: message`.
40pub fn load_hosted<H: 'static>(
41    entry: Entry<'_>,
42    loader: &dyn Loader,
43    host: &mut H,
44    install: impl FnOnce(&mut Interpreter<H>),
45) -> Result<Hosted<H>, RunError> {
46    // The host's primitives are installed BEFORE the check stage, so names
47    // the host binds resolve: the stage reads its name table off this
48    // interpreter. Installing is not evaluation; the stage order holds.
49    let mut interp = crate::interpreter(host);
50    install(&mut interp);
51    let prepared = prepare(entry, loader, None, &builtin_names(&interp))?;
52    eval_prepared(&mut interp, &prepared, host)?;
53    let entry = prepared.entry_namespace();
54    Ok(Hosted { interp, entry })
55}
56
57impl<H: 'static> Hosted<H> {
58    /// What `name` is bound to: the entry's definition of it (under its
59    /// runtime key), else a global of that name (a host primitive, a builtin).
60    fn lookup(&self, name: &str) -> Option<Value> {
61        self.interp
62            .lookup_global(&blue_lang_check::names::key(&self.entry, name))
63            .or_else(|| self.interp.lookup_global(name))
64    }
65
66    /// Whether the program defines a callable named `name`.
67    #[must_use]
68    pub fn defines(&self, name: &str) -> bool {
69        matches!(
70            self.lookup(name),
71            Some(Value::Closure(_) | Value::NativeFn(_))
72        )
73    }
74
75    /// Call the definition `name` with `args`.
76    ///
77    /// # Errors
78    ///
79    /// [`RunError::Eval`] naming the function when it is not defined or is not
80    /// callable, or with the message it raised.
81    pub fn call(&mut self, name: &str, args: Vec<Value>, host: &mut H) -> Result<Value, RunError> {
82        let callee = match self.lookup(name) {
83            Some(v @ (Value::Closure(_) | Value::NativeFn(_))) => v,
84            Some(other) => {
85                return Err(RunError::Eval(format!(
86                    "{name} is not a function (it is {})",
87                    kind(&other)
88                )))
89            }
90            None => return Err(RunError::Eval(format!("no function named {name}"))),
91        };
92        self.interp
93            .apply_external_value(&callee, args, host, tatara_lisp::Span::synthetic())
94            .map_err(|e| RunError::Eval(format!("{name}: {}", e.short_message())))
95    }
96}
97
98/// A string argument, the common case for a host passing text in.
99#[must_use]
100pub fn text(s: &str) -> Value {
101    Value::Str(Arc::from(s))
102}
103
104fn kind(v: &Value) -> &'static str {
105    match v {
106        Value::Nil => "nil",
107        Value::Bool(_) => "a boolean",
108        Value::Int(_) | Value::Float(_) => "a number",
109        Value::Str(_) => "a string",
110        Value::List(_) => "a list",
111        Value::Map(_) => "a map",
112        _ => "a value",
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119    use crate::uses::NoLoader;
120    use tatara_lisp_eval::Arity;
121
122    /// What a host sees of the program: the notes it was sent.
123    #[derive(Default)]
124    struct Notes(Vec<String>);
125
126    fn install(interp: &mut Interpreter<Notes>) {
127        interp.register_fn(
128            "note",
129            Arity::Exact(1),
130            |args: &[Value], h: &mut Notes, _span| {
131                if let Value::Str(s) = &args[0] {
132                    h.0.push(s.to_string());
133                }
134                Ok(Value::Nil)
135            },
136        );
137    }
138
139    fn load(src: &str, host: &mut Notes) -> Result<Hosted<Notes>, RunError> {
140        load_hosted(Entry::anonymous(src), &NoLoader, host, install)
141    }
142
143    fn as_text(v: &Value) -> String {
144        match v {
145            Value::Str(s) => s.to_string(),
146            other => format!("{other:?}"),
147        }
148    }
149
150    #[test]
151    fn an_empty_program_defines_nothing() {
152        let mut host = Notes::default();
153        let mut p = load("", &mut host).expect("an empty program loads");
154        assert!(!p.defines("run"));
155        let err = p.call("run", vec![], &mut host).unwrap_err().to_string();
156        assert!(err.contains("no function named run"), "{err}");
157    }
158
159    #[test]
160    fn a_call_returns_the_function_value_for_its_arguments() {
161        let mut host = Notes::default();
162        let src = "def greet(who)\n  \"hello #{who}\"\nend\n";
163        let mut p = load(src, &mut host).unwrap();
164        assert!(p.defines("greet"));
165        let v = p.call("greet", vec![text("blue")], &mut host).unwrap();
166        assert_eq!(as_text(&v), "hello blue");
167        // Identity: two calls with the same argument agree.
168        let again = p.call("greet", vec![text("blue")], &mut host).unwrap();
169        assert_eq!(as_text(&again), as_text(&v));
170    }
171
172    #[test]
173    fn host_primitives_reach_the_host_state() {
174        let mut host = Notes::default();
175        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";
176        let mut p = load(src, &mut host).unwrap();
177        let v = p.call("run", vec![text("abcd")], &mut host).unwrap();
178        assert!(matches!(v, Value::Int(4)), "{v:?}");
179        assert_eq!(host.0, vec!["saw abcd".to_owned(), "done".to_owned()]);
180    }
181
182    #[test]
183    fn a_type_error_stops_the_load_before_anything_runs() {
184        let mut host = Notes::default();
185        let src = "note(\"top level ran\")\ndef f(x: Int) -> Int\n  \"no\"\nend\n";
186        let err = load(src, &mut host).err().expect("refused").to_string();
187        assert!(err.contains("error[B0003]"), "{err}");
188        assert!(
189            host.0.is_empty(),
190            "no top-level form may run before the check passes"
191        );
192    }
193
194    #[test]
195    fn a_raise_and_a_non_function_are_typed_errors_naming_the_function() {
196        let mut host = Notes::default();
197        let src = "LIMIT = 3\ndef boom()\n  throw(error(\"bad\"))\nend\n";
198        let mut p = load(src, &mut host).unwrap();
199        let raised = p.call("boom", vec![], &mut host).unwrap_err().to_string();
200        assert!(raised.contains("boom"), "{raised}");
201        let not_fn = p.call("LIMIT", vec![], &mut host).unwrap_err().to_string();
202        assert!(not_fn.contains("not a function"), "{not_fn}");
203    }
204}