Skip to main content

blue_lang_runtime/
session.rs

1//! One evaluation session: the function the REPL, `blue eval`, `blue serve`
2//! and an editor all call (`theory/BLUE-TOOLING.md` §2, §4).
3//!
4//! A session holds one interpreter and, per **context**, the program code is
5//! evaluated against: a loaded file's top-level forms, or the session's own
6//! when no file is named. Evaluating code in a context runs the same stages as
7//! `blue run` — parse, resolve imports, check, lower, erase — over the
8//! context's forms with the new code spliced in, then evaluates only the new
9//! forms (and any bidama they newly import). So a name resolves in the REPL
10//! exactly as it would in the file, and a definition made in the session is
11//! one more top-level form of its context: redefining a function replaces its
12//! form, and because callers reach it by its runtime key at call time, every
13//! caller sees the new one at once.
14//!
15//! Three things bound an evaluation, each reported as its own status rather
16//! than as an error message:
17//!
18//! - **The frame.** A host-effect [`Capability`] the session's frame does not
19//!   grant is bound to a refusal: reaching it, directly or through any number
20//!   of calls, stops the evaluation as [`Stop::Volatile`] naming the
21//!   capability. The default frame grants none, so evaluation is pure unless
22//!   the session was started with a wider one.
23//! - **The step budget** ([`Stop::Exhausted`]), set fresh for each request.
24//! - **The interrupt flag** ([`Stop::Interrupted`]), which another thread sets
25//!   while an evaluation runs. The session never clears it before running, so
26//!   an interrupt that arrives between "start" and the first step is not lost;
27//!   a caller clears it when it starts a request ([`Session::interrupt_flag`]).
28//!
29//! What the code writes to stdout is captured into [`Outcome::out`], or
30//! written through as it happens when the session was built with a live sink.
31
32use std::collections::{BTreeMap, BTreeSet};
33use std::io::Write;
34use std::path::{Path, PathBuf};
35use std::sync::atomic::{AtomicBool, Ordering};
36use std::sync::Arc;
37
38use blue_lang_check::{NameTable, Namespace};
39use blue_lang_waku::{Capability, Reach, Waku, When, Where};
40use tatara_lisp::{Atom, Sexp, Span, Spanned};
41use tatara_lisp_eval::ffi::Arity;
42use tatara_lisp_eval::{EvalError, Interpreter, Value};
43
44use crate::pipeline::{self, RunError};
45use crate::uses::{Entry, Loader, ResolvedProgram};
46
47#[cfg(feature = "sys")]
48pub mod protocol;
49
50/// The frame a session evaluates in when nothing wider is asked for: every
51/// pure capability, no host effect.
52#[must_use]
53pub fn pure_frame() -> Waku {
54    frame_granting(&[])
55}
56
57/// A frame granting the pure capabilities plus `host`.
58#[must_use]
59pub fn frame_granting(host: &[Capability]) -> Waku {
60    Waku {
61        reach: Reach::only(
62            Capability::ALL
63                .into_iter()
64                .filter(|c| !c.is_host_effect() || host.contains(c)),
65        ),
66        when: When::Anytime,
67        place: Where::Process,
68    }
69}
70
71/// How a session is built.
72pub struct Config {
73    pub frame: Waku,
74    /// Steps each request may take when it names no budget; `None` falls back
75    /// to the process's configured `max_steps` (unbounded by default).
76    pub budget: Option<usize>,
77    pub loader: Box<dyn Loader + Send>,
78    /// Set from any thread to halt the running evaluation.
79    pub interrupt: Arc<AtomicBool>,
80    /// Write output through as it is produced instead of capturing it.
81    pub live: Option<Box<dyn Write + Send>>,
82}
83
84impl Config {
85    /// A pure session over `loader`, capturing output.
86    #[must_use]
87    pub fn new(loader: Box<dyn Loader + Send>) -> Self {
88        Self {
89            frame: pure_frame(),
90            budget: None,
91            loader,
92            interrupt: Arc::new(AtomicBool::new(false)),
93            live: None,
94        }
95    }
96}
97
98/// Why a request stopped without a result.
99#[derive(Clone, Debug, PartialEq, Eq)]
100pub enum Stop {
101    Error(Failure),
102    /// The code reached a host capability the frame does not grant.
103    Volatile {
104        reach: Capability,
105        name: String,
106    },
107    /// The step budget ran out.
108    Exhausted {
109        limit: usize,
110    },
111    Interrupted,
112}
113
114/// A request that failed, as a code and the rendered message.
115#[derive(Clone, Debug, PartialEq, Eq)]
116pub struct Failure {
117    /// `parse`, `import`, `runtime`, `not-found`, or the check rule's code
118    /// (`B0001`) when the check stage refused the code.
119    pub code: String,
120    pub message: String,
121}
122
123impl Failure {
124    fn new(code: impl Into<String>, message: impl Into<String>) -> Self {
125        Self {
126            code: code.into(),
127            message: message.into(),
128        }
129    }
130}
131
132/// What one request produced, and what the code wrote while producing it.
133#[derive(Debug)]
134pub struct Outcome<T> {
135    pub result: Result<T, Stop>,
136    pub out: String,
137}
138
139/// A name a context can complete to, with what it is.
140#[derive(Clone, Debug, PartialEq, Eq)]
141pub struct Item {
142    /// As a program spells it: bare, or `pkg::name` for a bidama's.
143    pub name: String,
144    pub namespace: String,
145    pub signature: Option<String>,
146    pub doc: Option<String>,
147}
148
149/// How much of a file a load evaluates.
150#[derive(Clone, Copy, Debug, PartialEq, Eq)]
151pub enum Load {
152    /// Every top-level form, as `blue run` does.
153    Whole,
154    /// Definitions and `use` forms only: the context an expression in the
155    /// file needs, without running the file's program.
156    Definitions,
157}
158
159/// One macro-expansion step, or all of them.
160#[derive(Clone, Copy, Debug, PartialEq, Eq)]
161pub enum Step {
162    One,
163    All,
164}
165
166/// What the code writes, and the host effect it reached, if any.
167#[derive(Default)]
168pub struct Host {
169    out: String,
170    live: Option<Box<dyn Write + Send>>,
171    reached: Option<(Capability, &'static str)>,
172}
173
174impl Host {
175    fn write(&mut self, text: &str) {
176        match &mut self.live {
177            Some(w) => {
178                let _ = w.write_all(text.as_bytes()).and_then(|()| w.flush());
179            }
180            None => self.out.push_str(text),
181        }
182    }
183}
184
185/// One top-level form of a context, as source.
186#[derive(Clone, Debug)]
187struct Form {
188    /// The text from the end of the previous form through this one, so a
189    /// comment or waiver above a definition travels with it.
190    text: String,
191    /// Where the form itself starts in `text`.
192    at: usize,
193    defines: Vec<String>,
194    is_use: bool,
195    /// Set on the forms a request runs: its place in the order they run, and
196    /// where it starts in the request's own code when it came from there.
197    fresh: Option<(usize, Option<usize>)>,
198}
199
200impl Form {
201    /// Whether a context keeps this form once the request that wrote it ends.
202    fn persists(&self) -> bool {
203        !self.defines.is_empty() || self.is_use
204    }
205
206    fn settled(mut self) -> Self {
207        self.fresh = None;
208        self
209    }
210}
211
212/// Put `form` into `forms`: in place of the form that defines the same name
213/// (dropping any other that does), else at the end.
214fn splice(forms: &mut Vec<Form>, form: Form) {
215    let same = |f: &Form| f.defines.iter().any(|d| form.defines.contains(d));
216    match forms.iter().position(same) {
217        Some(first) => {
218            let mut i = forms.len();
219            while i > first + 1 {
220                i -= 1;
221                if same(&forms[i]) {
222                    forms.remove(i);
223                }
224            }
225            forms[first] = form;
226        }
227        None => forms.push(form),
228    }
229}
230
231/// What code is evaluated against.
232#[derive(Default)]
233struct Context {
234    path: Option<PathBuf>,
235    forms: Vec<Form>,
236    /// The last program checked in this context, and its name table.
237    program: Option<ResolvedProgram>,
238    names: Option<NameTable>,
239}
240
241/// One evaluation session.
242pub struct Session {
243    interp: Interpreter<Host>,
244    host: Host,
245    frame: Waku,
246    budget: Option<usize>,
247    loader: Box<dyn Loader + Send>,
248    interrupt: Arc<AtomicBool>,
249    /// The builtin name table, read once off a pristine interpreter, so a
250    /// definition the session makes is never mistaken for a builtin.
251    builtins: NameTable,
252    contexts: BTreeMap<Option<PathBuf>, Context>,
253    /// Bidamas whose forms this interpreter has evaluated.
254    packages: BTreeSet<String>,
255}
256
257const CODE_LABEL: &str = "<eval>";
258
259impl Session {
260    #[must_use]
261    pub fn new(config: Config) -> Self {
262        let mut host = Host {
263            live: config.live,
264            ..Host::default()
265        };
266        let interp = build(&config.frame, &config.interrupt, &mut host);
267        let builtins = pipeline::builtin_names(&interp);
268        Self {
269            interp,
270            host,
271            frame: config.frame,
272            budget: config.budget,
273            loader: config.loader,
274            interrupt: config.interrupt,
275            builtins,
276            contexts: BTreeMap::new(),
277            packages: BTreeSet::new(),
278        }
279    }
280
281    /// The flag that halts the running evaluation.
282    #[must_use]
283    pub fn interrupt_flag(&self) -> Arc<AtomicBool> {
284        Arc::clone(&self.interrupt)
285    }
286
287    #[must_use]
288    pub fn frame(&self) -> &Waku {
289        &self.frame
290    }
291
292    /// Forget every context and definition: a fresh interpreter, same frame.
293    pub fn reset(&mut self) {
294        let live = self.host.live.take();
295        self.host = Host {
296            live,
297            ..Host::default()
298        };
299        self.interp = build(&self.frame, &self.interrupt, &mut self.host);
300        self.contexts.clear();
301        self.packages.clear();
302    }
303
304    /// Load `file` (its text from `text` when given, else from disk) as a
305    /// fresh context, replacing any earlier load of it. Answers the names the
306    /// file defines.
307    pub fn load(&mut self, file: &Path, text: Option<&str>, how: Load) -> Outcome<Vec<String>> {
308        let result = self.load_inner(file, text, how);
309        self.finish(result)
310    }
311
312    /// Evaluate `code` in `file`'s context (loading its definitions first if
313    /// it is not loaded), or the session's own when `file` is `None`. Answers
314    /// the value of the last form as canonical blue text.
315    pub fn eval(
316        &mut self,
317        code: &str,
318        file: Option<&Path>,
319        budget: Option<usize>,
320    ) -> Outcome<String> {
321        let result = self
322            .context_key(file)
323            .and_then(|key| self.run_code(&key, code, budget, Action::Evaluate))
324            .map(|v| render(&v));
325        self.finish(result)
326    }
327
328    /// Expand the macro call `code` one step or fully, in `file`'s context or
329    /// the session's.
330    pub fn expand(&mut self, code: &str, file: Option<&Path>, step: Step) -> Outcome<String> {
331        let result = self
332            .context_key(file)
333            .and_then(|key| self.run_code(&key, code, None, Action::Expand(step)))
334            .and_then(|v| match crate::literal::value_to_sexp(&v) {
335                Some(s) => Ok(crate::literal::format(&unkey(s))),
336                None => Err(Stop::Error(Failure::new(
337                    "runtime",
338                    "the expansion is not a form",
339                ))),
340            });
341        self.finish(result)
342    }
343
344    /// Every name `prefix` completes to in `file`'s context (or the
345    /// session's), each with its namespace and doc line.
346    #[must_use]
347    pub fn complete(&self, prefix: &str, file: Option<&Path>) -> Vec<Item> {
348        let mut seen = BTreeSet::new();
349        let mut out = Vec::new();
350        let (table, ctx) = self.table(file);
351        for (ns, b) in table.all() {
352            let name = spelled(ns, &b.name);
353            let spellable = name
354                .chars()
355                .next()
356                .is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
357                && !name.contains('-')
358                && !name.contains('%');
359            if !spellable || !name.starts_with(prefix) || !seen.insert(name.clone()) {
360                continue;
361            }
362            out.push(self.describe(ns, b, name, ctx));
363        }
364        out
365    }
366
367    /// What `name` is in `file`'s context (or the session's): the first
368    /// binding the resolution order reaches. `pkg::name` names a bidama's
369    /// definition and `blue::name` the builtin.
370    #[must_use]
371    pub fn doc(&self, name: &str, file: Option<&Path>) -> Option<Item> {
372        let (table, ctx) = self.table(file);
373        let (want_ns, bare) = match name.split_once("::") {
374            Some((blue_lang_syntax::BUILTIN_QUALIFIER, bare)) => (Some(Namespace::Builtin), bare),
375            Some((pkg, bare)) => (Some(Namespace::Bidama(pkg.to_string())), bare),
376            None => (None, name),
377        };
378        table
379            .all()
380            .find(|(ns, b)| {
381                b.name == bare
382                    && want_ns.as_ref().is_none_or(|w| w == *ns)
383                    && !matches!(ns, Namespace::Harness)
384            })
385            .map(|(ns, b)| self.describe(ns, b, spelled(ns, &b.name), ctx))
386    }
387
388    fn table(&self, file: Option<&Path>) -> (&NameTable, Option<&Context>) {
389        let key = file.map(canonical);
390        let ctx = self.contexts.get(&key);
391        match ctx.and_then(|c| c.names.as_ref()) {
392            Some(t) => (t, ctx),
393            None => (&self.builtins, ctx),
394        }
395    }
396
397    fn describe(
398        &self,
399        ns: &Namespace,
400        b: &blue_lang_check::Binding,
401        name: String,
402        ctx: Option<&Context>,
403    ) -> Item {
404        let (signature, doc) = match (b.top_level, ctx.and_then(|c| c.program.as_ref())) {
405            (Some(i), Some(program)) => definition_doc(program, i),
406            _ => match crate::docs::doc_of(&b.name) {
407                Some(d) => (Some(d.signature.to_string()), Some(d.doc.to_string())),
408                None => (None, None),
409            },
410        };
411        Item {
412            name,
413            namespace: ns.to_string(),
414            signature,
415            doc,
416        }
417    }
418
419    fn finish<T>(&mut self, result: Result<T, Stop>) -> Outcome<T> {
420        Outcome {
421            result,
422            out: std::mem::take(&mut self.host.out),
423        }
424    }
425
426    fn context_key(&mut self, file: Option<&Path>) -> Result<Option<PathBuf>, Stop> {
427        let key = file.map(canonical);
428        if let Some(path) = &key {
429            if !self.contexts.contains_key(&key) {
430                self.load_inner(path, None, Load::Definitions)?;
431            }
432        }
433        self.contexts.entry(key.clone()).or_default();
434        Ok(key)
435    }
436
437    fn load_inner(
438        &mut self,
439        file: &Path,
440        text: Option<&str>,
441        how: Load,
442    ) -> Result<Vec<String>, Stop> {
443        let path = canonical(file);
444        let text = match text {
445            Some(t) => t.to_string(),
446            None => std::fs::read_to_string(&path).map_err(|e| {
447                Stop::Error(Failure::new(
448                    "not-found",
449                    format!("{}: {e}", file.display()),
450                ))
451            })?,
452        };
453        let mut forms = split(&text)
454            .map_err(|e| Stop::Error(Failure::new("parse", format!("{}:{e}", file.display()))))?;
455        for (order, f) in forms.iter_mut().enumerate() {
456            if how == Load::Whole || f.persists() {
457                f.fresh = Some((order, None));
458            }
459        }
460        let defined: Vec<String> = forms.iter().flat_map(|f| f.defines.clone()).collect();
461        let key = Some(path.clone());
462        let mut ctx = Context {
463            path: Some(path),
464            ..Context::default()
465        };
466        let run = self.execute(&mut ctx, forms, "", None, Action::Load);
467        // A file whose forms ran is loaded even when a later form stopped:
468        // its definitions are in the interpreter, and the context holds them.
469        self.contexts.insert(key, ctx);
470        run.map(|_| defined)
471    }
472
473    fn run_code(
474        &mut self,
475        key: &Option<PathBuf>,
476        code: &str,
477        budget: Option<usize>,
478        action: Action,
479    ) -> Result<Value, Stop> {
480        let new = split(code)
481            .map_err(|e| Stop::Error(Failure::new("parse", format!("{CODE_LABEL}:{e}"))))?;
482        let mut ctx = self.contexts.remove(key).unwrap_or_default();
483        let mut forms = ctx.forms.clone();
484        let mut code_at = 0;
485        for (order, mut form) in new.into_iter().enumerate() {
486            form.fresh = Some((order, Some(code_at + form.at)));
487            code_at += form.text.len();
488            form.text.insert(0, '\n');
489            form.at += 1;
490            if form.defines.is_empty() {
491                forms.push(form);
492            } else {
493                splice(&mut forms, form);
494            }
495        }
496        let result = self.execute(&mut ctx, forms, code, budget, action);
497        self.contexts.insert(key.clone(), ctx);
498        result
499    }
500
501    /// Stage `forms` through every stage `blue run` runs, then evaluate (or
502    /// expand) the fresh ones. On return `ctx` holds what the request leaves
503    /// behind: a load's forms, or the context's own forms with every
504    /// persistent fresh form that ran spliced in.
505    fn execute(
506        &mut self,
507        ctx: &mut Context,
508        forms: Vec<Form>,
509        code: &str,
510        budget: Option<usize>,
511        action: Action,
512    ) -> Result<Value, Stop> {
513        let mut text = String::new();
514        // Composed-text start of each fresh form, to its place in the run
515        // order and its offset in `code`.
516        let mut fresh_at: BTreeMap<usize, (usize, Option<usize>, usize)> = BTreeMap::new();
517        for (i, f) in forms.iter().enumerate() {
518            if let Some((order, code_off)) = f.fresh {
519                fresh_at.insert(text.len() + f.at, (order, code_off, i));
520            }
521            text.push_str(&f.text);
522        }
523
524        let entry = Entry {
525            path: ctx.path.as_deref(),
526            text: &text,
527        };
528        let mut program = pipeline::parse_and_resolve(entry, self.loader.as_ref(), None)
529            .map_err(|e| Stop::Error(run_failure(e)))?;
530        program.retain(|f| !crate::uses::is_test_form(f));
531        let (outcome, names) = pipeline::check_stage(&program, &self.builtins, false);
532        let place = |top_level: usize, span: Span, message: &str| -> String {
533            let in_code = (program.owner_of(top_level) == Some(ResolvedProgram::ENTRY)
534                && !span.is_synthetic())
535            .then(|| in_code(&fresh_at, &forms, span))
536            .flatten();
537            match in_code {
538                Some(at) => {
539                    let (line, col) = Span::line_col(code, at.min(code.len()));
540                    format!("{CODE_LABEL}:{line}:{col}: {message}")
541                }
542                None => program.locate(top_level, span, message).to_string(),
543            }
544        };
545        if !outcome.ok() {
546            let code = outcome
547                .errors()
548                .next()
549                .map_or_else(|| "check".to_string(), |d| d.code.to_string());
550            let message = outcome
551                .errors()
552                .map(|d| {
553                    let mut line = place(d.top_level, d.span, &d.to_string());
554                    if let Some(help) = &d.help {
555                        line.push_str("\n  help: ");
556                        line.push_str(help);
557                    }
558                    line
559                })
560                .collect::<Vec<_>>()
561                .join("\n");
562            return Err(Stop::Error(Failure::new(code, message)));
563        }
564        let erased = crate::erase::erase_types(&pipeline::lower(&program, &names));
565
566        // What runs: every form of a bidama this interpreter has not seen, in
567        // program order, then the fresh forms in the order they were asked for.
568        let mut imported = Vec::new();
569        let mut asked: Vec<(usize, usize)> = Vec::new();
570        let mut new_packages = BTreeSet::new();
571        for (i, form) in program.forms().iter().enumerate() {
572            let owner = program.owner_of(i);
573            if owner == Some(ResolvedProgram::ENTRY) {
574                if let Some((order, _, _)) = fresh_at.get(&form.span.start) {
575                    asked.push((*order, i));
576                }
577                continue;
578            }
579            let package = owner
580                .and_then(|o| program.file(o))
581                .and_then(|f| f.package.clone());
582            if let Some(p) = package.filter(|p| !self.packages.contains(p)) {
583                new_packages.insert(p);
584                imported.push(i);
585            }
586        }
587        asked.sort_unstable();
588
589        let limit = budget
590            .or(self.budget)
591            .or(crate::execution_bounds().max_steps);
592        let _ = self.interp.set_budget(tatara_lisp_eval::vm::Budget {
593            fuel: limit,
594            max_depth: Some(crate::execution_bounds().max_call_depth),
595            quantum: None,
596        });
597        self.host.reached = None;
598
599        let mut value = Value::Nil;
600        let mut ran: BTreeSet<usize> = BTreeSet::new();
601        let mut failed = None;
602        for &i in &imported {
603            if let Err(e) = self.interp.eval_top_form(&erased[i], &mut self.host) {
604                failed = Some((i, e));
605                break;
606            }
607        }
608        if failed.is_none() {
609            self.packages.extend(new_packages);
610            for &(order, i) in &asked {
611                let step = match action {
612                    Action::Expand(step) => {
613                        expand_form(&mut self.interp, &mut self.host, &erased[i], step)
614                    }
615                    Action::Evaluate | Action::Load => {
616                        self.interp.eval_top_form(&erased[i], &mut self.host)
617                    }
618                };
619                match step {
620                    Ok(v) => {
621                        value = v;
622                        ran.insert(order);
623                    }
624                    Err(e) => {
625                        failed = Some((i, e));
626                        break;
627                    }
628                }
629            }
630        }
631        let stop = failed.map(|(i, e)| self.classify(&e, &erased, |span, msg| place(i, span, msg)));
632
633        match action {
634            Action::Load => ctx.forms = forms.into_iter().map(Form::settled).collect(),
635            Action::Evaluate => {
636                for f in forms {
637                    if f.persists() && f.fresh.is_some_and(|(order, _)| ran.contains(&order)) {
638                        splice(&mut ctx.forms, f.settled());
639                    }
640                }
641            }
642            Action::Expand(_) => {}
643        }
644        if action != Action::Expand(Step::One) && action != Action::Expand(Step::All) {
645            ctx.program = Some(program);
646            ctx.names = Some(names);
647        }
648        if let Some((reach, name)) = self.host.reached.take() {
649            return Err(Stop::Volatile {
650                reach,
651                name: name.to_string(),
652            });
653        }
654        match stop {
655            Some(s) => Err(s),
656            None => Ok(value),
657        }
658    }
659
660    fn classify(
661        &self,
662        e: &EvalError,
663        erased: &[Spanned],
664        place: impl Fn(Span, &str) -> String,
665    ) -> Stop {
666        match e {
667            EvalError::Halted => {
668                self.interrupt.store(false, Ordering::SeqCst);
669                Stop::Interrupted
670            }
671            EvalError::BudgetExceeded {
672                dimension: tatara_lisp_eval::error::BudgetDimension::Fuel,
673                limit,
674                ..
675            } => Stop::Exhausted { limit: *limit },
676            other => {
677                let at = other.span().unwrap_or_else(Span::synthetic);
678                let message = pipeline::display_keys(&crate::messages::describe(other, erased));
679                Stop::Error(Failure::new("runtime", place(at, &message)))
680            }
681        }
682    }
683}
684
685#[derive(Clone, Copy, Debug, PartialEq, Eq)]
686enum Action {
687    Load,
688    Evaluate,
689    Expand(Step),
690}
691
692/// The offset in the request's code of `span`, when it lies inside a fresh
693/// form that came from the code.
694fn in_code(
695    fresh_at: &BTreeMap<usize, (usize, Option<usize>, usize)>,
696    forms: &[Form],
697    span: Span,
698) -> Option<usize> {
699    let (start, (_, code_off, i)) = fresh_at.range(..=span.start).next_back()?;
700    let len = forms[*i].text.len() - forms[*i].at;
701    (span.start < start + len).then(|| (*code_off).map(|c| c + (span.start - start)))?
702}
703
704fn expand_form(
705    interp: &mut Interpreter<Host>,
706    host: &mut Host,
707    form: &Spanned,
708    step: Step,
709) -> tatara_lisp_eval::Result<Value> {
710    let head = match step {
711        Step::One => "macroexpand-1",
712        Step::All => "macroexpand",
713    };
714    let call = Sexp::List(vec![
715        Sexp::Atom(Atom::Symbol(head.to_string())),
716        Sexp::List(vec![
717            Sexp::Atom(Atom::Symbol("quote".to_string())),
718            form.to_sexp(),
719        ]),
720    ]);
721    interp.eval_spanned(&Spanned::from_sexp_synthetic(&call), host)
722}
723
724fn run_failure(e: RunError) -> Failure {
725    let code = match &e {
726        RunError::Parse(_) | RunError::Lower(_) => "parse",
727        RunError::Import(_) => "import",
728        RunError::Types(_) => "check",
729        RunError::Eval(_) => "runtime",
730    };
731    Failure::new(code, e.to_string())
732}
733
734/// The interpreter a session runs: the whole blue runtime, output captured,
735/// and every host name the frame does not grant bound to a refusal.
736fn build(frame: &Waku, interrupt: &Arc<AtomicBool>, host: &mut Host) -> Interpreter<Host> {
737    let mut interp = crate::interpreter(host);
738    capture_output(&mut interp);
739    for cap in Capability::host_effects() {
740        if frame.reach.grants(cap) {
741            continue;
742        }
743        for name in cap.names() {
744            if interp.lookup_global(name).is_none() {
745                continue;
746            }
747            interp.register_fn(name, Arity::Any, move |_: &[Value], h: &mut Host, span| {
748                h.reached.get_or_insert((cap, name));
749                Err(EvalError::native_fn(
750                    name,
751                    format!(
752                        "refused: `{name}` reaches the host ({}), which this session's frame does not grant",
753                        cap.label()
754                    ),
755                    span,
756                ))
757            });
758        }
759    }
760    // Processes the session's programs spawn are held to the same frame.
761    crate::attach_node_in_frame(&mut interp, frame);
762    interp.set_interrupt(Arc::clone(interrupt));
763    interp
764}
765
766/// Rebind every primitive that writes to stdout so the session receives what
767/// it writes. Each keeps its formatting.
768fn capture_output(interp: &mut Interpreter<Host>) {
769    interp.register_fn(
770        "write_stdout",
771        Arity::Exact(1),
772        |a: &[Value], h: &mut Host, s| match &a[0] {
773            Value::Str(t) => {
774                h.write(t);
775                Ok(Value::Nil)
776            }
777            other => Err(EvalError::type_mismatch("string", other.type_name(), s)),
778        },
779    );
780    interp.register_fn(
781        "display",
782        Arity::Exact(1),
783        |a: &[Value], h: &mut Host, _| {
784            h.write(&a[0].to_string());
785            Ok(Value::Nil)
786        },
787    );
788    interp.register_fn("print", Arity::Exact(1), |a: &[Value], h: &mut Host, _| {
789        h.write(&format!("{}\n", a[0]));
790        Ok(Value::Nil)
791    });
792    interp.register_fn(
793        "newline",
794        Arity::Exact(0),
795        |_: &[Value], h: &mut Host, _| {
796            h.write("\n");
797            Ok(Value::Nil)
798        },
799    );
800    interp.register_fn("println", Arity::Any, |a: &[Value], h: &mut Host, _| {
801        let line = a
802            .iter()
803            .map(ToString::to_string)
804            .collect::<Vec<_>>()
805            .join(" ");
806        h.write(&format!("{line}\n"));
807        Ok(Value::Nil)
808    });
809}
810
811/// A source's top-level forms, each with the text before it.
812fn split(text: &str) -> Result<Vec<Form>, blue_lang_syntax::ParseError> {
813    let tree = blue_lang_syntax::parse_program_tree(text)?;
814    let mut out = Vec::with_capacity(tree.len());
815    let mut prev = 0;
816    let last = tree.len().saturating_sub(1);
817    for (i, form) in tree.iter().enumerate() {
818        let end = if i == last { text.len() } else { form.span.end };
819        out.push(Form {
820            text: text[prev..end].to_string(),
821            at: form.span.start - prev,
822            defines: blue_lang_syntax::scope::definitions_of(form)
823                .into_iter()
824                .map(|(n, _, _)| n)
825                .collect(),
826            is_use: blue_lang_syntax::scope::use_target(form).is_some()
827                || blue_lang_syntax::scope::legacy_target(form).is_some(),
828            fresh: None,
829        });
830        prev = end;
831    }
832    Ok(out)
833}
834
835/// Whether `code` fails to parse only because it stops early: a block with
836/// no `end`, an open bracket or string. A REPL reads another line for it.
837#[must_use]
838pub fn incomplete(code: &str) -> bool {
839    match blue_lang_syntax::parse_program_tree(code) {
840        Ok(_) => false,
841        Err(e) => e.span.end >= code.trim_end().len() || e.message.starts_with("unterminated"),
842    }
843}
844
845/// A value as canonical blue text; a value no literal produces is described.
846#[must_use]
847pub fn render(v: &Value) -> String {
848    match crate::literal::value_literal(v) {
849        Some(Sexp::Atom(Atom::Symbol(s))) => unkey_name(&s),
850        Some(s) => crate::literal::format(&unkey(s)),
851        None => match v {
852            Value::Closure(_) => "<function>".to_string(),
853            Value::NativeFn(f) => format!("<builtin {}>", f.name),
854            other => other.to_string(),
855        },
856    }
857}
858
859/// A tree with every runtime key written as the author would: `%root/f` is
860/// `f`; `retsu/first` stays, and the formatter prints it `retsu::first`.
861fn unkey(s: Sexp) -> Sexp {
862    match s {
863        Sexp::Atom(Atom::Symbol(n)) => Sexp::Atom(Atom::Symbol(unkey_name(&n))),
864        Sexp::List(items) => Sexp::List(items.into_iter().map(unkey).collect()),
865        other => other,
866    }
867}
868
869fn unkey_name(n: &str) -> String {
870    let root = format!(
871        "{}{}",
872        blue_lang_check::names::ROOT_QUALIFIER,
873        blue_lang_syntax::QUALIFIER
874    );
875    n.strip_prefix(&root).unwrap_or(n).to_string()
876}
877
878/// A name as a program spells it from outside its namespace.
879fn spelled(ns: &Namespace, name: &str) -> String {
880    match ns {
881        Namespace::Bidama(p) => format!("{p}::{name}"),
882        _ => name.to_string(),
883    }
884}
885
886/// A definition's signature (the formatter's first line of it) and its doc:
887/// the comment lines directly above it.
888fn definition_doc(program: &ResolvedProgram, i: usize) -> (Option<String>, Option<String>) {
889    let Some(form) = program.forms().get(i) else {
890        return (None, None);
891    };
892    let rendered = crate::literal::format(&unkey(form.to_sexp()));
893    let signature = rendered.lines().next().map(str::to_string);
894    let text = program
895        .owner_of(i)
896        .and_then(|o| program.file(o))
897        .map(|f| f.text.as_str())
898        .unwrap_or_default();
899    let before = &text[..form.span.start.min(text.len())];
900    let mut lines: Vec<&str> = before
901        .lines()
902        .rev()
903        .skip_while(|l| l.trim().is_empty() && !before.ends_with('\n'))
904        .take_while(|l| l.trim_start().starts_with('#'))
905        .collect();
906    lines.reverse();
907    let doc = lines
908        .iter()
909        .map(|l| l.trim_start().trim_start_matches('#').trim())
910        .filter(|l| !l.starts_with("waive "))
911        .collect::<Vec<_>>()
912        .join(" ");
913    (signature, (!doc.is_empty()).then_some(doc))
914}
915
916fn canonical(p: &Path) -> PathBuf {
917    std::fs::canonicalize(p).unwrap_or_else(|_| p.to_path_buf())
918}
919
920#[cfg(test)]
921mod tests;