Skip to main content

quarb_session/
session.rs

1//! The session: a macro table where each accepted line becomes
2//! `def &N: <line> ;`, evaluated through an [`Executor`] and persisted
3//! through a [`Store`].
4//!
5//! History is the language's own reuse mechanism, not a bolted-on
6//! cell store: line 3 is the fragment `&3`, continued through the pipe
7//! (`&3 | /name::`, `&3 | [pred]`, `&3 @| count`). Frozen recall
8//! (`&N#`) replays a line's captured footprint; the live/version-
9//! pinned variants sharpen once a re-materializing executor (the
10//! daemon) is in play.
11
12use crate::{Cell, Executor, SessionState, Store};
13use anyhow::{Context, Result};
14use std::collections::HashMap;
15
16pub struct Session {
17    executor: Box<dyn Executor>,
18    store: Box<dyn Store>,
19    /// The macro table as definition text, re-parsed per query
20    /// (`def &1: …;\n …`). Kept as text so it seeds from `--defs`,
21    /// round-trips for display, and persists trivially.
22    defs_text: String,
23    /// Each line's rendered output, captured at commit — the frozen
24    /// footprint a `&N#` recall replays. In memory only for now.
25    snapshots: HashMap<usize, Vec<Cell>>,
26    /// The next line's number — the `&N` a fresh line will claim.
27    line_no: usize,
28}
29
30impl Session {
31    /// Build a session over an executor and a store, restoring any
32    /// persisted macro history from the store.
33    pub fn new(executor: Box<dyn Executor>, store: Box<dyn Store>) -> Session {
34        let state = store.load().unwrap_or_default();
35        let line_no = state.line_no.max(1);
36        Session {
37            executor,
38            store,
39            defs_text: state.defs_text,
40            snapshots: HashMap::new(),
41            line_no,
42        }
43    }
44
45    /// Seed the macro table from a `--defs` file (validated first).
46    pub fn seed_defs(&mut self, text: &str) -> Result<()> {
47        quarb::parse_defs(text).context("parsing --defs")?;
48        self.defs_text = format!("{text}\n");
49        self.persist();
50        Ok(())
51    }
52
53    /// Add a `def`/`macro` line to the table (validated first). Unlike
54    /// a query line, a definition is not run.
55    pub fn add_def(&mut self, line: &str) -> Result<()> {
56        let candidate = format!("{}{}\n", self.defs_text, line);
57        quarb::parse_defs(&candidate).context("parsing definition")?;
58        self.defs_text = candidate;
59        self.persist();
60        Ok(())
61    }
62
63    /// The line with the macro table prepended, so history refs
64    /// resolve inline.
65    fn combined(&self, line: &str) -> String {
66        if self.defs_text.is_empty() {
67            line.to_string()
68        } else {
69            format!("{}\n{line}", self.defs_text)
70        }
71    }
72
73    /// Evaluate a line against the standing arbor (`&N`). Pure —
74    /// history is not touched (a failed line commits nothing).
75    pub fn eval(&self, line: &str) -> Result<Vec<Cell>> {
76        self.executor.run(&self.combined(line))
77    }
78
79    /// Evaluate a line against a freshly re-materialized source — the
80    /// `&N!` live reading, which sees current data.
81    pub fn eval_fresh(&self, line: &str) -> Result<Vec<Cell>> {
82        self.executor.run_fresh(&self.combined(line))
83    }
84
85    /// Register an accepted line as `&N` and capture its output as the
86    /// frozen footprint for `&N#`. Returns whether the line's shape
87    /// could be a macro body (so `&N` will resolve); either way the
88    /// line number advances so labels track what the user saw.
89    pub fn commit(&mut self, line: &str, snapshot: Vec<Cell>) -> bool {
90        self.snapshots.insert(self.line_no, snapshot);
91        // A space before the `;` terminator: a line ending in a `::`
92        // projection would otherwise lex `::;` as the metadata sigil.
93        let candidate = format!("{}def &{}: {} ;\n", self.defs_text, self.line_no, line);
94        let referenceable = quarb::parse_defs(&candidate).is_ok();
95        if referenceable {
96            self.defs_text = candidate;
97        }
98        self.line_no += 1;
99        self.persist();
100        referenceable
101    }
102
103    /// The frozen output of line `n`, if captured — what a `&N#`
104    /// recall replays.
105    pub fn frozen(&self, n: usize) -> Option<&Vec<Cell>> {
106        self.snapshots.get(&n)
107    }
108
109    /// Record a frozen-recall line: it takes the next number and keeps
110    /// its own snapshot, but is not itself a referenceable macro body.
111    pub fn record_frozen(&mut self, snapshot: Vec<Cell>) {
112        self.snapshots.insert(self.line_no, snapshot);
113        self.line_no += 1;
114        self.persist();
115    }
116
117    /// Persist the durable state (best-effort; a store error does not
118    /// abort the session).
119    fn persist(&self) {
120        let state = SessionState {
121            defs_text: self.defs_text.clone(),
122            line_no: self.line_no,
123        };
124        let _ = self.store.save(&state);
125    }
126
127    /// The `&N` a fresh line will claim.
128    pub fn line_no(&self) -> usize {
129        self.line_no
130    }
131
132    /// The macro history text, for a `:history` command.
133    pub fn history(&self) -> &str {
134        &self.defs_text
135    }
136
137    /// Replace the macro history and line counter — restoring a
138    /// persisted session (e.g. from the browser's localStorage). Frozen
139    /// snapshots are not restored; they regenerate on re-run.
140    pub fn restore(&mut self, defs_text: String, line_no: usize) {
141        self.defs_text = defs_text;
142        self.line_no = line_no.max(1);
143    }
144
145    /// Clear the macro history and restart line numbering.
146    pub fn reset(&mut self) {
147        self.defs_text.clear();
148        self.snapshots.clear();
149        self.line_no = 1;
150        self.persist();
151    }
152}