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