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