Skip to main content

decl_lang/
session.rs

1//! The session object (docs/tooling/02_repl.md §1) — a port of the
2//! reference implementation's session.ts: a universe — the modules loaded
3//! from an entry file, their texts taken as a snapshot — plus an operation
4//! log (bindings, document edits, session declarations, reloads). The
5//! state is the universe with the log applied, recomputed deterministically
6//! from the snapshot, which is what makes `:undo` exact and a scripted
7//! session reproducible. The REPL (repl.rs) drives it; nothing here prints,
8//! and every answer is the same checker, inference, and engine the command
9//! line runs.
10use crate::ast::{Decl, DeclBody, Expr, TPart};
11use crate::checker::check_module;
12use crate::engine::{fmt_f, Engine, RootSrc};
13use crate::fmt::format;
14use crate::infer::{infer, make_ctx, std_names, type_text, Ctx, Ty};
15use crate::module::{load_modules, Module};
16use crate::package::{open_package_universe, verify_lock};
17use crate::parse::parse_source;
18use crate::semantics::{
19    json_str, parse_path, path_str, read_json, rec_members, seg_text, sort_diags, Diag, Env, Fail,
20    MKind, RTk, Scope, Seg, SegPath, SlotState, Value, RT,
21};
22use regex::Regex;
23use std::cell::{Cell, RefCell};
24use std::collections::{HashMap, HashSet};
25use std::path::{Path, PathBuf};
26use std::rc::Rc;
27use std::sync::LazyLock;
28use std::time::Instant;
29
30// a detached declaration's type text on one line (compiled once)
31static SQUEEZE_WS: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"\s*\n\s*").unwrap());
32
33// ---------------- operations ----------------
34#[derive(Clone, Debug)]
35pub enum BindSource {
36    File { file: String, text: String },
37    Inline { text: String },
38    Expr { text: String },
39}
40
41#[derive(Clone, Copy, Debug, PartialEq)]
42pub enum EditKind {
43    Create,
44    Update,
45    Remove,
46}
47impl EditKind {
48    pub fn word(self) -> &'static str {
49        match self {
50            EditKind::Create => "create",
51            EditKind::Update => "update",
52            EditKind::Remove => "remove",
53        }
54    }
55}
56
57#[derive(Clone)]
58pub enum Op {
59    Bind {
60        name: String,
61        src: BindSource,
62    },
63    Unbind {
64        name: String,
65    },
66    Edit {
67        kind: EditKind,
68        path: String,
69        expr: Option<String>,
70    },
71    Declare {
72        name: String,
73        text: String,
74    },
75    Output {
76        name: String,
77        ty: Option<String>,
78        expr: String,
79    },
80    Drop {
81        name: String,
82    },
83    Reload {
84        snapshot: HashMap<PathBuf, String>,
85    },
86    Reset,
87}
88
89#[derive(Clone, Copy, Debug, PartialEq)]
90pub enum Origin {
91    File,
92    Inline,
93    Expr,
94    Fallback,
95    Detached,
96}
97impl Origin {
98    pub fn word(self) -> &'static str {
99        match self {
100            Origin::File => "file",
101            Origin::Inline => "inline",
102            Origin::Expr => "expr",
103            Origin::Fallback => "fallback",
104            Origin::Detached => "detached",
105        }
106    }
107}
108
109/// the document a root is built from, as the session holds it
110#[derive(Clone)]
111pub struct Document {
112    pub origin: Origin,
113    pub file: Option<String>,
114    pub doc: Value,  // read_json's shape
115    pub base: Value, // what it started from (`:diff`)
116    pub edited: bool,
117}
118
119#[derive(Default)]
120struct State {
121    snapshot: HashMap<PathBuf, String>,
122    decls: Vec<(String, String)>, // session declarations, in order
123    outputs: Vec<(String, Option<String>, String)>, // session outputs `x = e`
124    documents: Vec<(String, Document)>,
125}
126impl State {
127    fn decl(&self, n: &str) -> Option<&String> {
128        self.decls.iter().find(|(k, _)| k == n).map(|(_, t)| t)
129    }
130    fn output(&self, n: &str) -> Option<(&Option<String>, &String)> {
131        self.outputs
132            .iter()
133            .find(|(k, _, _)| k == n)
134            .map(|(_, t, e)| (t, e))
135    }
136    fn document(&self, n: &str) -> Option<&Document> {
137        self.documents.iter().find(|(k, _)| k == n).map(|(_, d)| d)
138    }
139    fn document_mut(&mut self, n: &str) -> Option<&mut Document> {
140        self.documents
141            .iter_mut()
142            .find(|(k, _)| k == n)
143            .map(|(_, d)| d)
144    }
145    fn set_document(&mut self, n: &str, d: Document) {
146        if let Some(e) = self.documents.iter_mut().find(|(k, _)| k == n) {
147            e.1 = d;
148        } else {
149            self.documents.push((n.to_string(), d));
150        }
151    }
152    fn remove_decl(&mut self, n: &str) -> bool {
153        let before = self.decls.len();
154        self.decls.retain(|(k, _)| k != n);
155        before != self.decls.len()
156    }
157    fn remove_output(&mut self, n: &str) -> bool {
158        let before = self.outputs.len();
159        self.outputs.retain(|(k, _, _)| k != n);
160        before != self.outputs.len()
161    }
162}
163
164#[derive(Debug, Clone)]
165pub struct SessionError(pub String);
166impl SessionError {
167    fn new(msg: impl Into<String>) -> SessionError {
168        SessionError(msg.into())
169    }
170}
171pub type SResult<T> = Result<T, SessionError>;
172
173#[derive(Clone, Copy, Debug)]
174pub struct Timing {
175    pub load: f64,
176    pub check: f64,
177    pub bind: f64,
178    pub evaluate: f64,
179    pub total: f64,
180    /// the incremental step: how many slots were recomputed, and out of how many (a full run: None)
181    pub recomputed: Option<usize>,
182    pub slots: Option<usize>,
183}
184
185/// full recomputation on every question (the harness's cross-check)
186pub fn full_recompute() -> bool {
187    std::env::var("DECL_FULL_RECOMPUTE")
188        .map(|v| !v.is_empty())
189        .unwrap_or(false)
190}
191
192#[derive(Clone, Copy, PartialEq)]
193pub enum Mode {
194    Check,
195    Lazy,
196    Full,
197}
198
199#[derive(Clone)]
200pub struct Run {
201    pub modules: Vec<Rc<Module>>,
202    pub entry: Option<Rc<Module>>,
203    pub load_diags: Vec<Diag>,
204    pub checks: Vec<(String, Diag)>,
205    pub session_checks: Vec<Diag>, // session outputs whose expressions do not check (path: the output)
206    pub session_roots: Vec<(String, Rc<Expr>, RT)>, // the session outputs bound, as bound
207    pub eng: Option<Rc<Engine>>,
208    pub diags: Vec<Diag>,
209    pub timing: Timing,
210}
211
212pub struct RootInfo {
213    pub kind: &'static str, // "output" | "input"
214    pub name: String,
215    pub module: String, // "" for a session root
216    pub exported: bool,
217    pub session: bool,
218    pub binding: String, // for inputs and detached outputs
219    pub detail: String,  // the bound file, when there is one
220    pub edited: bool,
221}
222
223pub struct ExprResult {
224    pub value: Option<String>,
225    pub diags: Vec<Diag>,
226    /// Some(code, message): a failure; an empty message prints only `(invalid)`
227    pub error: Option<(Option<String>, String)>,
228}
229
230fn ms(i: Instant) -> f64 {
231    i.elapsed().as_secs_f64() * 1000.0
232}
233pub fn is_root_diag(d: &Diag, root: &str) -> bool {
234    d.path == root
235        || d.path.starts_with(&format!("{root}."))
236        || d.path.starts_with(&format!("{root}["))
237}
238
239/// parse one expression: the text is wrapped in a constant declaration
240pub fn parse_expr(text: &str) -> SResult<Rc<Expr>> {
241    let r = parse_source(&format!("const __e = {text}\n"));
242    if r.errors.is_empty() && r.decls.len() == 1 {
243        if let DeclBody::Const { expr, .. } = &r.decls[0].body {
244            return Ok(expr.clone());
245        }
246    }
247    Err(SessionError::new(format!(
248        "cannot parse expression: {}",
249        text.trim()
250    )))
251}
252
253/// parse one module-level declaration; returns it with its name
254pub fn parse_decl(text: &str) -> SResult<(Decl, String)> {
255    let r = parse_source(&format!("{}\n", text.trim()));
256    if !r.errors.is_empty() || r.decls.len() != 1 {
257        return Err(SessionError::new(format!(
258            "cannot parse declaration: {}",
259            text.trim().lines().next().unwrap_or("")
260        )));
261    }
262    let d = r.decls.into_iter().next().unwrap();
263    let name = match (&d.body, d.name()) {
264        (_, Some(n)) => n.to_string(),
265        (DeclBody::Import { from, .. }, None) => format!("import {from}"),
266        (DeclBody::ReExport { from, .. }, None) => format!("re_export {from}"),
267        _ => String::new(),
268    };
269    Ok((d, name))
270}
271
272fn parse_doc(text: &str, what: &str) -> SResult<Value> {
273    read_json(text).map_err(|_| SessionError::new(format!("{what} is not well-formed JSON")))
274}
275
276// ---------------- JSON documents (read_json's shape) ----------------
277pub fn doc_json(v: &Value) -> String {
278    match v {
279        Value::Null => "null".into(),
280        Value::Bool(b) => b.to_string(),
281        Value::Int(i) => i.to_string(),
282        Value::Float(f) => fmt_f(*f),
283        Value::Str(s) => json_str(s),
284        Value::JArr(items) => format!(
285            "[{}]",
286            items.iter().map(doc_json).collect::<Vec<_>>().join(",")
287        ),
288        Value::JObj(es) => format!(
289            "{{{}}}",
290            es.iter()
291                .map(|(k, x)| format!("{}:{}", json_str(k), doc_json(x)))
292                .collect::<Vec<_>>()
293                .join(",")
294        ),
295        _ => "null".into(),
296    }
297}
298
299fn doc_step(v: &Value, seg: &Seg) -> Option<Value> {
300    match (v, seg) {
301        (Value::JObj(es), Seg::Name(k)) | (Value::JObj(es), Seg::Key(k)) => {
302            es.iter().find(|(kk, _)| kk == k).map(|(_, x)| x.clone())
303        }
304        (Value::JArr(items), Seg::Idx(i)) => items.get(*i).cloned(),
305        _ => None,
306    }
307}
308
309// ---------------- pretty printing ----------------
310/// canonical JSON, re-indented (numbers and strings untouched)
311pub fn pretty_json(compact: &str) -> String {
312    let cs: Vec<char> = compact.chars().collect();
313    let mut out = String::new();
314    let mut depth = 0usize;
315    let mut i = 0;
316    let pad = |d: usize| "  ".repeat(d);
317    while i < cs.len() {
318        let c = cs[i];
319        if c == '"' {
320            let mut j = i + 1;
321            while j < cs.len() && cs[j] != '"' {
322                if cs[j] == '\\' {
323                    j += 1;
324                }
325                j += 1;
326            }
327            out.extend(&cs[i..=j.min(cs.len() - 1)]);
328            i = j + 1;
329            continue;
330        }
331        if c == '{' || c == '[' {
332            let close = if c == '{' { '}' } else { ']' };
333            if i + 1 < cs.len() && cs[i + 1] == close {
334                out.push(c);
335                out.push(close);
336                i += 2;
337                continue;
338            }
339            depth += 1;
340            out.push(c);
341            out.push('\n');
342            out.push_str(&pad(depth));
343            i += 1;
344            continue;
345        }
346        if c == '}' || c == ']' {
347            depth = depth.saturating_sub(1);
348            out.push('\n');
349            out.push_str(&pad(depth));
350            out.push(c);
351            i += 1;
352            continue;
353        }
354        if c == ',' {
355            out.push_str(",\n");
356            out.push_str(&pad(depth));
357            i += 1;
358            continue;
359        }
360        if c == ':' {
361            out.push_str(": ");
362            i += 1;
363            continue;
364        }
365        out.push(c);
366        i += 1;
367    }
368    out
369}
370
371fn identifiers(text: &str) -> HashSet<String> {
372    Regex::new(r"[A-Za-z_][A-Za-z0-9_]*")
373        .unwrap()
374        .find_iter(text)
375        .map(|m| m.as_str().to_string())
376        .collect()
377}
378
379fn relative_path(from_dir: &Path, p: &Path) -> String {
380    let a: Vec<_> = from_dir.components().collect();
381    let b: Vec<_> = p.components().collect();
382    let mut i = 0;
383    while i < a.len() && i < b.len() && a[i] == b[i] {
384        i += 1;
385    }
386    let mut parts: Vec<String> = vec!["..".to_string(); a.len() - i];
387    parts.extend(
388        b[i..]
389            .iter()
390            .map(|c| c.as_os_str().to_string_lossy().to_string()),
391    );
392    parts.join("/")
393}
394
395// ---------------- the session ----------------
396pub struct Session {
397    pub entry_path: Option<PathBuf>, // absolute
398    pub log: Vec<Op>,
399    pub cursor: usize,
400    pub last_timing: Cell<Option<Timing>>,
401    snapshot0: HashMap<PathBuf, String>,
402    state: State,
403    // the last full run, kept for the incremental step (§6): reused as long
404    // as the universe's texts and declarations are the same, its engine
405    // rebinding the documents that changed and recomputing what read them
406    last: RefCell<Option<Last>>,
407    /// texts that override the disk (the language server's open buffers), by absolute path
408    pub overlay: HashMap<PathBuf, String>,
409}
410
411pub const SCRATCH: &str = "<session>";
412
413#[derive(Clone)]
414struct Last {
415    key: String,
416    docs: Vec<(String, String)>,
417    run: Run,
418}
419
420fn under(path: &str, root: &str) -> bool {
421    path == root || path.starts_with(&format!("{root}.")) || path.starts_with(&format!("{root}["))
422}
423
424impl Session {
425    pub fn new(entry: Option<&str>) -> Session {
426        Session::with_overlay(entry, None)
427    }
428    /// `overlay`: texts that override the disk (the language server's open buffers), by absolute path
429    pub fn with_overlay(
430        entry: Option<&str>,
431        overlay: Option<&HashMap<PathBuf, String>>,
432    ) -> Session {
433        let entry_path = entry.map(|e| std::path::absolute(e).unwrap_or_else(|_| PathBuf::from(e)));
434        let mut s = Session {
435            entry_path,
436            log: vec![],
437            cursor: 0,
438            last_timing: Cell::new(None),
439            snapshot0: HashMap::new(),
440            state: State::default(),
441            last: RefCell::new(None),
442            overlay: overlay.cloned().unwrap_or_default(),
443        };
444        s.snapshot0 = s.snapshot_from_disk();
445        s.state = s.initial_state();
446        s
447    }
448
449    pub fn entry_abs(&self) -> PathBuf {
450        self.entry_path.clone().unwrap_or_else(|| {
451            std::path::absolute(SCRATCH).unwrap_or_else(|_| PathBuf::from(SCRATCH))
452        })
453    }
454    pub fn entry_name(&self) -> String {
455        self.entry_path
456            .as_ref()
457            .and_then(|p| p.file_name())
458            .map(|n| n.to_string_lossy().to_string())
459            .unwrap_or_else(|| SCRATCH.to_string())
460    }
461
462    // the universe's texts as they are on disk now: the entry and every
463    // module reachable from it (a module that cannot be read is absent and
464    // reported on use, as the command line reports it)
465    fn snapshot_from_disk(&self) -> HashMap<PathBuf, String> {
466        let mut snap = HashMap::new();
467        let Some(entry) = &self.entry_path else {
468            return snap;
469        };
470        let pkg = open_package_universe(entry);
471        let r = load_modules(
472            entry,
473            pkg.as_ref().map(|u| &u.resolver),
474            Some(&self.overlay),
475        );
476        let mut paths: Vec<PathBuf> = vec![entry.clone()];
477        paths.extend(r.modules.iter().map(|m| m.path.clone()));
478        for p in paths {
479            if let Some(t) = self.overlay.get(&p) {
480                snap.insert(p, t.clone());
481                continue;
482            }
483            if let Ok(t) = std::fs::read_to_string(&p) {
484                snap.insert(p, t);
485            }
486        }
487        snap
488    }
489    fn initial_state(&self) -> State {
490        State {
491            snapshot: self.snapshot0.clone(),
492            decls: vec![],
493            outputs: vec![],
494            documents: vec![],
495        }
496    }
497
498    // ---- the log ----
499    pub fn apply(&mut self, op: Op) -> SResult<()> {
500        self.log.truncate(self.cursor); // a new operation after :undo discards what was undone
501        let mut st = std::mem::take(&mut self.state);
502        let r = self.apply_to(&mut st, &op); // a refused operation errs and is not logged
503        self.state = st;
504        r?;
505        self.log.push(op);
506        self.cursor += 1;
507        Ok(())
508    }
509    pub fn undo(&mut self, n: usize) -> usize {
510        let to = self.cursor.saturating_sub(n);
511        let stepped = self.cursor - to;
512        self.cursor = to;
513        self.replay();
514        stepped
515    }
516    pub fn redo(&mut self, n: usize) -> usize {
517        let to = (self.cursor + n).min(self.log.len());
518        let stepped = to - self.cursor;
519        self.cursor = to;
520        self.replay();
521        stepped
522    }
523    fn replay(&mut self) {
524        let mut st = self.initial_state();
525        let ops: Vec<Op> = self.log[..self.cursor].to_vec();
526        for op in &ops {
527            let _ = self.apply_to(&mut st, op);
528        }
529        self.state = st;
530    }
531    pub fn reload_op(&self) -> Op {
532        Op::Reload {
533            snapshot: self.snapshot_from_disk(),
534        }
535    }
536
537    fn apply_to(&self, st: &mut State, op: &Op) -> SResult<()> {
538        match op {
539            Op::Bind { name, src } => {
540                let (modules, _, _) = self.build(st);
541                if !modules
542                    .iter()
543                    .any(|m| m.env.inputs.borrow().contains_key(name))
544                {
545                    return Err(SessionError::new(format!("no input named {name}")));
546                }
547                let (doc, origin, file) = match src {
548                    BindSource::Expr { text } => (self.eval_to_doc(st, text)?, Origin::Expr, None),
549                    BindSource::File { file, text } => {
550                        (parse_doc(text, file)?, Origin::File, Some(file.clone()))
551                    }
552                    BindSource::Inline { text } => {
553                        (parse_doc(text, "the document")?, Origin::Inline, None)
554                    }
555                };
556                st.set_document(
557                    name,
558                    Document {
559                        origin,
560                        file,
561                        base: doc.clone(),
562                        doc,
563                        edited: false,
564                    },
565                );
566                Ok(())
567            }
568            Op::Unbind { name } => {
569                if st.document(name).is_none() {
570                    return Err(SessionError::new(format!("{name} is not bound")));
571                }
572                st.documents.retain(|(k, _)| k != name);
573                Ok(())
574            }
575            Op::Edit { kind, path, expr } => self.edit(st, *kind, path, expr.as_deref()),
576            Op::Declare { name, text } => {
577                st.remove_decl(name);
578                st.remove_output(name);
579                st.decls.push((name.clone(), text.clone()));
580                Ok(())
581            }
582            Op::Output { name, ty, expr } => {
583                st.remove_decl(name);
584                st.remove_output(name);
585                st.outputs.push((name.clone(), ty.clone(), expr.clone()));
586                Ok(())
587            }
588            Op::Drop { name } => {
589                let a = st.remove_decl(name);
590                let b = st.remove_output(name);
591                if !a && !b {
592                    return Err(SessionError::new(format!(
593                        "no session declaration named {name}"
594                    )));
595                }
596                Ok(())
597            }
598            Op::Reload { snapshot } => {
599                st.snapshot = snapshot.clone();
600                Ok(())
601            }
602            Op::Reset => {
603                st.decls.clear();
604                st.outputs.clear();
605                st.documents.clear();
606                Ok(())
607            }
608        }
609    }
610
611    // ---- documents and edits (§3) ----
612    fn eval_to_doc(&self, st: &State, expr_text: &str) -> SResult<Value> {
613        let expr = parse_expr(expr_text)?;
614        let r = self.engine_for(st);
615        if r.eng.is_none() || r.entry.is_none() {
616            return Err(SessionError::new(self.load_failure(&r)));
617        }
618        let sc = Scope::new("", Some(r.entry.as_ref().unwrap().env.clone()));
619        self.scratch(&r, |eng, _| {
620            let result = (|| -> Result<Value, Fail> {
621                let v = eng.ev(&expr, &sc)?;
622                let v = eng.materialize(v, &[Seg::Name("_".into())])?;
623                eng.force_all(&v);
624                Ok(v)
625            })();
626            match result {
627                Ok(v) => {
628                    let text = eng.serialize(&v, "", false);
629                    if text.is_empty() {
630                        return Err(SessionError::new("the value is not data"));
631                    }
632                    read_json(&text).map_err(|_| SessionError::new("the value is not data"))
633                }
634                Err(Fail::Eval(e)) => Err(SessionError::new(e.msg)),
635                Err(_) => Err(SessionError::new("the value is invalid")),
636            }
637        })
638    }
639
640    fn edit(&self, st: &mut State, kind: EditKind, path: &str, expr: Option<&str>) -> SResult<()> {
641        let segs: SegPath =
642            parse_path(path, "").map_err(|_| SessionError::new(format!("bad path {path}")))?;
643        let root = match segs.first() {
644            Some(Seg::Name(n)) if !n.is_empty() => n.clone(),
645            _ => return Err(SessionError::new(format!("bad path {path}"))),
646        };
647        if segs.len() < 2 {
648            return Err(SessionError::new(format!(
649                "a path below a root is required, got {path}"
650            )));
651        }
652        let value = match kind {
653            EditKind::Remove => None,
654            _ => Some(self.eval_to_doc(st, expr.unwrap_or(""))?),
655        };
656        self.document_of(st, &root)?;
657        let doc = st.document(&root).cloned().unwrap();
658        let new_doc = edit_value(&doc.doc, &segs, 1, kind, value.as_ref(), path)?;
659        let d = st.document_mut(&root).unwrap();
660        d.doc = new_doc;
661        d.edited = true;
662        Ok(())
663    }
664
665    // the document of a root, made if the root has none yet: an unbound
666    // input's fallback, or an output detached into its settable projection
667    fn document_of(&self, st: &mut State, root: &str) -> SResult<()> {
668        if st.document(root).is_some() {
669            return Ok(());
670        }
671        let (modules, _, _) = self.build(st);
672        let input_mod = modules
673            .iter()
674            .any(|m| m.env.inputs.borrow().contains_key(root));
675        let output_mod = modules
676            .iter()
677            .any(|m| m.env.outputs.borrow().iter().any(|(o, _, _)| o == root));
678        if !input_mod && !output_mod {
679            return Err(SessionError::new(if st.output(root).is_some() {
680                format!("{root} is a session output; edit the roots it reads")
681            } else {
682                format!("no root named {root}")
683            }));
684        }
685        let r = self.run_state(st, Mode::Full);
686        let (Some(eng), Some(entry)) = (&r.eng, &r.entry) else {
687            return Err(SessionError::new(self.load_failure(&r)));
688        };
689        let v = entry.env.root(root);
690        let bad = v.is_none()
691            || r.diags
692                .iter()
693                .any(|d| d.severity == "error" && is_root_diag(d, root));
694        if bad {
695            return Err(SessionError::new(format!(
696                "{root} is invalid; fix it before editing"
697            )));
698        }
699        let text = eng.serialize(&v.unwrap(), root, true);
700        let doc = read_json(&text)
701            .map_err(|_| SessionError::new(format!("{root} is invalid; fix it before editing")))?;
702        st.set_document(
703            root,
704            Document {
705                origin: if input_mod {
706                    Origin::Fallback
707                } else {
708                    Origin::Detached
709                },
710                file: None,
711                base: doc.clone(),
712                doc,
713                edited: false,
714            },
715        );
716        Ok(())
717    }
718
719    // ---- building the universe ----
720    fn build(&self, st: &State) -> (Vec<Rc<Module>>, Option<Rc<Module>>, Vec<Diag>) {
721        let entry_abs = self.entry_abs();
722        let mut overlay = st.snapshot.clone();
723        let text = st.snapshot.get(&entry_abs).cloned().or_else(|| {
724            if self.entry_path.is_none() {
725                Some(String::new())
726            } else {
727                None
728            }
729        });
730        if let Some(mut text) = text {
731            let detached: Vec<String> = st
732                .documents
733                .iter()
734                .filter(|(_, d)| d.origin == Origin::Detached)
735                .map(|(n, _)| n.clone())
736                .collect();
737            text = detach_outputs(&text, &detached);
738            let extra: Vec<&str> = st.decls.iter().map(|(_, t)| t.as_str()).collect();
739            if !extra.is_empty() {
740                if text.ends_with('\n') {
741                    text.pop();
742                }
743                text.push('\n');
744                text.push_str(&extra.join("\n"));
745                text.push('\n');
746            }
747            overlay.insert(entry_abs.clone(), text);
748        }
749        let pkg = if self.entry_path.is_some() {
750            open_package_universe(&entry_abs)
751        } else {
752            None
753        };
754        let mut diags: Vec<Diag> = vec![];
755        if let Some(u) = &pkg {
756            diags.extend(u.diags.clone());
757            diags.extend(verify_lock(u));
758        }
759        let r = load_modules(
760            &entry_abs,
761            pkg.as_ref().map(|u| &u.resolver),
762            Some(&overlay),
763        );
764        diags.extend(r.diags);
765        (r.modules, r.entry, diags)
766    }
767
768    fn load_failure(&self, r: &Run) -> String {
769        match r.load_diags.first() {
770            Some(d) => format!(
771                "{}{}",
772                d.code
773                    .as_ref()
774                    .map(|c| format!("[{c}] "))
775                    .unwrap_or_default(),
776                d.message
777            ),
778            None => "the universe did not load".into(),
779        }
780    }
781
782    // an inference context over the entry's scope in which the session's
783    // outputs are variables of their inferred types, in declaration order
784    fn session_ctx(
785        &self,
786        st: &State,
787        env: &Rc<Env>,
788        report: Rc<dyn Fn(&str, String)>,
789        up_to: Option<&str>,
790    ) -> Ctx {
791        let mut cx = make_ctx(env.clone(), report);
792        for (name, ty_text, expr_text) in &st.outputs {
793            if Some(name.as_str()) == up_to {
794                break;
795            }
796            let Ok(expr) = parse_expr(expr_text) else {
797                continue;
798            }; // a session output that does not parse is not in scope
799            let mut quiet = make_ctx(env.clone(), Rc::new(|_, _| {}));
800            quiet.vars = cx.vars.clone();
801            let mut rt = infer(&quiet, &expr).rt;
802            if let Some(t) = ty_text {
803                if let Ok((d, _)) = parse_decl(&format!("output {name}: {t} = 0")) {
804                    if let DeclBody::Output { ty, .. } = &d.body {
805                        rt = env.resolve(ty, None).ok();
806                    }
807                }
808            }
809            cx.vars.insert(name.clone(), Ty { rt, abs: false });
810        }
811        cx
812    }
813
814    /// load, check, and (unless `mode` says otherwise) evaluate the state
815    pub fn run(&self, mode: Mode) -> Run {
816        self.run_state(&self.state, mode)
817    }
818    fn run_state(&self, st: &State, mode: Mode) -> Run {
819        if mode == Mode::Full && !full_recompute() {
820            if let Some(r) = self.step_from(st) {
821                return r;
822            }
823        }
824        let r = self.run_fresh(st, mode);
825        if mode == Mode::Full {
826            *self.last.borrow_mut() = if r.eng.is_some() {
827                Some(Last {
828                    key: self.universe_key(st),
829                    docs: self.doc_keys(st),
830                    run: r.clone(),
831                })
832            } else {
833                None
834            };
835        }
836        r
837    }
838    fn universe_key(&self, st: &State) -> String {
839        let mut snap: Vec<(String, &String)> = st
840            .snapshot
841            .iter()
842            .map(|(p, t)| (p.display().to_string(), t))
843            .collect();
844        snap.sort();
845        let mut detached: Vec<&String> = st
846            .documents
847            .iter()
848            .filter(|(_, d)| d.origin == Origin::Detached)
849            .map(|(n, _)| n)
850            .collect();
851        detached.sort();
852        format!(
853            "{:?}|{:?}|{:?}|{:?}|{:?}",
854            self.entry_abs(),
855            snap,
856            st.decls,
857            st.outputs,
858            detached
859        )
860    }
861    fn doc_keys(&self, st: &State) -> Vec<(String, String)> {
862        st.documents
863            .iter()
864            .map(|(n, d)| (n.clone(), doc_json(&d.doc)))
865            .collect()
866    }
867    // the incremental step: the same universe, some documents changed
868    fn step_from(&self, st: &State) -> Option<Run> {
869        let last = self.last.borrow().clone()?;
870        let (Some(eng), Some(entry)) = (last.run.eng.clone(), last.run.entry.clone()) else {
871            return None;
872        };
873        if last.key != self.universe_key(st) {
874            return None;
875        }
876        let docs = self.doc_keys(st);
877        let mut changed: Vec<String> = vec![];
878        for (n, k) in &docs {
879            if last.docs.iter().find(|(m, _)| m == n).map(|(_, v)| v) != Some(k) {
880                changed.push(n.clone());
881            }
882        }
883        for (n, _) in &last.docs {
884            if !docs.iter().any(|(m, _)| m == n) {
885                changed.push(n.clone());
886            }
887        }
888        if changed.is_empty() {
889            self.last_timing.set(Some(last.run.timing));
890            return Some(last.run.clone());
891        }
892        let t0 = Instant::now();
893        let r = &last.run;
894        let env = entry.env.clone();
895        // 1. what the change touches: the roots themselves, every slot under
896        //    them, and the `$referrers` queries over types instantiated under them
897        let mut seeds: Vec<String> = vec![];
898        let read_keys: Vec<String> = eng.reads.borrow().keys().cloned().collect();
899        let registry = env.registry_snapshot();
900        for root in &changed {
901            seeds.push(format!("root:{root}"));
902            for k in &read_keys {
903                if !k.starts_with("root:") && under(k.strip_prefix("assert:").unwrap_or(k), root) {
904                    seeds.push(k.clone());
905                }
906            }
907            for inst in &registry {
908                let b = inst.borrow();
909                if under(&path_str(&b.path, None), root) {
910                    if let Some(tn) = &b.type_name {
911                        seeds.push(format!("referrers:{tn}"));
912                    }
913                }
914            }
915        }
916        // 2. everything that read them, transitively
917        let mut readers: HashMap<String, HashSet<String>> = HashMap::new();
918        for (reader, set) in eng.reads.borrow().iter() {
919            for k in set {
920                readers.entry(k.clone()).or_default().insert(reader.clone());
921            }
922        }
923        let mut invalid: HashSet<String> = HashSet::new();
924        let mut queue: Vec<String> = seeds;
925        while let Some(k) = queue.pop() {
926            if !invalid.insert(k.clone()) {
927                continue;
928            }
929            if let Some(rs) = readers.get(&k) {
930                for rd in rs {
931                    if !invalid.contains(rd) {
932                        queue.push(rd.clone());
933                    }
934                }
935            }
936        }
937        // the roots to rebind: the changed ones, and every root that read them at binding
938        let mut rebind: HashSet<String> = invalid
939            .iter()
940            .filter_map(|k| k.strip_prefix("root:").map(|s| s.to_string()))
941            .collect();
942        loop {
943            let mut grew = false;
944            for root in rebind.clone() {
945                for inst in &registry {
946                    let (p, tn) = {
947                        let b = inst.borrow();
948                        (path_str(&b.path, None), b.type_name.clone())
949                    };
950                    let Some(tn) = tn else { continue };
951                    if !under(&p, &root) {
952                        continue;
953                    }
954                    let rk = format!("referrers:{tn}");
955                    if invalid.insert(rk.clone()) {
956                        if let Some(rs) = readers.get(&rk) {
957                            for rd in rs {
958                                if !invalid.contains(rd) {
959                                    invalid.insert(rd.clone());
960                                    queue.push(rd.clone());
961                                }
962                            }
963                        }
964                    }
965                }
966            }
967            while let Some(k) = queue.pop() {
968                if let Some(rs) = readers.get(&k) {
969                    for rd in rs {
970                        if !invalid.contains(rd) {
971                            invalid.insert(rd.clone());
972                            queue.push(rd.clone());
973                        }
974                    }
975                }
976            }
977            for k in &invalid {
978                if let Some(r) = k.strip_prefix("root:") {
979                    if rebind.insert(r.to_string()) {
980                        grew = true;
981                    }
982                }
983            }
984            if !grew {
985                break;
986            }
987        }
988        // 3. forget: the diagnostics of the invalidated steps and of the rebound roots, the slots, the instances
989        let gone = |d: &Diag| {
990            d.by.as_ref().map(|b| invalid.contains(b)).unwrap_or(false)
991                || rebind.iter().any(|root| under(&d.path, root))
992        };
993        env.diag_set(
994            env.diagnostics_vec()
995                .into_iter()
996                .filter(|d| !gone(d))
997                .collect(),
998        );
999        let mut recomputed = 0usize;
1000        for k in &invalid {
1001            if k.starts_with("root:") || k.starts_with("assert:") || k.starts_with("referrers:") {
1002                continue;
1003            }
1004            if rebind.iter().any(|root| under(k, root)) {
1005                eng.slots_by_key.borrow_mut().remove(k);
1006                eng.reads.borrow_mut().remove(k);
1007                continue;
1008            }
1009            if eng.reset_slot(k) {
1010                recomputed += 1;
1011            }
1012            eng.reads.borrow_mut().remove(k);
1013        }
1014        let mut dropped: Vec<crate::engine::Inst> = vec![];
1015        env.registry_retain(|inst| {
1016            let g = rebind
1017                .iter()
1018                .any(|root| under(&path_str(&inst.borrow().path, None), root));
1019            if g {
1020                dropped.push(inst.clone());
1021            }
1022            !g
1023        });
1024        for root in &rebind {
1025            env.remove_root(root);
1026            eng.failed_inputs.borrow_mut().remove(root);
1027            eng.reads.borrow_mut().remove(&format!("root:{root}"));
1028        }
1029        eng.deferred_slots
1030            .borrow_mut()
1031            .retain(|(i, _)| !dropped.iter().any(|d| Rc::ptr_eq(d, i)));
1032        let assert_keys: Vec<String> = eng
1033            .reads
1034            .borrow()
1035            .keys()
1036            .filter(|k| k.starts_with("assert:"))
1037            .cloned()
1038            .collect();
1039        for k in assert_keys {
1040            if rebind.iter().any(|root| under(&k[7..], root)) {
1041                eng.reads.borrow_mut().remove(&k);
1042            }
1043        }
1044        // 4. rebind the roots in the fresh run's order — the documents in the
1045        //    state's order, the modules' outputs in declaration order, the
1046        //    session's outputs — then force everything: what is `ok` stays
1047        //    (an unbound input is demanded through its fallback on first read)
1048        eng.set_phase(1);
1049        for (name, d) in &st.documents {
1050            if !rebind.contains(name) {
1051                continue;
1052            }
1053            let m = r
1054                .modules
1055                .iter()
1056                .find(|x| x.env.inputs.borrow().contains_key(name))
1057                .cloned()
1058                .unwrap_or_else(|| entry.clone());
1059            let decl = m.env.inputs.borrow().get(name).cloned();
1060            let Some((ty_ast, _)) = decl else { continue };
1061            let sc = Scope::new(name, Some(m.env.clone()));
1062            match m.env.resolve(&ty_ast, None) {
1063                Ok(rt) => eng.bind_root(name, RootSrc::Doc(d.doc.clone()), &rt, &sc),
1064                Err(e) => env.report(Diag::error(e, name.clone(), None)),
1065            }
1066        }
1067        for m in &r.modules {
1068            let outs = m.env.outputs.borrow().clone();
1069            for (name, ty_ast, expr) in outs {
1070                if !rebind.contains(&name) {
1071                    continue;
1072                }
1073                let sc = Scope::new(&name, Some(m.env.clone()));
1074                match m.env.resolve(&ty_ast, None) {
1075                    Ok(rt) => eng.bind_root(&name, RootSrc::Expr(&expr), &rt, &sc),
1076                    Err(e) => env.report(Diag::error(e, name.clone(), None)),
1077                }
1078            }
1079        }
1080        for (name, expr, rt) in &r.session_roots {
1081            if !rebind.contains(name) {
1082                continue;
1083            }
1084            let sc = Scope::new(name, Some(entry.env.clone()));
1085            eng.bind_root(name, RootSrc::Expr(expr), rt, &sc);
1086        }
1087        eng.force_roots(&env);
1088        eng.set_phase(2);
1089        let mut i = 0;
1090        loop {
1091            let item = {
1092                let d = eng.deferred_slots.borrow();
1093                if i >= d.len() {
1094                    break;
1095                }
1096                d[i].clone()
1097            };
1098            eng.force_slot_safe(&item.0, &item.1);
1099            i += 1;
1100        }
1101        eng.bind_deferred_roots();
1102        eng.force_roots(&env);
1103        // 5. the asserts of the instances that are new or whose asserts read what changed
1104        for inst in env.registry_snapshot() {
1105            let key = format!("assert:{}", path_str(&inst.borrow().path, None));
1106            let fresh = !eng.reads.borrow().contains_key(&key);
1107            if fresh || invalid.contains(&key) {
1108                eng.validate_inst(&inst, "");
1109            }
1110        }
1111        let sorted = sort_diags(env.diagnostics_vec());
1112        env.diag_set(sorted.clone());
1113        let elapsed = ms(t0);
1114        let timing = Timing {
1115            load: 0.0,
1116            check: 0.0,
1117            bind: 0.0,
1118            evaluate: elapsed,
1119            total: elapsed,
1120            recomputed: Some(recomputed),
1121            slots: Some(eng.slots_by_key.borrow().len()),
1122        };
1123        let run = Run {
1124            diags: sorted,
1125            timing,
1126            ..r.clone()
1127        };
1128        *self.last.borrow_mut() = Some(Last {
1129            key: last.key.clone(),
1130            docs,
1131            run: run.clone(),
1132        });
1133        self.last_timing.set(Some(timing));
1134        Some(run)
1135    }
1136    fn run_fresh(&self, st: &State, mode: Mode) -> Run {
1137        let t0 = Instant::now();
1138        let (modules, entry, load_diags) = self.build(st);
1139        let load = ms(t0);
1140        let mut out = Run {
1141            modules,
1142            entry,
1143            load_diags,
1144            checks: vec![],
1145            session_checks: vec![],
1146            session_roots: vec![],
1147            eng: None,
1148            diags: vec![],
1149            timing: Timing {
1150                load,
1151                check: 0.0,
1152                bind: 0.0,
1153                evaluate: 0.0,
1154                total: 0.0,
1155                recomputed: None,
1156                slots: None,
1157            },
1158        };
1159        let finish = |mut out: Run| -> Run {
1160            out.timing.total = ms(t0);
1161            self.last_timing.set(Some(out.timing));
1162            out
1163        };
1164        if !out.load_diags.is_empty() || out.entry.is_none() {
1165            return finish(out);
1166        }
1167        let entry = out.entry.clone().unwrap();
1168        let t1 = Instant::now();
1169        for m in &out.modules {
1170            for d in check_module(&m.decls, Some(m.env.clone()), None) {
1171                out.checks.push((m.path.display().to_string(), d));
1172            }
1173        }
1174        // session outputs: their expressions are inferred where a declared
1175        // output's would be checked; the inferred type is the root's type
1176        let mut session_roots: Vec<(String, Rc<Expr>, RT)> = vec![];
1177        for (name, ty_text, expr_text) in &st.outputs {
1178            let taken = out.modules.iter().any(|m| {
1179                m.env.inputs.borrow().contains_key(name)
1180                    || m.env.outputs.borrow().iter().any(|(o, _, _)| o == name)
1181            });
1182            if taken {
1183                out.session_checks.push(Diag {
1184                    severity: "error".into(),
1185                    id: None,
1186                    code: Some("E3018".into()),
1187                    message: format!("root {name} is already declared by the universe"),
1188                    path: name.clone(),
1189                    loc: None,
1190                    by: None,
1191                });
1192                continue;
1193            }
1194            let expr = match parse_expr(expr_text) {
1195                Ok(e) => e,
1196                Err(e) => {
1197                    out.session_checks.push(Diag {
1198                        severity: "error".into(),
1199                        id: None,
1200                        code: None,
1201                        message: e.0,
1202                        path: name.clone(),
1203                        loc: None,
1204                        by: None,
1205                    });
1206                    continue;
1207                }
1208            };
1209            let sink: Rc<RefCell<Vec<Diag>>> = Rc::new(RefCell::new(vec![]));
1210            let sink2 = sink.clone();
1211            let n2 = name.clone();
1212            let cx = self.session_ctx(
1213                st,
1214                &entry.env,
1215                Rc::new(move |code, msg| {
1216                    sink2.borrow_mut().push(Diag {
1217                        severity: "error".into(),
1218                        id: None,
1219                        code: Some(code.to_string()),
1220                        message: msg,
1221                        path: n2.clone(),
1222                        loc: None,
1223                        by: None,
1224                    })
1225                }),
1226                Some(name),
1227            );
1228            let ty = infer(&cx, &expr);
1229            let found: Vec<Diag> = sink.borrow().clone();
1230            if !found.is_empty() {
1231                out.session_checks.extend(found);
1232                continue;
1233            }
1234            let rt: RT = match ty_text {
1235                Some(t) => {
1236                    let resolved =
1237                        parse_decl(&format!("output {name}: {t} = 0")).and_then(|(d, _)| match &d
1238                            .body
1239                        {
1240                            DeclBody::Output { ty, .. } => {
1241                                entry.env.resolve(ty, None).map_err(SessionError::new)
1242                            }
1243                            _ => Err(SessionError::new("not an output")),
1244                        });
1245                    match resolved {
1246                        Ok(rt) => rt,
1247                        Err(e) => {
1248                            out.session_checks.push(Diag {
1249                                severity: "error".into(),
1250                                id: None,
1251                                code: None,
1252                                message: e.0,
1253                                path: name.clone(),
1254                                loc: None,
1255                                by: None,
1256                            });
1257                            continue;
1258                        }
1259                    }
1260                }
1261                None => ty.rt.unwrap_or_else(|| crate::semantics::ty(RTk::Any)),
1262            };
1263            session_roots.push((name.clone(), expr, rt));
1264        }
1265        out.timing.check = ms(t1);
1266        // a static error in a module stops full evaluation as it stops `decl
1267        // evaluate`; a session output that does not check is left out, and a
1268        // bare expression (lazy) evaluates over what loaded regardless
1269        if mode == Mode::Check
1270            || (mode == Mode::Full && out.checks.iter().any(|(_, d)| d.severity == "error"))
1271        {
1272            return finish(out);
1273        }
1274
1275        let t2 = Instant::now();
1276        let eng = Engine::new(entry.env.clone());
1277        for m in &out.modules {
1278            eng.install_hooks(&m.env, true);
1279        }
1280        // documents first (an output may read an input, §5.5), then the
1281        // modules' outputs, then the session's
1282        for (name, d) in &st.documents {
1283            let m = out
1284                .modules
1285                .iter()
1286                .find(|x| x.env.inputs.borrow().contains_key(name))
1287                .cloned()
1288                .unwrap_or_else(|| entry.clone());
1289            let decl = m.env.inputs.borrow().get(name).cloned();
1290            let Some((ty_ast, _)) = decl else { continue };
1291            let sc = Scope::new(name, Some(m.env.clone()));
1292            match m.env.resolve(&ty_ast, None) {
1293                Ok(rt) => eng.bind_root(name, RootSrc::Doc(d.doc.clone()), &rt, &sc),
1294                Err(e) => entry.env.report(Diag::error(e, name.clone(), None)),
1295            }
1296        }
1297        for m in &out.modules {
1298            let outs = m.env.outputs.borrow().clone();
1299            for (name, ty_ast, expr) in outs {
1300                let sc = Scope::new(&name, Some(m.env.clone()));
1301                match m.env.resolve(&ty_ast, None) {
1302                    Ok(rt) => eng.bind_root(&name, RootSrc::Expr(&expr), &rt, &sc),
1303                    Err(e) => entry.env.report(Diag::error(e, name.clone(), None)),
1304                }
1305            }
1306        }
1307        for (name, expr, rt) in &session_roots {
1308            let sc = Scope::new(name, Some(entry.env.clone()));
1309            eng.bind_root(name, RootSrc::Expr(expr), rt, &sc);
1310        }
1311        out.session_roots = session_roots;
1312        out.eng = Some(eng.clone());
1313        out.timing.bind = ms(t2);
1314        if mode == Mode::Lazy {
1315            eng.set_phase(2);
1316            out.diags = entry.env.diagnostics_vec();
1317            return finish(out);
1318        }
1319        let t3 = Instant::now();
1320        eng.drive(&entry.env);
1321        out.diags = entry.env.diagnostics_vec(); // sorted by drive's caller? no: sorted here (§6.7)
1322        let sorted = sort_diags(out.diags.clone());
1323        entry.env.diag_set(sorted.clone());
1324        out.diags = sorted;
1325        out.timing.evaluate = ms(t3);
1326        finish(out)
1327    }
1328
1329    // ---- questions ----
1330    // the engine an expression evaluates over: the last full run's when the
1331    // universe evaluates (complete, so `$referrers` answers over every
1332    // instance, and nothing is rebuilt), else a lazy run's (bound, unforced)
1333    fn engine_for(&self, st: &State) -> Run {
1334        let full = self.run_state(st, Mode::Full);
1335        if full.eng.is_some() {
1336            full
1337        } else {
1338            self.run_state(st, Mode::Lazy)
1339        }
1340    }
1341    // evaluate `f` over the run's engine and leave the run as it was: the
1342    // diagnostics the expression added and the instances it materialized
1343    // under `_` are removed, forced slots keep the values a full run gives
1344    fn scratch<T>(&self, r: &Run, f: impl FnOnce(&Rc<Engine>, &Rc<Env>) -> T) -> T {
1345        let (eng, env) = (r.eng.clone().unwrap(), r.entry.clone().unwrap().env.clone());
1346        let n = env.diag_len();
1347        let reg = env.registry_snapshot().len();
1348        let roots: HashSet<String> = env.root_names().into_iter().collect();
1349        let out = f(&eng, &env);
1350        // an input demanded through its fallback by the expression alone is not a root of the run
1351        let demanded: Vec<String> = env
1352            .root_names()
1353            .into_iter()
1354            .filter(|k| !roots.contains(k))
1355            .collect();
1356        let under_demanded = |p: &str| demanded.iter().any(|k| under(p, k));
1357        for k in &demanded {
1358            env.remove_root(k);
1359            eng.failed_inputs.borrow_mut().remove(k);
1360            eng.reads.borrow_mut().remove(&format!("root:{k}"));
1361        }
1362        let keys: Vec<String> = eng.reads.borrow().keys().cloned().collect();
1363        for k in keys {
1364            if under_demanded(k.strip_prefix("assert:").unwrap_or(&k)) {
1365                eng.reads.borrow_mut().remove(&k);
1366            }
1367        }
1368        let skeys: Vec<String> = eng.slots_by_key.borrow().keys().cloned().collect();
1369        for k in skeys {
1370            if under_demanded(&k) {
1371                eng.slots_by_key.borrow_mut().remove(&k);
1372            }
1373        }
1374        env.diag_truncate(n);
1375        let mut i = 0usize;
1376        env.registry_retain(|inst| {
1377            let idx = i;
1378            i += 1;
1379            let p = path_str(&inst.borrow().path, None);
1380            let scratch_root = matches!(inst.borrow().path.first(), Some(Seg::Name(n)) if n == "_");
1381            if idx < reg {
1382                !under_demanded(&p)
1383            } else {
1384                !scratch_root && !under_demanded(&p)
1385            }
1386        });
1387        eng.computing.borrow_mut().clear();
1388        out
1389    }
1390
1391    // ---- questions ----
1392    /// partial evaluation of one expression (§2.1)
1393    pub fn evaluate_expr(&self, text: &str) -> SResult<ExprResult> {
1394        let expr = parse_expr(text)?;
1395        let r = self.engine_for(&self.state);
1396        if r.eng.is_none() || r.entry.is_none() {
1397            return Ok(ExprResult {
1398                value: None,
1399                diags: r.load_diags.clone(),
1400                error: Some((None, String::new())),
1401            });
1402        }
1403        let entry_env = r.entry.as_ref().unwrap().env.clone();
1404        let sc = Scope::new("", Some(entry_env.clone()));
1405        let named = identifiers(text);
1406        Ok(self.scratch(&r, |eng, env| {
1407            // the run may already have reported (a root whose binding failed); the
1408            // expression's own diagnostics are the ones that arise from here on,
1409            // plus the diagnostics of the roots it names
1410            let from = env.diag_len();
1411            let arising = |env: &Env| -> Vec<Diag> {
1412                let all = env.diagnostics_vec();
1413                let mut out: Vec<Diag> = all[..from.min(all.len())]
1414                    .iter()
1415                    .filter(|d| named.contains(&d.path))
1416                    .cloned()
1417                    .collect();
1418                out.extend(all[from.min(all.len())..].iter().cloned());
1419                sort_diags(out)
1420            };
1421            let result = (|| -> Result<Value, Fail> {
1422                let v = eng.ev(&expr, &sc)?;
1423                let v = eng.materialize(v, &[Seg::Name("_".into())])?;
1424                eng.force_all(&v);
1425                Ok(v)
1426            })();
1427            match result {
1428                Ok(v) => ExprResult {
1429                    value: Some(self.value_text(eng, &v)),
1430                    diags: arising(env),
1431                    error: None,
1432                },
1433                Err(Fail::Eval(e)) => ExprResult {
1434                    value: None,
1435                    diags: arising(env),
1436                    error: Some((e.code, e.msg)),
1437                },
1438                Err(_) => ExprResult {
1439                    value: None,
1440                    diags: arising(env),
1441                    error: Some((None, String::new())),
1442                },
1443            }
1444        }))
1445    }
1446    fn value_text(&self, eng: &Engine, v: &Value) -> String {
1447        match v {
1448            Value::Absent | Value::Undef => "absent".into(),
1449            Value::Clo(_) | Value::Nat(_) | Value::Std(_) => "<function>".into(),
1450            Value::NsRef(_) => "<namespace>".into(),
1451            Value::Pat(re) => format!("/{re}/"),
1452            _ => eng.serialize(v, "", false),
1453        }
1454    }
1455
1456    /// the roots of the universe and of the session (`:roots`)
1457    pub fn roots(&self) -> Vec<RootInfo> {
1458        let (modules, _, _) = self.build(&self.state);
1459        let entry_abs = self.entry_abs();
1460        let entry_dir = entry_abs
1461            .parent()
1462            .map(|p| p.to_path_buf())
1463            .unwrap_or_default();
1464        let rel = |p: &Path| {
1465            if p == entry_abs {
1466                self.entry_name()
1467            } else {
1468                relative_path(&entry_dir, p)
1469            }
1470        };
1471        let mut out = vec![];
1472        for m in &modules {
1473            // the module's roots in declaration order, from its text as loaded
1474            // (a detached output is blanked from the universe but still a root)
1475            let parsed;
1476            let decls: &Vec<Decl> = if m.path == entry_abs {
1477                parsed = parse_source(
1478                    self.state
1479                        .snapshot
1480                        .get(&m.path)
1481                        .map(|s| s.as_str())
1482                        .unwrap_or(""),
1483                )
1484                .decls;
1485                &parsed
1486            } else {
1487                &m.decls
1488            };
1489            for decl in decls {
1490                match &decl.body {
1491                    DeclBody::Output { name, .. } => {
1492                        let d = self.state.document(name);
1493                        out.push(RootInfo {
1494                            kind: "output",
1495                            name: name.clone(),
1496                            module: rel(&m.path),
1497                            exported: decl.exported,
1498                            session: false,
1499                            binding: if d.map(|d| d.origin) == Some(Origin::Detached) {
1500                                "detached".into()
1501                            } else {
1502                                String::new()
1503                            },
1504                            detail: String::new(),
1505                            edited: d.map(|d| d.edited).unwrap_or(false),
1506                        });
1507                    }
1508                    DeclBody::Input { name, fallback, .. } => {
1509                        let d = self.state.document(name);
1510                        let binding = match d {
1511                            Some(d) if d.origin == Origin::Fallback => "fallback",
1512                            Some(_) => "bound",
1513                            None => {
1514                                if fallback.is_some() {
1515                                    "fallback"
1516                                } else {
1517                                    "unbound"
1518                                }
1519                            }
1520                        };
1521                        let detail = match d {
1522                            Some(d) => match d.origin {
1523                                Origin::File => d.file.clone().unwrap_or_default(),
1524                                Origin::Inline => "(inline)".into(),
1525                                Origin::Expr => "(expression)".into(),
1526                                _ => String::new(),
1527                            },
1528                            None => String::new(),
1529                        };
1530                        out.push(RootInfo {
1531                            kind: "input",
1532                            name: name.clone(),
1533                            module: rel(&m.path),
1534                            exported: false,
1535                            session: false,
1536                            binding: binding.into(),
1537                            detail,
1538                            edited: d.map(|d| d.edited).unwrap_or(false),
1539                        });
1540                    }
1541                    _ => {}
1542                }
1543            }
1544        }
1545        for (name, _, _) in &self.state.outputs {
1546            out.push(RootInfo {
1547                kind: "output",
1548                name: name.clone(),
1549                module: String::new(),
1550                exported: false,
1551                session: true,
1552                binding: String::new(),
1553                detail: String::new(),
1554                edited: false,
1555            });
1556        }
1557        out
1558    }
1559    pub fn all_root_names(&self) -> Vec<String> {
1560        self.roots().into_iter().map(|r| r.name).collect()
1561    }
1562    pub fn has_root(&self, name: &str) -> bool {
1563        self.all_root_names().iter().any(|n| n == name)
1564    }
1565
1566    /// static diagnostics of every module, with the file each is reported against
1567    pub fn check(&self) -> Vec<(String, Diag)> {
1568        let r = self.run(Mode::Check);
1569        let entry = self.entry_abs().display().to_string();
1570        let mut out: Vec<(String, Diag)> = r
1571            .load_diags
1572            .iter()
1573            .map(|d| (entry.clone(), d.clone()))
1574            .collect();
1575        out.extend(r.checks);
1576        out.extend(r.session_checks.into_iter().map(|d| (entry.clone(), d)));
1577        out
1578    }
1579
1580    /// full evaluation of the named roots (`:evaluate`), or of the exported outputs
1581    pub fn evaluate(
1582        &self,
1583        names: &[String],
1584    ) -> SResult<(Run, Vec<(String, Option<String>)>, bool)> {
1585        let r = self.run(Mode::Full);
1586        let exported = names.is_empty();
1587        let Some(entry) = r.entry.clone() else {
1588            return Ok((r, vec![], exported));
1589        };
1590        let want: Vec<String> = if names.is_empty() {
1591            entry
1592                .decls
1593                .iter()
1594                .filter(|d| d.exported)
1595                .filter_map(|d| match &d.body {
1596                    DeclBody::Output { name, .. } => Some(name.clone()),
1597                    _ => None,
1598                })
1599                .collect()
1600        } else {
1601            names.to_vec()
1602        };
1603        for n in names {
1604            if !self.has_root(n) {
1605                return Err(SessionError::new(format!("no root named {n}")));
1606            }
1607        }
1608        let Some(eng) = r.eng.clone() else {
1609            return Ok((r, want.into_iter().map(|n| (n, None)).collect(), exported));
1610        };
1611        let mut docs = vec![];
1612        for name in want {
1613            let v = entry.env.root(&name);
1614            let bad = v.is_none()
1615                || r.diags
1616                    .iter()
1617                    .any(|d| d.severity == "error" && is_root_diag(d, &name));
1618            let json = if bad {
1619                None
1620            } else {
1621                Some(eng.serialize(&v.unwrap(), &name, false))
1622            };
1623            docs.push((name, json));
1624        }
1625        Ok((r, docs, exported))
1626    }
1627
1628    /// whole-document validation of the named roots (`:validate`), or of every root
1629    pub fn validate(
1630        &self,
1631        names: &[String],
1632    ) -> SResult<(Run, Vec<(String, usize, usize)>, Vec<Diag>)> {
1633        for n in names {
1634            if !self.has_root(n) {
1635                return Err(SessionError::new(format!("no root named {n}")));
1636            }
1637        }
1638        let r = self.run(Mode::Full);
1639        let want: Vec<String> = if !names.is_empty() {
1640            names.to_vec()
1641        } else if let Some(entry) = &r.entry {
1642            entry
1643                .env
1644                .roots
1645                .borrow()
1646                .borrow()
1647                .iter()
1648                .map(|(n, _)| n.clone())
1649                .collect()
1650        } else {
1651            vec![]
1652        };
1653        let diags: Vec<Diag> = r
1654            .diags
1655            .iter()
1656            .filter(|d| {
1657                want.iter().any(|n| is_root_diag(d, n)) || (d.path.is_empty() && names.is_empty())
1658            })
1659            .cloned()
1660            .collect();
1661        let verdicts = want
1662            .iter()
1663            .map(|name| {
1664                let errors = r
1665                    .diags
1666                    .iter()
1667                    .filter(|d| d.severity == "error" && is_root_diag(d, name))
1668                    .count()
1669                    + if r
1670                        .entry
1671                        .as_ref()
1672                        .map(|e| e.env.root(name).is_some())
1673                        .unwrap_or(false)
1674                    {
1675                        0
1676                    } else if r.eng.is_some() {
1677                        1
1678                    } else {
1679                        0
1680                    };
1681                let warnings = r
1682                    .diags
1683                    .iter()
1684                    .filter(|d| d.severity == "warning" && is_root_diag(d, name))
1685                    .count();
1686                (name.clone(), errors, warnings)
1687            })
1688            .collect();
1689        Ok((r, verdicts, diags))
1690    }
1691
1692    /// the static type of an expression (`:type`)
1693    pub fn type_of(&self, text: &str) -> SResult<(String, bool, Vec<Diag>)> {
1694        let expr = parse_expr(text)?;
1695        let (_, entry, diags) = self.build(&self.state);
1696        let Some(entry) = entry else {
1697            return Err(SessionError::new(
1698                diags
1699                    .first()
1700                    .map(|d| d.message.clone())
1701                    .unwrap_or_else(|| "the universe did not load".into()),
1702            ));
1703        };
1704        let sink: Rc<RefCell<Vec<Diag>>> = Rc::new(RefCell::new(vec![]));
1705        let sink2 = sink.clone();
1706        let cx = self.session_ctx(
1707            &self.state,
1708            &entry.env,
1709            Rc::new(move |code, message| {
1710                sink2.borrow_mut().push(Diag {
1711                    severity: "error".into(),
1712                    id: None,
1713                    code: Some(code.to_string()),
1714                    message,
1715                    path: String::new(),
1716                    loc: None,
1717                    by: None,
1718                })
1719            }),
1720            None,
1721        );
1722        let ty = infer(&cx, &expr);
1723        let found = sink.borrow().clone();
1724        Ok((type_text(ty.rt.as_ref()), ty.abs, found))
1725    }
1726
1727    /// the canonical path of the place a navigation names (`:path`)
1728    pub fn path_of(&self, text: &str) -> SResult<String> {
1729        let expr = parse_expr(text)?;
1730        let r = self.engine_for(&self.state);
1731        if r.eng.is_none() || r.entry.is_none() {
1732            return Err(SessionError::new(self.load_failure(&r)));
1733        }
1734        let entry = r.entry.clone().unwrap();
1735        let sc = Scope::new("", Some(entry.env.clone()));
1736        self.scratch(&r, |eng, _| {
1737            let result = (|| -> Result<Option<SegPath>, Fail> {
1738                let mut segs = eng.eval_place(&expr, &sc)?;
1739                // a scalar member or element is a place too: its container's place, one step down
1740                if segs.is_none() {
1741                    match &*expr {
1742                        Expr::Member { x, name, .. } => {
1743                            if let Some(mut base) = eng.eval_place(x, &sc)? {
1744                                base.push(Seg::Name(name.clone()));
1745                                segs = Some(base);
1746                            }
1747                        }
1748                        Expr::Index { x, i } => {
1749                            if let Some(mut base) = eng.eval_place(x, &sc)? {
1750                                let iv = eng.ev(i, &sc)?;
1751                                base.push(match iv {
1752                                    Value::Int(n) => Seg::Idx(n.to_string().parse().unwrap_or(0)),
1753                                    Value::Str(s) => Seg::Key(s),
1754                                    other => Seg::Key(crate::infer::js_str(&other)),
1755                                });
1756                                segs = Some(base);
1757                            }
1758                        }
1759                        Expr::Name(n) if entry.env.root(n).is_some() => {
1760                            segs = Some(vec![Seg::Name(n.clone())])
1761                        }
1762                        _ => {}
1763                    }
1764                }
1765                Ok(segs)
1766            })();
1767            match result {
1768                Ok(Some(segs)) => Ok(path_str(&segs, None)),
1769                Ok(None) => Err(SessionError::new("the expression does not name a place")),
1770                Err(Fail::Eval(e)) => Err(SessionError::new(e.msg)),
1771                Err(_) => Err(SessionError::new("the place is invalid")),
1772            }
1773        })
1774    }
1775
1776    /// the declaration a name resolves to, with its documentation (`:doc`)
1777    pub fn doc_of(&self, name: &str) -> SResult<Vec<String>> {
1778        let mut parts = name.splitn(2, '.');
1779        let head = parts.next().unwrap_or("");
1780        let member = parts.next();
1781        // a session declaration first
1782        if member.is_none() {
1783            if let Some(t) = self.state.decl(head) {
1784                return Ok(t.split('\n').map(|s| s.to_string()).collect());
1785            }
1786            if let Some((ty, expr)) = self.state.output(head) {
1787                return Ok(vec![format!(
1788                    "{head}{} = {expr}",
1789                    ty.as_ref().map(|t| format!(": {t}")).unwrap_or_default()
1790                )]);
1791            }
1792        }
1793        let (modules, entry, diags) = self.build(&self.state);
1794        let Some(entry) = entry else {
1795            return Err(SessionError::new(
1796                diags
1797                    .first()
1798                    .map(|d| d.message.clone())
1799                    .unwrap_or_else(|| "the universe did not load".into()),
1800            ));
1801        };
1802        let mut module: Option<Rc<Module>> = Some(entry.clone());
1803        let mut target = head.to_string();
1804        if !entry.decls.iter().any(|d| d.name() == Some(head)) {
1805            let im = entry.env.imports.borrow().get(head).cloned();
1806            match im {
1807                Some(im) => {
1808                    module = modules
1809                        .iter()
1810                        .find(|m| Rc::ptr_eq(&m.env, &im.env))
1811                        .cloned();
1812                    target = im.name.clone();
1813                }
1814                None => module = None,
1815            }
1816        }
1817        let decl = module.as_ref().and_then(|m| {
1818            m.decls
1819                .iter()
1820                .find(|d| d.name() == Some(target.as_str()) && d.loc.is_some())
1821                .cloned()
1822        });
1823        let (Some(module), Some(decl)) = (module, decl) else {
1824            return Err(SessionError::new(format!("no declaration named {head}")));
1825        };
1826        let text = self
1827            .state
1828            .snapshot
1829            .get(&module.path)
1830            .cloned()
1831            .unwrap_or_default();
1832        let lines: Vec<&str> = text.split('\n').collect();
1833        let loc = decl.loc.unwrap();
1834        let mut from = loc.sl;
1835        let mut doc_lines: Vec<String> = vec![];
1836        let is_doc = |l: &str| l.trim_start().starts_with("///");
1837        while from > 0 && is_doc(lines[from - 1]) {
1838            from -= 1;
1839            doc_lines.insert(0, lines[from].to_string());
1840        }
1841        let body: Vec<&str> = lines[loc.sl..=loc.el.min(lines.len() - 1)].to_vec();
1842        if let Some(member) = member {
1843            let re = Regex::new(&format!(r"^\s*{}\$?\??\s*[:=]", regex::escape(member))).unwrap();
1844            let mut picked: Vec<String> = vec![];
1845            for (i, l) in body.iter().enumerate() {
1846                if re.is_match(l) {
1847                    let mut j = i;
1848                    let mut ds: Vec<String> = vec![];
1849                    while j > 0 && is_doc(body[j - 1]) {
1850                        j -= 1;
1851                        ds.insert(0, body[j].trim().to_string());
1852                    }
1853                    picked.extend(ds);
1854                    picked.push(l.trim().to_string());
1855                }
1856            }
1857            if picked.is_empty() {
1858                return Err(SessionError::new(format!("{head} has no member {member}")));
1859            }
1860            return Ok(picked);
1861        }
1862        doc_lines.extend(body.iter().map(|s| s.to_string()));
1863        Ok(doc_lines)
1864    }
1865
1866    /// the derivation of a valid place, or the root cause of an invalid one (`:trace`)
1867    pub fn trace(&self, path_text: &str) -> SResult<Vec<String>> {
1868        let segs: SegPath = parse_path(path_text, "")
1869            .map_err(|_| SessionError::new(format!("bad path {path_text}")))?;
1870        let root = seg_text(&segs[0]);
1871        if !self.has_root(&root) {
1872            return Err(SessionError::new(format!("no root named {root}")));
1873        }
1874        let r = self.run(Mode::Full);
1875        let (Some(eng), Some(entry)) = (r.eng.clone(), r.entry.clone()) else {
1876            return Err(SessionError::new(self.load_failure(&r)));
1877        };
1878        let mut lines: Vec<String> = vec![];
1879        let mut seen: HashSet<String> = HashSet::new();
1880        let has_doc = self.state.document(&root).is_some();
1881        self.walk(&mut lines, &mut seen, &r, &eng, &entry, &segs, 0, has_doc);
1882        Ok(lines)
1883    }
1884    #[allow(clippy::too_many_arguments)]
1885    fn walk(
1886        &self,
1887        lines: &mut Vec<String>,
1888        seen: &mut HashSet<String>,
1889        r: &Run,
1890        eng: &Rc<Engine>,
1891        entry: &Rc<Module>,
1892        segs: &[Seg],
1893        depth: usize,
1894        has_doc: bool,
1895    ) {
1896        let short = |v: &Value| {
1897            let t = self.value_text(eng, v);
1898            if t.chars().count() > 60 {
1899                format!("{}...", t.chars().take(57).collect::<String>())
1900            } else {
1901                t
1902            }
1903        };
1904        let path = path_str(segs, None);
1905        let ind = "  ".repeat(depth);
1906        if seen.contains(&path) {
1907            lines.push(format!("{ind}{path}  (above)"));
1908            return;
1909        }
1910        seen.insert(path.clone());
1911        let own: Vec<&Diag> = r.diags.iter().filter(|d| d.path == path).collect();
1912        let parent = if segs.len() > 1 {
1913            self.value_at(eng, entry, &segs[..segs.len() - 1])
1914        } else {
1915            None
1916        };
1917        let last = segs.last().unwrap();
1918        let slot_info = match (&parent, last) {
1919            (Some(Value::Rec(inst)), Seg::Name(n)) => {
1920                let b = inst.borrow();
1921                b.slot(n).map(|s| {
1922                    (
1923                        s.kind,
1924                        s.state,
1925                        s.value.clone(),
1926                        b.entry_order.contains(n),
1927                        rec_members(&b.rt).into_iter().find(|m| &m.name == n),
1928                    )
1929                })
1930            }
1931            _ => None,
1932        };
1933        if let Some((kind, state, value, in_entry, m)) = slot_info {
1934            let inst = match &parent {
1935                Some(Value::Rec(i)) => i.clone(),
1936                _ => unreachable!(),
1937            };
1938            let kind_word = match kind {
1939                MKind::Der => "derived",
1940                MKind::Dflt => "defaulted",
1941                MKind::Opt => "optional",
1942                MKind::Req => "required",
1943            };
1944            let supplied =
1945                matches!(kind, MKind::Req | MKind::Opt) || (kind == MKind::Dflt && in_entry);
1946            let m_expr = m.as_ref().and_then(|m| m.expr.clone());
1947            if state == SlotState::Invalid {
1948                lines.push(format!("{ind}{path}  (invalid)"));
1949                for d in &own {
1950                    lines.push(format!("{ind}  {}", fmt_diag(d, None)));
1951                }
1952                if own.is_empty() {
1953                    if let Some(e) = &m_expr {
1954                        for rd in reads_of(e) {
1955                            if let Some(s) = self.read_segs(eng, &inst, &rd, entry) {
1956                                self.walk(lines, seen, r, eng, entry, &s, depth + 1, has_doc);
1957                            }
1958                        }
1959                    }
1960                }
1961                return;
1962            }
1963            if state == SlotState::Absent {
1964                lines.push(format!("{ind}{path}  absent"));
1965                return;
1966            }
1967            let how = if supplied {
1968                "supplied".to_string()
1969            } else {
1970                kind_word.to_string()
1971            };
1972            let ex = match (&m_expr, supplied) {
1973                (Some(e), false) => format!(": {}", expr_text(e)),
1974                _ => String::new(),
1975            };
1976            lines.push(format!("{ind}{path} = {}  ({how}{ex})", short(&value)));
1977            if !supplied && depth < 6 {
1978                if let Some(e) = &m_expr {
1979                    for rd in reads_of(e) {
1980                        match self.read_segs(eng, &inst, &rd, entry) {
1981                            Some(s) => {
1982                                self.walk(lines, seen, r, eng, entry, &s, depth + 1, has_doc)
1983                            }
1984                            None => lines.push(format!("{ind}  {}  (not a place)", expr_text(&rd))),
1985                        }
1986                    }
1987                }
1988            }
1989            return;
1990        }
1991        match self.value_at(eng, entry, segs) {
1992            None => {
1993                if r.diags
1994                    .iter()
1995                    .any(|d| d.severity == "error" && is_root_diag(d, &path))
1996                {
1997                    lines.push(format!("{ind}{path}  (invalid)"));
1998                    for d in r.diags.iter().filter(|d| is_root_diag(d, &path)) {
1999                        lines.push(format!("{ind}  {}", fmt_diag(d, None)));
2000                    }
2001                } else {
2002                    lines.push(format!("{ind}{path}  nothing there"));
2003                }
2004            }
2005            Some(v) => {
2006                let how = if segs.len() == 1 {
2007                    if has_doc {
2008                        "document"
2009                    } else {
2010                        "root literal"
2011                    }
2012                } else {
2013                    "supplied"
2014                };
2015                lines.push(format!("{ind}{path} = {}  ({how})", short(&v)));
2016                for d in &own {
2017                    lines.push(format!("{ind}  {}", fmt_diag(d, None)));
2018                }
2019            }
2020        }
2021    }
2022    fn value_at(&self, eng: &Engine, entry: &Module, segs: &[Seg]) -> Option<Value> {
2023        let mut v = entry.env.root(&seg_text(&segs[0]))?;
2024        for s in &segs[1..] {
2025            v = eng.deref(v).ok()?;
2026            v = match (&v, s) {
2027                (Value::Rec(inst), Seg::Name(n)) => {
2028                    let st = eng.force_state(inst, n);
2029                    if st == SlotState::Ok {
2030                        inst.borrow().slot(n).map(|s| s.value.clone())?
2031                    } else {
2032                        return None;
2033                    }
2034                }
2035                (Value::Rec(_), _) => return None,
2036                (Value::Arr(a), Seg::Idx(i)) => a.borrow().items.get(*i).cloned()?,
2037                (Value::Arr(_), _) => return None,
2038                (Value::Map(m), _) => m.borrow().get(&seg_text(s)).cloned()?,
2039                _ => return None,
2040            };
2041            if v.is_undef() || v.is_absent() {
2042                return None;
2043            }
2044        }
2045        Some(v)
2046    }
2047    fn read_segs(
2048        &self,
2049        eng: &Engine,
2050        inst: &Rc<RefCell<crate::semantics::RecInst>>,
2051        rd: &Rc<Expr>,
2052        entry: &Module,
2053    ) -> Option<SegPath> {
2054        // a bare name read inside a record is a sibling member (§4.4's scope
2055        // chain), else a root; a chain is navigated to the place it names
2056        if let Expr::Name(n) = &**rd {
2057            let mut cur = Some(inst.clone());
2058            while let Some(c) = cur {
2059                if c.borrow().has_slot(n) {
2060                    let mut p = c.borrow().path.clone();
2061                    p.push(Seg::Name(n.clone()));
2062                    return Some(p);
2063                }
2064                cur = c.borrow().parent.clone();
2065            }
2066            return if entry.env.root(n).is_some() {
2067                Some(vec![Seg::Name(n.clone())])
2068            } else {
2069                None
2070            };
2071        }
2072        let root_name = inst.borrow().path.first().map(seg_text).unwrap_or_default();
2073        let sc = Scope {
2074            inst: Some(inst.clone()),
2075            locals: Rc::new(HashMap::new()),
2076            root_name,
2077            menv: Some(entry.env.clone()),
2078        };
2079        eng.eval_place(rd, &sc).ok().flatten()
2080    }
2081
2082    /// the candidates completion offers at the end of `text` (`:complete`)
2083    pub fn complete(&self, text: &str, commands: &[&str]) -> Vec<String> {
2084        let uniq = |xs: Vec<String>| -> Vec<String> {
2085            let mut v: Vec<String> = xs.into_iter().collect::<HashSet<_>>().into_iter().collect();
2086            v.sort();
2087            v
2088        };
2089        if text.starts_with(':') {
2090            let Some(sp) = text.find(' ') else {
2091                return uniq(
2092                    commands
2093                        .iter()
2094                        .filter(|c| c.starts_with(text))
2095                        .map(|c| c.to_string())
2096                        .collect(),
2097                );
2098            };
2099            let cmd = &text[..sp];
2100            let rest = &text[sp + 1..];
2101            let last = Regex::new(r"[\s,=]+")
2102                .unwrap()
2103                .split(rest)
2104                .last()
2105                .unwrap_or("")
2106                .to_string();
2107            let by =
2108                |xs: Vec<String>| uniq(xs.into_iter().filter(|x| x.starts_with(&last)).collect());
2109            return match cmd {
2110                ":evaluate" | ":validate" | ":unbind" | ":diff" | ":save" | ":bind" => {
2111                    by(self.all_root_names())
2112                }
2113                ":drop" => by(self
2114                    .state
2115                    .decls
2116                    .iter()
2117                    .map(|(n, _)| n.clone())
2118                    .chain(self.state.outputs.iter().map(|(n, _, _)| n.clone()))
2119                    .collect()),
2120                ":set" => by(vec!["pretty".into(), "compact".into()]),
2121                ":help" => by(commands.iter().map(|c| c.to_string()).collect()),
2122                ":trace" | ":path" | ":create" | ":update" | ":remove" => self.complete_path(&last),
2123                _ => vec![],
2124            };
2125        }
2126        let member_re = Regex::new(
2127            r"([A-Za-z_$][A-Za-z0-9_$]*(?:\.[A-Za-z_][A-Za-z0-9_$]*|\[[^\]]*\])*)\.([A-Za-z_]*)$",
2128        )
2129        .unwrap();
2130        if let Some(m) = member_re.captures(text) {
2131            let base = m.get(1).unwrap().as_str();
2132            let prefix = m.get(2).unwrap().as_str();
2133            if base == "std" || base.starts_with("std.") {
2134                let ns = if base == "std" {
2135                    String::new()
2136                } else {
2137                    format!("{}.", &base[4..])
2138                };
2139                return uniq(
2140                    std_names()
2141                        .filter(|k| k.starts_with(&ns))
2142                        .map(|k| k[ns.len()..].split('.').next().unwrap_or("").to_string())
2143                        .filter(|k| k.starts_with(prefix))
2144                        .collect(),
2145                );
2146            }
2147            let (_, entry, _) = self.build(&self.state);
2148            let Some(entry) = entry else { return vec![] };
2149            let Ok(expr) = parse_expr(base) else {
2150                return vec![];
2151            };
2152            let cx = self.session_ctx(&self.state, &entry.env, Rc::new(|_, _| {}), None);
2153            let rt = infer(&cx, &expr).rt;
2154            fn members(t: Option<&RT>) -> Option<Vec<crate::semantics::Member>> {
2155                let t = t?;
2156                match &t.k {
2157                    RTk::Rec(_) => Some(rec_members(t)),
2158                    RTk::Union(arms) => {
2159                        let sets: Vec<Option<Vec<crate::semantics::Member>>> =
2160                            arms.iter().map(|a| members(Some(a))).collect();
2161                        if sets.iter().any(|s| s.is_none()) {
2162                            return None;
2163                        }
2164                        let sets: Vec<Vec<crate::semantics::Member>> =
2165                            sets.into_iter().map(|s| s.unwrap()).collect();
2166                        let first = sets.first().cloned().unwrap_or_default();
2167                        Some(
2168                            first
2169                                .into_iter()
2170                                .filter(|m| sets.iter().all(|s| s.iter().any(|x| x.name == m.name)))
2171                                .collect(),
2172                        )
2173                    }
2174                    RTk::Pred { base, .. } => members(Some(base)),
2175                    _ => None,
2176                }
2177            }
2178            let ms = members(rt.as_ref()).unwrap_or_default();
2179            return uniq(
2180                ms.iter()
2181                    .filter(|x| x.name.starts_with(prefix))
2182                    .map(|x| {
2183                        let kind = match x.kind {
2184                            MKind::Der => "derived",
2185                            MKind::Dflt => "defaulted",
2186                            MKind::Opt => "optional",
2187                            MKind::Req => "required",
2188                        };
2189                        format!(
2190                            "{}{}  {}{}",
2191                            x.name,
2192                            if x.hidden { "$" } else { "" },
2193                            kind,
2194                            x.ty.as_ref()
2195                                .map(|t| format!(": {}", type_text(Some(t))))
2196                                .unwrap_or_default()
2197                        )
2198                    })
2199                    .collect(),
2200            );
2201        }
2202        let word_re = Regex::new(r"([A-Za-z_$][A-Za-z0-9_$]*)$").unwrap();
2203        let prefix = word_re
2204            .captures(text)
2205            .map(|m| m.get(1).unwrap().as_str().to_string())
2206            .unwrap_or_default();
2207        if prefix.starts_with('$') {
2208            return uniq(
2209                ["$this", "$parent", "$root", "$key", "$path", "$referrers"]
2210                    .iter()
2211                    .filter(|x| x.starts_with(&prefix))
2212                    .map(|x| x.to_string())
2213                    .collect(),
2214            );
2215        }
2216        let mut names: Vec<String> = vec!["std".into()];
2217        let (_, entry, _) = self.build(&self.state);
2218        if let Some(entry) = entry {
2219            let e = &entry.env;
2220            names.extend(e.type_asts.borrow().keys().cloned());
2221            names.extend(e.consts.borrow().keys().cloned());
2222            names.extend(e.funcs.borrow().keys().cloned());
2223            names.extend(e.inputs.borrow().keys().cloned());
2224            names.extend(e.outputs.borrow().iter().map(|(o, _, _)| o.clone()));
2225            names.extend(e.imports.borrow().keys().cloned());
2226            names.extend(e.namespaces.borrow().keys().cloned());
2227            names.extend(e.diags.borrow().keys().cloned());
2228        }
2229        names.extend(self.state.outputs.iter().map(|(n, _, _)| n.clone()));
2230        let kw = [
2231            "if", "then", "else", "for", "in", "match", "with", "matches", "true", "false", "null",
2232        ];
2233        names.extend(kw.iter().map(|k| k.to_string()));
2234        uniq(
2235            names
2236                .into_iter()
2237                .filter(|n| n.starts_with(&prefix))
2238                .collect(),
2239        )
2240    }
2241    fn complete_path(&self, partial: &str) -> Vec<String> {
2242        if !partial.contains('.') && !partial.contains('[') {
2243            let mut v: Vec<String> = self
2244                .all_root_names()
2245                .into_iter()
2246                .filter(|n| n.starts_with(partial))
2247                .collect();
2248            v.sort();
2249            return v;
2250        }
2251        // the base is the shortest prefix whose remainder is `.ident?` or `[…` (the reference's lazy match)
2252        let tail_dot = Regex::new(r"^\.([A-Za-z_][A-Za-z0-9_]*)?$").unwrap();
2253        let tail_bracket = Regex::new(r#"^\["?[^\]]*$"#).unwrap();
2254        let mut base = partial.to_string();
2255        for (i, c) in partial.char_indices() {
2256            if c == '.' || c == '[' {
2257                let rest = &partial[i..];
2258                if tail_dot.is_match(rest) || tail_bracket.is_match(rest) {
2259                    base = partial[..i].to_string();
2260                    break;
2261                }
2262            }
2263        }
2264        let r = self.run(Mode::Full);
2265        let (Some(eng), Some(entry)) = (&r.eng, &r.entry) else {
2266            return vec![];
2267        };
2268        let Ok(segs) = parse_path(&base, "") else {
2269            return vec![];
2270        };
2271        let Some(v) = self.value_at(eng, entry, &segs) else {
2272            return vec![];
2273        };
2274        let Ok(v) = eng.deref(v) else { return vec![] };
2275        let mut out: Vec<String> = vec![];
2276        match &v {
2277            Value::Rec(inst) => {
2278                for (n, s) in &inst.borrow().slots {
2279                    if s.hidden {
2280                        continue;
2281                    }
2282                    out.push(format!("{base}.{n}"));
2283                }
2284            }
2285            Value::Map(m) => {
2286                for (k, _) in &m.borrow().entries {
2287                    out.push(format!("{base}[{}]", json_str(k)));
2288                }
2289            }
2290            Value::Arr(a) => {
2291                for i in 0..a.borrow().items.len() {
2292                    out.push(format!("{base}[{i}]"));
2293                }
2294            }
2295            _ => {}
2296        }
2297        let mut out: Vec<String> = out.into_iter().filter(|x| x.starts_with(partial)).collect();
2298        out.sort();
2299        out
2300    }
2301
2302    // ---- the scratch module (§4) ----
2303    pub fn scratch_text(&self) -> String {
2304        let mut parts: Vec<String> = self
2305            .state
2306            .decls
2307            .iter()
2308            .map(|(_, t)| t.trim().to_string())
2309            .collect();
2310        for (n, ty, expr) in &self.state.outputs {
2311            parts.push(format!(
2312                "output {n}: {} = {expr}",
2313                ty.clone().unwrap_or_else(|| self.inferred_type_text(expr))
2314            ));
2315        }
2316        if parts.is_empty() {
2317            String::new()
2318        } else {
2319            format!("{}\n", parts.join("\n"))
2320        }
2321    }
2322    fn inferred_type_text(&self, expr: &str) -> String {
2323        self.type_of(expr)
2324            .map(|(t, _, _)| t)
2325            .unwrap_or_else(|_| "any".into())
2326    }
2327    /// the scratch module as a file: imports of the entry's exports it uses, then the declarations
2328    pub fn module_text(&self) -> String {
2329        let body = self.scratch_text();
2330        let (_, entry, _) = self.build(&self.state);
2331        let used = identifiers(&body);
2332        let mut names: Vec<String> = entry
2333            .map(|e| {
2334                e.exports
2335                    .borrow()
2336                    .keys()
2337                    .filter(|n| used.contains(*n))
2338                    .cloned()
2339                    .collect()
2340            })
2341            .unwrap_or_default();
2342        names.sort();
2343        let header = match (&self.entry_path, names.is_empty()) {
2344            (Some(p), false) => format!(
2345                "import {{ {} }} from \"./{}\"\n\n",
2346                names.join(", "),
2347                p.file_name()
2348                    .map(|n| n.to_string_lossy().to_string())
2349                    .unwrap_or_default()
2350            ),
2351            _ => String::new(),
2352        };
2353        header + &body
2354    }
2355    pub fn fmt(&self) -> SResult<String> {
2356        let t = self.scratch_text();
2357        if t.is_empty() {
2358            return Ok(String::new());
2359        }
2360        format(&t).map_err(SessionError::new)
2361    }
2362    pub fn write(&self, file: &str) -> SResult<()> {
2363        std::fs::write(file, self.module_text())
2364            .map_err(|_| SessionError::new(format!("cannot write {file}")))
2365    }
2366
2367    // ---- documents out (§3) ----
2368    pub fn document_text(&self, name: &str) -> SResult<String> {
2369        if let Some(d) = self.state.document(name) {
2370            return Ok(doc_json(&d.doc));
2371        }
2372        if !self.has_root(name) {
2373            return Err(SessionError::new(format!("no root named {name}")));
2374        }
2375        let (_, docs, _) = self.evaluate(&[name.to_string()])?;
2376        docs.into_iter()
2377            .next()
2378            .and_then(|(_, j)| j)
2379            .ok_or_else(|| SessionError::new(format!("{name} is invalid")))
2380    }
2381    pub fn save(&self, name: &str, file: &str) -> SResult<()> {
2382        let text = self.document_text(name)?;
2383        std::fs::write(file, format!("{text}\n"))
2384            .map_err(|_| SessionError::new(format!("cannot write {file}")))
2385    }
2386    pub fn diff(&self, name: &str) -> SResult<Vec<String>> {
2387        let Some(d) = self.state.document(name) else {
2388            return Err(SessionError::new(if self.has_root(name) {
2389                format!("{name} holds no document")
2390            } else {
2391                format!("no root named {name}")
2392            }));
2393        };
2394        let (before, after) = (doc_json(&d.base), doc_json(&d.doc));
2395        if before == after {
2396            return Ok(vec!["(no changes)".to_string()]);
2397        }
2398        let a: Vec<String> = pretty_json(&before)
2399            .split('\n')
2400            .map(|s| s.to_string())
2401            .collect();
2402        let b: Vec<String> = pretty_json(&after)
2403            .split('\n')
2404            .map(|s| s.to_string())
2405            .collect();
2406        Ok(line_diff(&a, &b))
2407    }
2408
2409    // ---- introspection ----
2410    pub fn session_lines(&self) -> Vec<String> {
2411        let mut out = vec![];
2412        for (n, t) in &self.state.decls {
2413            out.push(format!(
2414                "declaration  {:<16} {}",
2415                n,
2416                t.trim().lines().next().unwrap_or("")
2417            ));
2418        }
2419        for (n, ty, expr) in &self.state.outputs {
2420            out.push(format!(
2421                "output       {:<16} {n}{} = {expr}",
2422                n,
2423                ty.as_ref().map(|t| format!(": {t}")).unwrap_or_default()
2424            ));
2425        }
2426        for (n, d) in &self.state.documents {
2427            out.push(format!(
2428                "document     {:<16} {}{}{}",
2429                n,
2430                d.origin.word(),
2431                d.file.as_ref().map(|f| format!(" {f}")).unwrap_or_default(),
2432                if d.edited { " (edited)" } else { "" }
2433            ));
2434        }
2435        out
2436    }
2437    pub fn history_lines(&self) -> Vec<String> {
2438        let mut out = vec![format!(
2439            "{} 0  (start)",
2440            if self.cursor == 0 { "*" } else { " " }
2441        )];
2442        for (i, op) in self.log.iter().enumerate() {
2443            out.push(format!(
2444                "{} {}  {}",
2445                if self.cursor == i + 1 { "*" } else { " " },
2446                i + 1,
2447                op_text(op)
2448            ));
2449        }
2450        out
2451    }
2452    pub fn script_lines(&self) -> Vec<String> {
2453        self.log[..self.cursor].iter().map(op_text).collect()
2454    }
2455}
2456
2457// ---------------- helpers ----------------
2458pub fn fmt_diag(d: &Diag, in_file: Option<&str>) -> String {
2459    format!(
2460        "{}{}{}{}: {}{}",
2461        d.severity,
2462        d.code
2463            .as_ref()
2464            .map(|c| format!(" [{c}]"))
2465            .unwrap_or_default(),
2466        d.id.as_ref().map(|i| format!(" {i}")).unwrap_or_default(),
2467        if d.path.is_empty() {
2468            String::new()
2469        } else {
2470            format!(" at {}", d.path)
2471        },
2472        d.message,
2473        in_file.map(|f| format!(" (in {f})")).unwrap_or_default()
2474    )
2475}
2476
2477pub fn op_text(op: &Op) -> String {
2478    match op {
2479        Op::Bind { name, src } => match src {
2480            BindSource::File { file, .. } => format!(":bind {name}={file}"),
2481            BindSource::Inline { text } => format!(
2482                ":bind {name} {}",
2483                read_json(text).map(|v| doc_json(&v)).unwrap_or_default()
2484            ),
2485            BindSource::Expr { text } => format!(":bind {name} = {}", text.trim()),
2486        },
2487        Op::Unbind { name } => format!(":unbind {name}"),
2488        Op::Edit { kind, path, expr } => format!(
2489            ":{} {path}{}",
2490            kind.word(),
2491            expr.as_ref()
2492                .map(|e| format!(" = {}", e.trim()))
2493                .unwrap_or_default()
2494        ),
2495        Op::Declare { text, .. } => text.trim().to_string(),
2496        Op::Output { name, ty, expr } => format!(
2497            "{name}{} = {}",
2498            ty.as_ref().map(|t| format!(": {t}")).unwrap_or_default(),
2499            expr.trim()
2500        ),
2501        Op::Drop { name } => format!(":drop {name}"),
2502        Op::Reload { .. } => ":reload".into(),
2503        Op::Reset => ":reset".into(),
2504    }
2505}
2506
2507// a functional edit of a document at a path (read_json's shape)
2508fn edit_value(
2509    node: &Value,
2510    segs: &[Seg],
2511    i: usize,
2512    kind: EditKind,
2513    value: Option<&Value>,
2514    path: &str,
2515) -> SResult<Value> {
2516    if i < segs.len() - 1 {
2517        let s = &segs[i];
2518        let Some(child) = doc_step(node, s) else {
2519            return Err(SessionError::new(format!(
2520                "nothing at {}",
2521                path_str(&segs[..=i], None)
2522            )));
2523        };
2524        let new_child = edit_value(&child, segs, i + 1, kind, value, path)?;
2525        return Ok(replace_child(node, s, new_child));
2526    }
2527    let last = &segs[segs.len() - 1];
2528    let k = seg_text(last);
2529    match (node, last) {
2530        (Value::JObj(es), _) => {
2531            let idx = es.iter().position(|(kk, _)| *kk == k);
2532            let mut es: Vec<(String, Value)> = (**es).clone();
2533            match kind {
2534                EditKind::Create => {
2535                    if idx.is_some() {
2536                        return Err(SessionError::new(format!("{path} already holds a value")));
2537                    }
2538                    es.push((k, value.cloned().unwrap_or(Value::Null)));
2539                }
2540                EditKind::Update => match idx {
2541                    Some(i) => es[i].1 = value.cloned().unwrap_or(Value::Null),
2542                    None => return Err(SessionError::new(format!("nothing at {path}"))),
2543                },
2544                EditKind::Remove => match idx {
2545                    Some(i) => {
2546                        es.remove(i);
2547                    }
2548                    None => return Err(SessionError::new(format!("nothing at {path}"))),
2549                },
2550            }
2551            Ok(Value::JObj(Rc::new(es)))
2552        }
2553        (Value::JArr(items), Seg::Idx(k)) => {
2554            let mut items: Vec<Value> = (**items).clone();
2555            match kind {
2556                EditKind::Create => {
2557                    if *k < items.len() {
2558                        return Err(SessionError::new(format!("{path} already holds a value")));
2559                    }
2560                    if *k > items.len() {
2561                        return Err(SessionError::new(format!(
2562                            "{path} is past the end of the array"
2563                        )));
2564                    }
2565                    items.push(value.cloned().unwrap_or(Value::Null));
2566                }
2567                EditKind::Update => {
2568                    if *k >= items.len() {
2569                        return Err(SessionError::new(format!("nothing at {path}")));
2570                    }
2571                    items[*k] = value.cloned().unwrap_or(Value::Null);
2572                }
2573                EditKind::Remove => {
2574                    if *k >= items.len() {
2575                        return Err(SessionError::new(format!("nothing at {path}")));
2576                    }
2577                    items.remove(*k);
2578                }
2579            }
2580            Ok(Value::JArr(Rc::new(items)))
2581        }
2582        _ => Err(SessionError::new(format!(
2583            "{} is not a record, map, or array",
2584            path_str(&segs[..segs.len() - 1], None)
2585        ))),
2586    }
2587}
2588fn replace_child(node: &Value, s: &Seg, child: Value) -> Value {
2589    match (node, s) {
2590        (Value::JObj(es), _) => {
2591            let k = seg_text(s);
2592            let es: Vec<(String, Value)> = es
2593                .iter()
2594                .map(|(kk, v)| {
2595                    if *kk == k {
2596                        (kk.clone(), child.clone())
2597                    } else {
2598                        (kk.clone(), v.clone())
2599                    }
2600                })
2601                .collect();
2602            Value::JObj(Rc::new(es))
2603        }
2604        (Value::JArr(items), Seg::Idx(i)) => {
2605            let items: Vec<Value> = items
2606                .iter()
2607                .enumerate()
2608                .map(|(j, v)| if j == *i { child.clone() } else { v.clone() })
2609                .collect();
2610            Value::JArr(Rc::new(items))
2611        }
2612        _ => node.clone(),
2613    }
2614}
2615
2616// a detached output (§3): its declaration becomes `input name: T` in the
2617// session's copy of the module — the name stays declared, the checker
2618// sees a root of the same type, and the session binds the projected
2619// document to it; line numbers are kept
2620fn detach_outputs(text: &str, names: &[String]) -> String {
2621    if names.is_empty() {
2622        return text.to_string();
2623    }
2624    let decls = parse_source(text).decls;
2625    let mut lines: Vec<String> = text.split('\n').map(|s| s.to_string()).collect();
2626    for d in &decls {
2627        let (DeclBody::Output { name, .. }, Some(loc)) = (&d.body, d.loc) else {
2628            continue;
2629        };
2630        if !names.contains(name) {
2631            continue;
2632        }
2633        let src = lines[loc.sl..=loc.el.min(lines.len() - 1)].join("\n");
2634        let name_at = src.find(name.as_str()).unwrap_or(0);
2635        let colon = src[name_at..]
2636            .find(':')
2637            .map(|i| i + name_at)
2638            .unwrap_or(src.len());
2639        // the type text: from the colon to the `=` at bracket depth 0
2640        let bytes = src.as_bytes();
2641        let mut depth = 0i32;
2642        let mut eq: Option<usize> = None;
2643        let mut i = colon + 1;
2644        while i < bytes.len() {
2645            let c = bytes[i] as char;
2646            if "{[(<".contains(c) {
2647                depth += 1;
2648            } else if "}])>".contains(c) {
2649                depth -= 1;
2650            } else if c == '='
2651                && depth == 0
2652                && bytes.get(i + 1) != Some(&b'=')
2653                && i > 0
2654                && !matches!(bytes[i - 1], b'!' | b'<' | b'>')
2655            {
2656                eq = Some(i);
2657                break;
2658            }
2659            i += 1;
2660        }
2661        let type_text = match eq {
2662            Some(e) => src[colon + 1..e].trim().to_string(),
2663            None => src[(colon + 1).min(src.len())..].trim().to_string(),
2664        };
2665        let squeezed = SQUEEZE_WS.replace_all(&type_text, " ").to_string();
2666        lines[loc.sl] = format!("input {name}: {squeezed}");
2667        for l in lines.iter_mut().take(loc.el + 1).skip(loc.sl + 1) {
2668            l.clear();
2669        }
2670    }
2671    lines.join("\n")
2672}
2673
2674// the places an expression reads, as navigation chains (a static
2675// approximation of the engine's read set: names, members, indexes)
2676fn reads_of(e: &Rc<Expr>) -> Vec<Rc<Expr>> {
2677    fn is_chain(x: &Expr) -> bool {
2678        match x {
2679            Expr::Name(_) | Expr::Ctx(_) => true,
2680            Expr::Member { x, .. } | Expr::Index { x, .. } => is_chain(x),
2681            _ => false,
2682        }
2683    }
2684    fn go(x: &Rc<Expr>, out: &mut Vec<Rc<Expr>>) {
2685        match &**x {
2686            Expr::Member { .. } | Expr::Index { .. } if is_chain(x) => {
2687                out.push(x.clone());
2688                if let Expr::Index { i, .. } = &**x {
2689                    go(i, out);
2690                }
2691            }
2692            Expr::Name(_) => out.push(x.clone()),
2693            Expr::Lit(_)
2694            | Expr::UnitLit { .. }
2695            | Expr::Ctx(_)
2696            | Expr::Pattern(_)
2697            | Expr::Referrers { .. } => {}
2698            Expr::Template(parts) => {
2699                for p in parts {
2700                    if let TPart::Expr(e) = p {
2701                        go(e, out);
2702                    }
2703                }
2704            }
2705            Expr::Obj(es) => es.iter().for_each(|(_, v)| go(v, out)),
2706            Expr::Arr(items) => items.iter().for_each(|(_, v)| go(v, out)),
2707            Expr::Comp { head, clauses } => {
2708                go(head, out);
2709                for c in clauses {
2710                    go(&c.iter, out);
2711                    c.filters.iter().for_each(|f| go(f, out));
2712                }
2713            }
2714            Expr::MapComp { key, val, clauses } => {
2715                go(key, out);
2716                go(val, out);
2717                for c in clauses {
2718                    go(&c.iter, out);
2719                    c.filters.iter().for_each(|f| go(f, out));
2720                }
2721            }
2722            Expr::Bin { l, r, .. } => {
2723                go(l, out);
2724                go(r, out);
2725            }
2726            Expr::Un { x, .. } | Expr::Paren(x) => go(x, out),
2727            Expr::If { c, t, f } => {
2728                go(c, out);
2729                go(t, out);
2730                go(f, out);
2731            }
2732            Expr::Lambda { body, .. } => go(body, out),
2733            Expr::Call { fun, args } => {
2734                go(fun, out);
2735                args.iter().for_each(|a| go(a, out));
2736            }
2737            Expr::Member { x, .. } => go(x, out),
2738            Expr::Index { x, i } => {
2739                go(x, out);
2740                go(i, out);
2741            }
2742            Expr::With { base, patch } => {
2743                go(base, out);
2744                go(patch, out);
2745            }
2746            Expr::Match { subject, arms } => {
2747                go(subject, out);
2748                arms.iter().for_each(|a| go(&a.body, out));
2749            }
2750        }
2751    }
2752    let mut out = vec![];
2753    go(e, &mut out);
2754    out.into_iter()
2755        .filter(|x| !matches!(&**x, Expr::Name(n) if n == "true" || n == "false" || n == "null"))
2756        .collect()
2757}
2758
2759/// an expression's text, for chains and simple forms (the trace view)
2760pub fn expr_text(e: &Expr) -> String {
2761    match e {
2762        Expr::Lit(v) => match v {
2763            Value::Str(s) => json_str(s),
2764            other => crate::infer::js_str(other),
2765        },
2766        Expr::UnitLit { num, unit } => format!("{}{unit}", crate::semantics::js_num_str(*num)),
2767        Expr::Name(n) | Expr::Ctx(n) => n.clone(),
2768        Expr::Member { x, name, safe } => {
2769            format!("{}{}{name}", expr_text(x), if *safe { "?." } else { "." })
2770        }
2771        Expr::Index { x, i } => format!("{}[{}]", expr_text(x), expr_text(i)),
2772        Expr::Paren(x) => format!("({})", expr_text(x)),
2773        Expr::Bin { op, l, r } => format!("{} {op} {}", expr_text(l), expr_text(r)),
2774        Expr::Un { op, x } => format!("{op}{}", expr_text(x)),
2775        Expr::Call { fun, args } => format!(
2776            "{}({})",
2777            expr_text(fun),
2778            args.iter()
2779                .map(|a| expr_text(a))
2780                .collect::<Vec<_>>()
2781                .join(", ")
2782        ),
2783        Expr::If { c, t, f } => format!(
2784            "if {} then {} else {}",
2785            expr_text(c),
2786            expr_text(t),
2787            expr_text(f)
2788        ),
2789        Expr::Referrers { ty, member } => format!("$referrers({ty}, {})", json_str(member)),
2790        Expr::Template(parts) => format!(
2791            "`{}`",
2792            parts
2793                .iter()
2794                .map(|p| match p {
2795                    TPart::Text(s) => s.clone(),
2796                    TPart::Expr(e) => format!("${{{}}}", expr_text(e)),
2797                })
2798                .collect::<String>()
2799        ),
2800        Expr::Obj(es) => format!(
2801            "{{ {} }}",
2802            es.iter()
2803                .map(|(k, v)| format!("{k}: {}", expr_text(v)))
2804                .collect::<Vec<_>>()
2805                .join(", ")
2806        ),
2807        Expr::Arr(items) => format!(
2808            "[{}]",
2809            items
2810                .iter()
2811                .map(|(spread, v)| format!("{}{}", if *spread { "..." } else { "" }, expr_text(v)))
2812                .collect::<Vec<_>>()
2813                .join(", ")
2814        ),
2815        Expr::Comp { clauses, .. } => format!(
2816            "[for {} … ]",
2817            clauses
2818                .iter()
2819                .map(|c| format!("{} in {}", c.v, expr_text(&c.iter)))
2820                .collect::<Vec<_>>()
2821                .join(", ")
2822        ),
2823        Expr::Lambda { params, .. } => format!("({}) => …", params.join(", ")),
2824        Expr::With { base, .. } => format!("{} with …", expr_text(base)),
2825        Expr::Match { subject, .. } => format!("match {} {{ … }}", expr_text(subject)),
2826        _ => "…".into(),
2827    }
2828}
2829
2830// a minimal line diff (longest common subsequence)
2831fn line_diff(a: &[String], b: &[String]) -> Vec<String> {
2832    let (n, m) = (a.len(), b.len());
2833    let mut dp = vec![vec![0usize; m + 1]; n + 1];
2834    for i in (0..n).rev() {
2835        for j in (0..m).rev() {
2836            dp[i][j] = if a[i] == b[j] {
2837                dp[i + 1][j + 1] + 1
2838            } else {
2839                dp[i + 1][j].max(dp[i][j + 1])
2840            };
2841        }
2842    }
2843    let mut out = vec![];
2844    let (mut i, mut j) = (0, 0);
2845    while i < n && j < m {
2846        if a[i] == b[j] {
2847            out.push(format!("  {}", a[i]));
2848            i += 1;
2849            j += 1;
2850        } else if dp[i + 1][j] >= dp[i][j + 1] {
2851            out.push(format!("- {}", a[i]));
2852            i += 1;
2853        } else {
2854            out.push(format!("+ {}", b[j]));
2855            j += 1;
2856        }
2857    }
2858    while i < n {
2859        out.push(format!("- {}", a[i]));
2860        i += 1;
2861    }
2862    while j < m {
2863        out.push(format!("+ {}", b[j]));
2864        j += 1;
2865    }
2866    out
2867}