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    /// A qualified `pkg::name` is bidama `pkg`'s definition, exactly as a
61    /// program writes it, and `blue::name` is the builtin.
62    fn lookup(&self, name: &str) -> Option<Value> {
63        match name.split_once("::") {
64            Some((blue_lang_syntax::BUILTIN_QUALIFIER, bare)) => self.interp.lookup_global(bare),
65            Some((pkg, bare)) => self.interp.lookup_global(&blue_lang_check::names::key(
66                &blue_lang_check::Namespace::Bidama(pkg.to_owned()),
67                bare,
68            )),
69            None => self
70                .interp
71                .lookup_global(&blue_lang_check::names::key(&self.entry, name))
72                .or_else(|| self.interp.lookup_global(name)),
73        }
74    }
75
76    /// Whether the program defines a callable named `name`.
77    #[must_use]
78    pub fn defines(&self, name: &str) -> bool {
79        matches!(
80            self.lookup(name),
81            Some(Value::Closure(_) | Value::NativeFn(_))
82        )
83    }
84
85    /// Call the definition `name` with `args`.
86    ///
87    /// # Errors
88    ///
89    /// [`RunError::Eval`] naming the function when it is not defined or is not
90    /// callable, or with the message it raised.
91    pub fn call(&mut self, name: &str, args: Vec<Value>, host: &mut H) -> Result<Value, RunError> {
92        let callee = match self.lookup(name) {
93            Some(v @ (Value::Closure(_) | Value::NativeFn(_))) => v,
94            Some(other) => {
95                return Err(RunError::Eval(format!(
96                    "{name} is not a function (it is {})",
97                    kind(&other)
98                )))
99            }
100            None => return Err(RunError::Eval(format!("no function named {name}"))),
101        };
102        self.interp
103            .apply_external_value(&callee, args, host, tatara_lisp::Span::synthetic())
104            .map_err(|e| RunError::Eval(format!("{name}: {}", e.short_message())))
105    }
106}
107
108/// A string argument, the common case for a host passing text in.
109#[must_use]
110pub fn text(s: &str) -> Value {
111    Value::Str(Arc::from(s))
112}
113
114fn kind(v: &Value) -> &'static str {
115    match v {
116        Value::Nil => "nil",
117        Value::Bool(_) => "a boolean",
118        Value::Int(_) | Value::Float(_) => "a number",
119        Value::Str(_) => "a string",
120        Value::List(_) => "a list",
121        Value::Map(_) => "a map",
122        _ => "a value",
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use super::*;
129    use crate::uses::NoLoader;
130    use tatara_lisp_eval::Arity;
131
132    /// What a host sees of the program: the notes it was sent.
133    #[derive(Default)]
134    struct Notes(Vec<String>);
135
136    fn install(interp: &mut Interpreter<Notes>) {
137        interp.register_fn(
138            "note",
139            Arity::Exact(1),
140            |args: &[Value], h: &mut Notes, _span| {
141                if let Value::Str(s) = &args[0] {
142                    h.0.push(s.to_string());
143                }
144                Ok(Value::Nil)
145            },
146        );
147    }
148
149    fn load(src: &str, host: &mut Notes) -> Result<Hosted<Notes>, RunError> {
150        load_hosted(Entry::anonymous(src), &NoLoader, host, install)
151    }
152
153    fn as_text(v: &Value) -> String {
154        match v {
155            Value::Str(s) => s.to_string(),
156            other => format!("{other:?}"),
157        }
158    }
159
160    #[test]
161    fn an_empty_program_defines_nothing() {
162        let mut host = Notes::default();
163        let mut p = load("", &mut host).expect("an empty program loads");
164        assert!(!p.defines("run"));
165        let err = p.call("run", vec![], &mut host).unwrap_err().to_string();
166        assert!(err.contains("no function named run"), "{err}");
167    }
168
169    #[test]
170    fn a_call_returns_the_function_value_for_its_arguments() {
171        let mut host = Notes::default();
172        let src = "def greet(who)\n  \"hello #{who}\"\nend\n";
173        let mut p = load(src, &mut host).unwrap();
174        assert!(p.defines("greet"));
175        let v = p.call("greet", vec![text("blue")], &mut host).unwrap();
176        assert_eq!(as_text(&v), "hello blue");
177        // Identity: two calls with the same argument agree.
178        let again = p.call("greet", vec![text("blue")], &mut host).unwrap();
179        assert_eq!(as_text(&again), as_text(&v));
180    }
181
182    #[test]
183    fn host_primitives_reach_the_host_state() {
184        let mut host = Notes::default();
185        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";
186        let mut p = load(src, &mut host).unwrap();
187        let v = p.call("run", vec![text("abcd")], &mut host).unwrap();
188        assert!(matches!(v, Value::Int(4)), "{v:?}");
189        assert_eq!(host.0, vec!["saw abcd".to_owned(), "done".to_owned()]);
190    }
191
192    #[test]
193    fn a_type_error_stops_the_load_before_anything_runs() {
194        let mut host = Notes::default();
195        let src = "note(\"top level ran\")\ndef f(x: Int) -> Int\n  \"no\"\nend\n";
196        let err = load(src, &mut host).err().expect("refused").to_string();
197        assert!(err.contains("error[B0003]"), "{err}");
198        assert!(
199            host.0.is_empty(),
200            "no top-level form may run before the check passes"
201        );
202    }
203
204    #[test]
205    fn a_qualified_name_calls_a_bidamas_definition_and_a_bare_one_does_not() {
206        let mut host = Notes::default();
207        let loader = crate::uses::tests::MemLoader(
208            [("tama", "def exports()\n  \"tama rows\"\nend\n")]
209                .into_iter()
210                .collect(),
211        );
212        let mut p = load_hosted(
213            Entry::anonymous("use(\"tama\")\n"),
214            &loader,
215            &mut host,
216            install,
217        )
218        .unwrap();
219        assert!(p.defines("tama::exports"));
220        let v = p.call("tama::exports", vec![], &mut host).unwrap();
221        assert_eq!(as_text(&v), "tama rows");
222        assert!(!p.defines("exports"));
223        assert!(!p.defines("other::exports"));
224        assert!(p.defines("blue::length"));
225        let err = p
226            .call("tama::absent", vec![], &mut host)
227            .unwrap_err()
228            .to_string();
229        assert!(err.contains("no function named tama::absent"), "{err}");
230    }
231
232    #[test]
233    fn a_raise_and_a_non_function_are_typed_errors_naming_the_function() {
234        let mut host = Notes::default();
235        let src = "LIMIT = 3\ndef boom()\n  throw(error(\"bad\"))\nend\n";
236        let mut p = load(src, &mut host).unwrap();
237        let raised = p.call("boom", vec![], &mut host).unwrap_err().to_string();
238        assert!(raised.contains("boom"), "{raised}");
239        let not_fn = p.call("LIMIT", vec![], &mut host).unwrap_err().to_string();
240        assert!(not_fn.contains("not a function"), "{not_fn}");
241    }
242}