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 = self.callee(name)?;
209        if let Err(e) = self.interp.set_budget(budget) {
210            return Err(CallError::Raised {
211                name: name.to_string(),
212                message: e.to_string(),
213            });
214        }
215        self.apply(name, &callee, args, host)
216    }
217
218    pub fn try_call(
219        &mut self,
220        name: &str,
221        args: Vec<Value>,
222        host: &mut H,
223    ) -> Result<Value, CallError> {
224        let callee = self.callee(name)?;
225        self.apply(name, &callee, args, host)
226    }
227
228    fn callee(&self, name: &str) -> Result<Value, CallError> {
229        match self.lookup(name) {
230            Some(v @ (Value::Closure(_) | Value::NativeFn(_))) => Ok(v),
231            Some(other) => Err(CallError::NotAFunction {
232                name: name.to_string(),
233                kind: kind(&other),
234            }),
235            None => Err(CallError::Undefined(name.to_string())),
236        }
237    }
238
239    fn apply(
240        &mut self,
241        name: &str,
242        callee: &Value,
243        args: Vec<Value>,
244        host: &mut H,
245    ) -> Result<Value, CallError> {
246        self.interp
247            .apply_external_value(callee, args, host, tatara_lisp::Span::synthetic())
248            .map_err(|e| match e {
249                tatara_lisp_eval::EvalError::BudgetExceeded { .. } => CallError::Exhausted {
250                    name: name.to_string(),
251                    message: e.short_message(),
252                },
253                other => CallError::Raised {
254                    name: name.to_string(),
255                    message: other.short_message(),
256                },
257            })
258    }
259}
260
261/// A string argument, the common case for a host passing text in.
262#[must_use]
263pub fn text(s: &str) -> Value {
264    Value::Str(Arc::from(s))
265}
266
267fn kind(v: &Value) -> &'static str {
268    match v {
269        Value::Nil => "nil",
270        Value::Bool(_) => "a boolean",
271        Value::Int(_) | Value::Float(_) => "a number",
272        Value::Str(_) => "a string",
273        Value::List(_) => "a list",
274        Value::Map(_) => "a map",
275        _ => "a value",
276    }
277}
278
279#[cfg(test)]
280mod tests {
281    use super::*;
282    use crate::uses::NoLoader;
283    use tatara_lisp_eval::Arity;
284
285    /// What a host sees of the program: the notes it was sent.
286    #[derive(Default)]
287    struct Notes(Vec<String>);
288
289    fn install(interp: &mut Interpreter<Notes>) {
290        interp.register_fn(
291            "note",
292            Arity::Exact(1),
293            |args: &[Value], h: &mut Notes, _span| {
294                if let Value::Str(s) = &args[0] {
295                    h.0.push(s.to_string());
296                }
297                Ok(Value::Nil)
298            },
299        );
300    }
301
302    fn load(src: &str, host: &mut Notes) -> Result<Hosted<Notes>, RunError> {
303        load_hosted(Entry::anonymous(src), &NoLoader, host, install)
304    }
305
306    fn as_text(v: &Value) -> String {
307        match v {
308            Value::Str(s) => s.to_string(),
309            other => format!("{other:?}"),
310        }
311    }
312
313    #[test]
314    fn an_empty_program_defines_nothing() {
315        let mut host = Notes::default();
316        let mut p = load("", &mut host).expect("an empty program loads");
317        assert!(!p.defines("run"));
318        let err = p.call("run", vec![], &mut host).unwrap_err().to_string();
319        assert!(err.contains("no function named run"), "{err}");
320    }
321
322    #[test]
323    fn a_call_returns_the_function_value_for_its_arguments() {
324        let mut host = Notes::default();
325        let src = "def greet(who)\n  \"hello #{who}\"\nend\n";
326        let mut p = load(src, &mut host).unwrap();
327        assert!(p.defines("greet"));
328        let v = p.call("greet", vec![text("blue")], &mut host).unwrap();
329        assert_eq!(as_text(&v), "hello blue");
330        // Identity: two calls with the same argument agree.
331        let again = p.call("greet", vec![text("blue")], &mut host).unwrap();
332        assert_eq!(as_text(&again), as_text(&v));
333    }
334
335    #[test]
336    fn host_primitives_reach_the_host_state() {
337        let mut host = Notes::default();
338        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";
339        let mut p = load(src, &mut host).unwrap();
340        let v = p.call("run", vec![text("abcd")], &mut host).unwrap();
341        assert!(matches!(v, Value::Int(4)), "{v:?}");
342        assert_eq!(host.0, vec!["saw abcd".to_owned(), "done".to_owned()]);
343    }
344
345    #[test]
346    fn a_type_error_stops_the_load_before_anything_runs() {
347        let mut host = Notes::default();
348        let src = "note(\"top level ran\")\ndef f(x: Int) -> Int\n  \"no\"\nend\n";
349        let err = load(src, &mut host).err().expect("refused").to_string();
350        assert!(err.contains("error[B0003]"), "{err}");
351        assert!(
352            host.0.is_empty(),
353            "no top-level form may run before the check passes"
354        );
355    }
356
357    #[test]
358    fn a_qualified_name_calls_a_bidamas_definition_and_a_bare_one_does_not() {
359        let mut host = Notes::default();
360        let loader = crate::uses::tests::MemLoader(
361            [("tama", "def exports()\n  \"tama rows\"\nend\n")]
362                .into_iter()
363                .collect(),
364        );
365        let mut p = load_hosted(
366            Entry::anonymous("use(\"tama\")\n"),
367            &loader,
368            &mut host,
369            install,
370        )
371        .unwrap();
372        assert!(p.defines("tama::exports"));
373        let v = p.call("tama::exports", vec![], &mut host).unwrap();
374        assert_eq!(as_text(&v), "tama rows");
375        assert!(!p.defines("exports"));
376        assert!(!p.defines("other::exports"));
377        assert!(p.defines("blue::length"));
378        let err = p
379            .call("tama::absent", vec![], &mut host)
380            .unwrap_err()
381            .to_string();
382        assert!(err.contains("no function named tama::absent"), "{err}");
383    }
384
385    #[test]
386    fn a_raise_and_a_non_function_are_typed_errors_naming_the_function() {
387        let mut host = Notes::default();
388        let src = "LIMIT = 3\ndef boom()\n  throw(error(\"bad\"))\nend\n";
389        let mut p = load(src, &mut host).unwrap();
390        let raised = p.call("boom", vec![], &mut host).unwrap_err().to_string();
391        assert!(raised.contains("boom"), "{raised}");
392        let not_fn = p.call("LIMIT", vec![], &mut host).unwrap_err().to_string();
393        assert!(not_fn.contains("not a function"), "{not_fn}");
394    }
395}