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 (mut outcome, names) = pipeline::check_stage(&program, &self.builtins, false);
532        outcome.diagnostics.retain(|d| {
533            !matches!(
534                d.code,
535                blue_lang_check::Code::B0023
536                    | blue_lang_check::Code::B0036
537                    | blue_lang_check::Code::B0037
538            )
539        });
540        let place = |top_level: usize, span: Span, message: &str| -> String {
541            let in_code = (program.owner_of(top_level) == Some(ResolvedProgram::ENTRY)
542                && !span.is_synthetic())
543            .then(|| in_code(&fresh_at, &forms, span))
544            .flatten();
545            match in_code {
546                Some(at) => {
547                    let (line, col) = Span::line_col(code, at.min(code.len()));
548                    format!("{CODE_LABEL}:{line}:{col}: {message}")
549                }
550                None => program.locate(top_level, span, message).to_string(),
551            }
552        };
553        if !outcome.ok() {
554            let code = outcome
555                .errors()
556                .next()
557                .map_or_else(|| "check".to_string(), |d| d.code.to_string());
558            let message = outcome
559                .errors()
560                .map(|d| {
561                    let mut line = place(d.top_level, d.span, &d.to_string());
562                    if let Some(help) = &d.help {
563                        line.push_str("\n  help: ");
564                        line.push_str(help);
565                    }
566                    line
567                })
568                .collect::<Vec<_>>()
569                .join("\n");
570            return Err(Stop::Error(Failure::new(code, message)));
571        }
572        let erased = crate::erase::erase_types(&pipeline::lower(&program, &names));
573
574        // What runs: every form of a bidama this interpreter has not seen, in
575        // program order, then the fresh forms in the order they were asked for.
576        let mut imported = Vec::new();
577        let mut asked: Vec<(usize, usize)> = Vec::new();
578        let mut new_packages = BTreeSet::new();
579        for (i, form) in program.forms().iter().enumerate() {
580            let owner = program.owner_of(i);
581            if owner == Some(ResolvedProgram::ENTRY) {
582                if let Some((order, _, _)) = fresh_at.get(&form.span.start) {
583                    asked.push((*order, i));
584                }
585                continue;
586            }
587            let package = owner
588                .and_then(|o| program.file(o))
589                .and_then(|f| f.package.clone());
590            if let Some(p) = package.filter(|p| !self.packages.contains(p)) {
591                new_packages.insert(p);
592                imported.push(i);
593            }
594        }
595        asked.sort_unstable();
596
597        let limit = budget
598            .or(self.budget)
599            .or(crate::execution_bounds().max_steps);
600        let _ = self.interp.set_budget(tatara_lisp_eval::vm::Budget {
601            fuel: limit,
602            max_depth: Some(crate::execution_bounds().max_call_depth),
603            quantum: None,
604        });
605        self.host.reached = None;
606
607        let mut value = Value::Nil;
608        let mut ran: BTreeSet<usize> = BTreeSet::new();
609        let mut failed = None;
610        for &i in &imported {
611            if let Err(e) = self.interp.eval_top_form(&erased[i], &mut self.host) {
612                failed = Some((i, e));
613                break;
614            }
615        }
616        if failed.is_none() {
617            self.packages.extend(new_packages);
618            for &(order, i) in &asked {
619                let step = match action {
620                    Action::Expand(step) => {
621                        expand_form(&mut self.interp, &mut self.host, &erased[i], step)
622                    }
623                    Action::Evaluate | Action::Load => {
624                        self.interp.eval_top_form(&erased[i], &mut self.host)
625                    }
626                };
627                match step {
628                    Ok(v) => {
629                        value = v;
630                        ran.insert(order);
631                    }
632                    Err(e) => {
633                        failed = Some((i, e));
634                        break;
635                    }
636                }
637            }
638        }
639        let stop = failed.map(|(i, e)| self.classify(&e, &erased, |span, msg| place(i, span, msg)));
640
641        match action {
642            Action::Load => ctx.forms = forms.into_iter().map(Form::settled).collect(),
643            Action::Evaluate => {
644                for f in forms {
645                    if f.persists() && f.fresh.is_some_and(|(order, _)| ran.contains(&order)) {
646                        splice(&mut ctx.forms, f.settled());
647                    }
648                }
649            }
650            Action::Expand(_) => {}
651        }
652        if action != Action::Expand(Step::One) && action != Action::Expand(Step::All) {
653            ctx.program = Some(program);
654            ctx.names = Some(names);
655        }
656        if let Some((reach, name)) = self.host.reached.take() {
657            return Err(Stop::Volatile {
658                reach,
659                name: name.to_string(),
660            });
661        }
662        match stop {
663            Some(s) => Err(s),
664            None => Ok(value),
665        }
666    }
667
668    fn classify(
669        &self,
670        e: &EvalError,
671        erased: &[Spanned],
672        place: impl Fn(Span, &str) -> String,
673    ) -> Stop {
674        match e {
675            EvalError::Halted => {
676                self.interrupt.store(false, Ordering::SeqCst);
677                Stop::Interrupted
678            }
679            EvalError::BudgetExceeded {
680                dimension: tatara_lisp_eval::error::BudgetDimension::Fuel,
681                limit,
682                ..
683            } => Stop::Exhausted { limit: *limit },
684            other => {
685                let at = other.span().unwrap_or_else(Span::synthetic);
686                let message = pipeline::display_keys(&crate::messages::describe(other, erased));
687                Stop::Error(Failure::new("runtime", place(at, &message)))
688            }
689        }
690    }
691}
692
693#[derive(Clone, Copy, Debug, PartialEq, Eq)]
694enum Action {
695    Load,
696    Evaluate,
697    Expand(Step),
698}
699
700/// The offset in the request's code of `span`, when it lies inside a fresh
701/// form that came from the code.
702fn in_code(
703    fresh_at: &BTreeMap<usize, (usize, Option<usize>, usize)>,
704    forms: &[Form],
705    span: Span,
706) -> Option<usize> {
707    let (start, (_, code_off, i)) = fresh_at.range(..=span.start).next_back()?;
708    let len = forms[*i].text.len() - forms[*i].at;
709    (span.start < start + len).then(|| (*code_off).map(|c| c + (span.start - start)))?
710}
711
712fn expand_form(
713    interp: &mut Interpreter<Host>,
714    host: &mut Host,
715    form: &Spanned,
716    step: Step,
717) -> tatara_lisp_eval::Result<Value> {
718    let head = match step {
719        Step::One => "macroexpand-1",
720        Step::All => "macroexpand",
721    };
722    let call = Sexp::List(vec![
723        Sexp::Atom(Atom::Symbol(head.to_string())),
724        Sexp::List(vec![
725            Sexp::Atom(Atom::Symbol("quote".to_string())),
726            form.to_sexp(),
727        ]),
728    ]);
729    interp.eval_spanned(&Spanned::from_sexp_synthetic(&call), host)
730}
731
732fn run_failure(e: RunError) -> Failure {
733    let code = match &e {
734        RunError::Parse(_) | RunError::Lower(_) => "parse",
735        RunError::Import(_) => "import",
736        RunError::Types(_) => "check",
737        RunError::Eval(_) => "runtime",
738    };
739    Failure::new(code, e.to_string())
740}
741
742/// The interpreter a session runs: the whole blue runtime, output captured,
743/// and every host name the frame does not grant bound to a refusal.
744fn build(frame: &Waku, interrupt: &Arc<AtomicBool>, host: &mut Host) -> Interpreter<Host> {
745    let mut interp = crate::interpreter(host);
746    capture_output(&mut interp);
747    for cap in Capability::host_effects() {
748        if frame.reach.grants(cap) {
749            continue;
750        }
751        for name in cap.names() {
752            if interp.lookup_global(name).is_none() {
753                continue;
754            }
755            interp.register_fn(name, Arity::Any, move |_: &[Value], h: &mut Host, span| {
756                h.reached.get_or_insert((cap, name));
757                Err(EvalError::native_fn(
758                    name,
759                    format!(
760                        "refused: `{name}` reaches the host ({}), which this session's frame does not grant",
761                        cap.label()
762                    ),
763                    span,
764                ))
765            });
766        }
767    }
768    // Processes the session's programs spawn are held to the same frame.
769    crate::attach_node_in_frame(&mut interp, frame);
770    interp.set_interrupt(Arc::clone(interrupt));
771    interp
772}
773
774/// Rebind every primitive that writes to stdout so the session receives what
775/// it writes. Each keeps its formatting.
776fn capture_output(interp: &mut Interpreter<Host>) {
777    interp.register_fn(
778        "write_stdout",
779        Arity::Exact(1),
780        |a: &[Value], h: &mut Host, s| match &a[0] {
781            Value::Str(t) => {
782                h.write(t);
783                Ok(Value::Nil)
784            }
785            other => Err(EvalError::type_mismatch("string", other.type_name(), s)),
786        },
787    );
788    interp.register_fn(
789        "display",
790        Arity::Exact(1),
791        |a: &[Value], h: &mut Host, _| {
792            h.write(&a[0].to_string());
793            Ok(Value::Nil)
794        },
795    );
796    interp.register_fn("print", Arity::Exact(1), |a: &[Value], h: &mut Host, _| {
797        h.write(&format!("{}\n", a[0]));
798        Ok(Value::Nil)
799    });
800    interp.register_fn(
801        "newline",
802        Arity::Exact(0),
803        |_: &[Value], h: &mut Host, _| {
804            h.write("\n");
805            Ok(Value::Nil)
806        },
807    );
808    interp.register_fn("println", Arity::Any, |a: &[Value], h: &mut Host, _| {
809        let line = a
810            .iter()
811            .map(ToString::to_string)
812            .collect::<Vec<_>>()
813            .join(" ");
814        h.write(&format!("{line}\n"));
815        Ok(Value::Nil)
816    });
817}
818
819/// A source's top-level forms, each with the text before it.
820fn split(text: &str) -> Result<Vec<Form>, blue_lang_syntax::ParseError> {
821    let tree = blue_lang_syntax::parse_program_tree(text)?;
822    let mut out = Vec::with_capacity(tree.len());
823    let mut prev = 0;
824    let last = tree.len().saturating_sub(1);
825    for (i, form) in tree.iter().enumerate() {
826        let end = if i == last { text.len() } else { form.span.end };
827        out.push(Form {
828            text: text[prev..end].to_string(),
829            at: form.span.start - prev,
830            defines: blue_lang_syntax::scope::definitions_of(form)
831                .into_iter()
832                .map(|(n, _, _)| n)
833                .collect(),
834            is_use: blue_lang_syntax::scope::use_target(form).is_some()
835                || blue_lang_syntax::scope::legacy_target(form).is_some(),
836            fresh: None,
837        });
838        prev = end;
839    }
840    Ok(out)
841}
842
843/// Whether `code` fails to parse only because it stops early: a block with
844/// no `end`, an open bracket or string. A REPL reads another line for it.
845#[must_use]
846pub fn incomplete(code: &str) -> bool {
847    match blue_lang_syntax::parse_program_tree(code) {
848        Ok(_) => false,
849        Err(e) => e.span.end >= code.trim_end().len() || e.message.starts_with("unterminated"),
850    }
851}
852
853/// A value as canonical blue text; a value no literal produces is described.
854#[must_use]
855pub fn render(v: &Value) -> String {
856    match crate::literal::value_literal(v) {
857        Some(Sexp::Atom(Atom::Symbol(s))) => unkey_name(&s),
858        Some(s) => crate::literal::format(&unkey(s)),
859        None => match v {
860            Value::Closure(_) => "<function>".to_string(),
861            Value::NativeFn(f) => format!("<builtin {}>", f.name),
862            other => other.to_string(),
863        },
864    }
865}
866
867/// A tree with every runtime key written as the author would: `%root/f` is
868/// `f`; `retsu/first` stays, and the formatter prints it `retsu::first`.
869fn unkey(s: Sexp) -> Sexp {
870    match s {
871        Sexp::Atom(Atom::Symbol(n)) => Sexp::Atom(Atom::Symbol(unkey_name(&n))),
872        Sexp::List(items) => Sexp::List(items.into_iter().map(unkey).collect()),
873        other => other,
874    }
875}
876
877fn unkey_name(n: &str) -> String {
878    let root = format!(
879        "{}{}",
880        blue_lang_check::names::ROOT_QUALIFIER,
881        blue_lang_syntax::QUALIFIER
882    );
883    n.strip_prefix(&root).unwrap_or(n).to_string()
884}
885
886/// A name as a program spells it from outside its namespace.
887fn spelled(ns: &Namespace, name: &str) -> String {
888    match ns {
889        Namespace::Bidama(p) => format!("{p}::{name}"),
890        _ => name.to_string(),
891    }
892}
893
894/// A definition's signature (the formatter's first line of it) and its doc:
895/// the comment lines directly above it.
896fn definition_doc(program: &ResolvedProgram, i: usize) -> (Option<String>, Option<String>) {
897    let Some(form) = program.forms().get(i) else {
898        return (None, None);
899    };
900    let rendered = crate::literal::format(&unkey(form.to_sexp()));
901    let signature = rendered.lines().next().map(str::to_string);
902    let text = program
903        .owner_of(i)
904        .and_then(|o| program.file(o))
905        .map(|f| f.text.as_str())
906        .unwrap_or_default();
907    let before = &text[..form.span.start.min(text.len())];
908    let mut lines: Vec<&str> = before
909        .lines()
910        .rev()
911        .skip_while(|l| l.trim().is_empty() && !before.ends_with('\n'))
912        .take_while(|l| l.trim_start().starts_with('#'))
913        .collect();
914    lines.reverse();
915    let doc = lines
916        .iter()
917        .map(|l| l.trim_start().trim_start_matches('#').trim())
918        .filter(|l| !l.starts_with("waive "))
919        .collect::<Vec<_>>()
920        .join(" ");
921    (signature, (!doc.is_empty()).then_some(doc))
922}
923
924fn canonical(p: &Path) -> PathBuf {
925    std::fs::canonicalize(p).unwrap_or_else(|_| p.to_path_buf())
926}
927
928#[cfg(test)]
929mod tests;