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(Box::new(crate::observe::Fault::from(
96                    format!("{name} is not a function (it is {})", kind(&other)),
97                ))))
98            }
99            None => {
100                return Err(RunError::Eval(Box::new(
101                    format!("no function named {name}").into(),
102                )))
103            }
104        };
105        self.interp
106            .apply_external_value(&callee, args, host, tatara_lisp::Span::synthetic())
107            .map_err(|e| RunError::Eval(Box::new(format!("{name}: {}", e.short_message()).into())))
108    }
109}
110
111/// Why a framed load ([`load_hosted_in_frame`]) refused a program.
112#[derive(Debug, thiserror::Error)]
113pub enum FramedError {
114    /// A stage of the pipeline refused it, or its top-level forms raised.
115    #[error("{0}")]
116    Run(#[from] RunError),
117    /// It names what its frame does not permit. Every name, not the first.
118    #[error("it names {} outside its frame: {}", if .names.len() == 1 { "a name" } else { "names" }, .names.join(", "))]
119    Escapes { names: Vec<String> },
120}
121
122/// Why a budgeted call ([`Hosted::call_budgeted`]) stopped short.
123#[derive(Debug, thiserror::Error)]
124pub enum CallError {
125    #[error("no function named {0}")]
126    Undefined(String),
127    #[error("{name} is not a function (it is {kind})")]
128    NotAFunction { name: String, kind: &'static str },
129    /// The call ran past its step or depth budget.
130    #[error("{name}: {message}")]
131    Exhausted { name: String, message: String },
132    /// The call raised.
133    #[error("{name}: {message}")]
134    Raised { name: String, message: String },
135}
136
137/// [`load_hosted`], refusing a program that names anything `permits` does
138/// not admit, and running its top-level forms under `budget`.
139///
140/// The frame is checked over the whole resolved program, every imported
141/// bidama's forms included, after the check stage and before any form runs
142/// (`blue_lang_waku::free_names`, the reach check a Bluefile is read under).
143/// A name the frame refuses that the interpreter binds is also rebound to a
144/// refusal, so a name the tree cannot show, one built at run time and
145/// applied, is refused when it is called rather than run.
146///
147/// # Errors
148///
149/// [`FramedError::Run`] for every [`RunError`] [`load_hosted`] has,
150/// [`FramedError::Escapes`] naming every refused name.
151pub fn load_hosted_in_frame<H: 'static>(
152    entry: Entry<'_>,
153    loader: &dyn Loader,
154    host: &mut H,
155    install: impl FnOnce(&mut Interpreter<H>),
156    permits: &dyn Fn(&str) -> bool,
157    budget: tatara_lisp_eval::vm::Budget,
158) -> Result<Hosted<H>, FramedError> {
159    let mut interp = crate::interpreter(host);
160    install(&mut interp);
161    let prepared = prepare(entry, loader, None, &builtin_names(&interp))?;
162    let escapes = blue_lang_waku::free_names(prepared.erased(), permits);
163    if !escapes.is_empty() {
164        return Err(FramedError::Escapes {
165            names: escapes.into_iter().collect(),
166        });
167    }
168    for name in interp.reserved_head_names() {
169        if !permits(&name) && interp.lookup_global(&name).is_some() {
170            let refused = name.clone();
171            interp.register_fn(
172                name,
173                tatara_lisp_eval::Arity::Any,
174                move |_: &[Value], _: &mut H, span| {
175                    Err(tatara_lisp_eval::EvalError::native_fn(
176                        refused.to_string(),
177                        "outside this program's frame",
178                        span,
179                    ))
180                },
181            );
182        }
183    }
184    interp
185        .set_budget(budget)
186        .map_err(|e| RunError::Eval(Box::new(e.to_string().into())))?;
187    eval_prepared(&mut interp, &prepared, host)?;
188    let entry = prepared.entry_namespace();
189    Ok(Hosted { interp, entry })
190}
191
192impl<H: 'static> Hosted<H> {
193    /// Call the definition `name` with `args` under `budget`, which starts
194    /// unspent for this call. Running past it is [`CallError::Exhausted`],
195    /// told apart from a raise.
196    ///
197    /// # Errors
198    ///
199    /// [`CallError`]: the name is not a function, or the call ran out or
200    /// raised.
201    pub fn call_budgeted(
202        &mut self,
203        name: &str,
204        args: Vec<Value>,
205        host: &mut H,
206        budget: tatara_lisp_eval::vm::Budget,
207    ) -> Result<Value, CallError> {
208        let callee = match self.lookup(name) {
209            Some(v @ (Value::Closure(_) | Value::NativeFn(_))) => v,
210            Some(other) => {
211                return Err(CallError::NotAFunction {
212                    name: name.to_string(),
213                    kind: kind(&other),
214                })
215            }
216            None => return Err(CallError::Undefined(name.to_string())),
217        };
218        if let Err(e) = self.interp.set_budget(budget) {
219            return Err(CallError::Raised {
220                name: name.to_string(),
221                message: e.to_string(),
222            });
223        }
224        self.interp
225            .apply_external_value(&callee, args, host, tatara_lisp::Span::synthetic())
226            .map_err(|e| match e {
227                tatara_lisp_eval::EvalError::BudgetExceeded { .. } => CallError::Exhausted {
228                    name: name.to_string(),
229                    message: e.short_message(),
230                },
231                other => CallError::Raised {
232                    name: name.to_string(),
233                    message: other.short_message(),
234                },
235            })
236    }
237}
238
239/// A string argument, the common case for a host passing text in.
240#[must_use]
241pub fn text(s: &str) -> Value {
242    Value::Str(Arc::from(s))
243}
244
245fn kind(v: &Value) -> &'static str {
246    match v {
247        Value::Nil => "nil",
248        Value::Bool(_) => "a boolean",
249        Value::Int(_) | Value::Float(_) => "a number",
250        Value::Str(_) => "a string",
251        Value::List(_) => "a list",
252        Value::Map(_) => "a map",
253        _ => "a value",
254    }
255}
256
257#[cfg(test)]
258mod tests {
259    use super::*;
260    use crate::uses::NoLoader;
261    use tatara_lisp_eval::Arity;
262
263    /// What a host sees of the program: the notes it was sent.
264    #[derive(Default)]
265    struct Notes(Vec<String>);
266
267    fn install(interp: &mut Interpreter<Notes>) {
268        interp.register_fn(
269            "note",
270            Arity::Exact(1),
271            |args: &[Value], h: &mut Notes, _span| {
272                if let Value::Str(s) = &args[0] {
273                    h.0.push(s.to_string());
274                }
275                Ok(Value::Nil)
276            },
277        );
278    }
279
280    fn load(src: &str, host: &mut Notes) -> Result<Hosted<Notes>, RunError> {
281        load_hosted(Entry::anonymous(src), &NoLoader, host, install)
282    }
283
284    fn as_text(v: &Value) -> String {
285        match v {
286            Value::Str(s) => s.to_string(),
287            other => format!("{other:?}"),
288        }
289    }
290
291    #[test]
292    fn an_empty_program_defines_nothing() {
293        let mut host = Notes::default();
294        let mut p = load("", &mut host).expect("an empty program loads");
295        assert!(!p.defines("run"));
296        let err = p.call("run", vec![], &mut host).unwrap_err().to_string();
297        assert!(err.contains("no function named run"), "{err}");
298    }
299
300    #[test]
301    fn a_call_returns_the_function_value_for_its_arguments() {
302        let mut host = Notes::default();
303        let src = "def greet(who)\n  \"hello #{who}\"\nend\n";
304        let mut p = load(src, &mut host).unwrap();
305        assert!(p.defines("greet"));
306        let v = p.call("greet", vec![text("blue")], &mut host).unwrap();
307        assert_eq!(as_text(&v), "hello blue");
308        // Identity: two calls with the same argument agree.
309        let again = p.call("greet", vec![text("blue")], &mut host).unwrap();
310        assert_eq!(as_text(&again), as_text(&v));
311    }
312
313    #[test]
314    fn host_primitives_reach_the_host_state() {
315        let mut host = Notes::default();
316        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";
317        let mut p = load(src, &mut host).unwrap();
318        let v = p.call("run", vec![text("abcd")], &mut host).unwrap();
319        assert!(matches!(v, Value::Int(4)), "{v:?}");
320        assert_eq!(host.0, vec!["saw abcd".to_owned(), "done".to_owned()]);
321    }
322
323    #[test]
324    fn a_type_error_stops_the_load_before_anything_runs() {
325        let mut host = Notes::default();
326        let src = "note(\"top level ran\")\ndef f(x: Int) -> Int\n  \"no\"\nend\n";
327        let err = load(src, &mut host).err().expect("refused").to_string();
328        assert!(err.contains("error[B0003]"), "{err}");
329        assert!(
330            host.0.is_empty(),
331            "no top-level form may run before the check passes"
332        );
333    }
334
335    #[test]
336    fn a_qualified_name_calls_a_bidamas_definition_and_a_bare_one_does_not() {
337        let mut host = Notes::default();
338        let loader = crate::uses::tests::MemLoader(
339            [("tama", "def exports()\n  \"tama rows\"\nend\n")]
340                .into_iter()
341                .collect(),
342        );
343        let mut p = load_hosted(
344            Entry::anonymous("use(\"tama\")\n"),
345            &loader,
346            &mut host,
347            install,
348        )
349        .unwrap();
350        assert!(p.defines("tama::exports"));
351        let v = p.call("tama::exports", vec![], &mut host).unwrap();
352        assert_eq!(as_text(&v), "tama rows");
353        assert!(!p.defines("exports"));
354        assert!(!p.defines("other::exports"));
355        assert!(p.defines("blue::length"));
356        let err = p
357            .call("tama::absent", vec![], &mut host)
358            .unwrap_err()
359            .to_string();
360        assert!(err.contains("no function named tama::absent"), "{err}");
361    }
362
363    #[test]
364    fn a_raise_and_a_non_function_are_typed_errors_naming_the_function() {
365        let mut host = Notes::default();
366        let src = "LIMIT = 3\ndef boom()\n  throw(error(\"bad\"))\nend\n";
367        let mut p = load(src, &mut host).unwrap();
368        let raised = p.call("boom", vec![], &mut host).unwrap_err().to_string();
369        assert!(raised.contains("boom"), "{raised}");
370        let not_fn = p.call("LIMIT", vec![], &mut host).unwrap_err().to_string();
371        assert!(not_fn.contains("not a function"), "{not_fn}");
372    }
373}