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 /// Swap the executor under a running session — an in-session
46 /// remount (`:mount`). History and the macro table stay: `&N`
47 /// re-runs against the new source set.
48 pub fn set_executor(&mut self, executor: Box<dyn Executor>) {
49 self.executor = executor;
50 }
51
52 /// Seed the macro table from a `--defs` file (validated first).
53 /// Stored stripped of `#` comment lines: the table is prepended
54 /// to query text, and the query lexer has no comment syntax.
55 pub fn seed_defs(&mut self, text: &str) -> Result<()> {
56 quarb::parse_defs(text).context("parsing --defs")?;
57 self.defs_text = format!("{}\n", quarb::strip_defs_comments(text));
58 self.persist();
59 Ok(())
60 }
61
62 /// Add a `def`/`macro` line to the table (validated first). Unlike
63 /// a query line, a definition is not run.
64 pub fn add_def(&mut self, line: &str) -> Result<()> {
65 let candidate = format!("{}{}\n", self.defs_text, line);
66 quarb::parse_defs(&candidate).context("parsing definition")?;
67 self.defs_text = candidate;
68 self.persist();
69 Ok(())
70 }
71
72 /// The line with the macro table prepended, so history refs
73 /// resolve inline.
74 fn combined(&self, line: &str) -> String {
75 if self.defs_text.is_empty() {
76 line.to_string()
77 } else {
78 format!("{}\n{line}", self.defs_text)
79 }
80 }
81
82 /// Evaluate a line against the standing arbor (`&N`). Pure —
83 /// history is not touched (a failed line commits nothing).
84 pub fn eval(&self, line: &str) -> Result<Vec<Cell>> {
85 self.executor.run(&self.combined(line))
86 }
87
88 /// Evaluate a line against a freshly re-materialized source — the
89 /// `&N!` live reading, which sees current data.
90 pub fn eval_fresh(&self, line: &str) -> Result<Vec<Cell>> {
91 self.executor.run_fresh(&self.combined(line))
92 }
93
94 /// Run a line and render its results as exportable markup
95 /// (`md`, `html`, `txt`) — the panel's rendered-export path.
96 /// History refs resolve inline, exactly as in [`eval`](Self::eval);
97 /// an empty line exports the whole document.
98 pub fn export(&self, line: &str, kind: &str) -> Result<String> {
99 let line = line.trim();
100 if line.is_empty() {
101 return self.executor.export("", kind);
102 }
103 self.executor.export(&self.combined(line), kind)
104 }
105
106 /// Register an accepted line as `&N` and capture its output as the
107 /// frozen footprint for `&N#`. Returns whether the line's shape
108 /// could be a macro body (so `&N` will resolve); either way the
109 /// line number advances so labels track what the user saw.
110 pub fn commit(&mut self, line: &str, snapshot: Vec<Cell>) -> bool {
111 self.snapshots.insert(self.line_no, snapshot);
112 // A space before the `;` terminator: a line ending in a `::`
113 // projection would otherwise lex `::;` as the metadata sigil.
114 let candidate = format!("{}def &{}: {} ;\n", self.defs_text, self.line_no, line);
115 let referenceable = quarb::parse_defs(&candidate).is_ok();
116 if referenceable {
117 self.defs_text = candidate;
118 }
119 self.line_no += 1;
120 self.persist();
121 referenceable
122 }
123
124 /// The frozen output of line `n`, if captured — what a `&N#`
125 /// recall replays.
126 pub fn frozen(&self, n: usize) -> Option<&Vec<Cell>> {
127 self.snapshots.get(&n)
128 }
129
130 /// Record a frozen-recall line: it takes the next number and keeps
131 /// its own snapshot, but is not itself a referenceable macro body.
132 pub fn record_frozen(&mut self, snapshot: Vec<Cell>) {
133 self.snapshots.insert(self.line_no, snapshot);
134 self.line_no += 1;
135 self.persist();
136 }
137
138 /// Persist the durable state (best-effort; a store error does not
139 /// abort the session).
140 fn persist(&self) {
141 let state = SessionState {
142 defs_text: self.defs_text.clone(),
143 line_no: self.line_no,
144 };
145 let _ = self.store.save(&state);
146 }
147
148 /// The `&N` a fresh line will claim.
149 pub fn line_no(&self) -> usize {
150 self.line_no
151 }
152
153 /// The macro history text, for a `:history` command.
154 pub fn history(&self) -> &str {
155 &self.defs_text
156 }
157
158 /// Replace the macro history and line counter — restoring a
159 /// persisted session (e.g. from the browser's localStorage). Frozen
160 /// snapshots are not restored; they regenerate on re-run.
161 pub fn restore(&mut self, defs_text: String, line_no: usize) {
162 self.defs_text = defs_text;
163 self.line_no = line_no.max(1);
164 }
165
166 /// Clear the macro history and restart line numbering.
167 pub fn reset(&mut self) {
168 self.defs_text.clear();
169 self.snapshots.clear();
170 self.line_no = 1;
171 self.persist();
172 }
173}
174
175#[cfg(test)]
176mod tests {
177 use super::*;
178 use crate::MemStore;
179
180 struct NullExec;
181 impl Executor for NullExec {
182 fn run(&self, _query: &str) -> anyhow::Result<Vec<Cell>> {
183 Ok(vec![])
184 }
185 }
186
187 /// A commented defs file seeds a table the query lexer can take:
188 /// the stored text is prepended to every line, and the lexer has
189 /// no comment syntax, so `#` lines must not survive seeding.
190 #[test]
191 fn seed_defs_strips_comments() {
192 let mut s = Session::new(Box::new(NullExec), Box::new(MemStore));
193 s.seed_defs("# a library header\ndef &a: /x;\n# and a note\ndef &b: /y;")
194 .unwrap();
195 assert!(!s.history().contains('#'), "history: {}", s.history());
196 assert!(s.history().contains("def &a"));
197 assert!(s.history().contains("def &b"));
198 }
199}