Skip to main content

logicaffeine_compile/
repl.rs

1//! `ReplSession` — the replay-based interactive session behind `largo repl`
2//! (and any future notebook surface).
3//!
4//! # Architecture: replay, not a persistent interpreter
5//!
6//! The session accumulates source text — `## ` definition blocks and Main
7//! statements — and on every [`eval`](ReplSession::eval) composes them into
8//! a complete program and re-runs it through
9//! [`interpret_for_ui_with_args`](crate::interpret_for_ui_with_args): the
10//! exact engine behind `largo run --interpret` (VM+JIT, prelude
11//! auto-import, the optimizer, and the debug shadow oracle). Only output
12//! lines past a high-water mark are surfaced, so each eval appears
13//! incremental. This gives zero semantic divergence between the REPL and
14//! real programs by construction, and makes [`source`](ReplSession::source)
15//! a valid, runnable `.lg` program at every moment (`:save` = a program).
16//!
17//! A failing input **rolls back**: the offending chunk is popped and the
18//! high-water mark stays put, so the session never wedges and never
19//! duplicates output.
20//!
21//! # The replay caveat
22//!
23//! Re-running the whole program re-executes side effects. Deterministic
24//! programs are unaffected (their prior output is suppressed by the
25//! high-water mark), but non-deterministic operations — current time,
26//! random numbers, file or network I/O — are re-evaluated on every eval,
27//! so a binding like `Let t be the current time.` drifts across lines.
28//! [`reset`](ReplSession::reset) is the escape hatch; a seeded replay mode
29//! is the growth path.
30
31/// The outcome of one REPL evaluation.
32#[derive(Debug, Default)]
33pub struct ReplOutcome {
34    /// Output lines newly produced by this eval (past the high-water mark).
35    pub new_lines: Vec<String>,
36    /// The error message, if the input failed (the input was rolled back).
37    pub error: Option<String>,
38}
39
40/// A persistent imperative REPL session. See the module docs for the
41/// replay architecture.
42#[derive(Debug, Default)]
43pub struct ReplSession {
44    /// Accumulated `## ` definition blocks (functions, types, policies…).
45    defs: Vec<String>,
46    /// Accumulated Main-body statement chunks.
47    stmts: Vec<String>,
48    /// Output lines already surfaced (the high-water mark).
49    emitted: usize,
50    /// The argv the program's `args()` sees; index 0 is the program name.
51    argv: Vec<String>,
52}
53
54impl ReplSession {
55    /// A fresh session.
56    pub fn new() -> Self {
57        Self { defs: Vec::new(), stmts: Vec::new(), emitted: 0, argv: vec!["repl".to_string()] }
58    }
59
60    /// Evaluate one input chunk (a statement, a multi-statement chunk, or a
61    /// `## ` definition block), returning the new output lines.
62    pub async fn eval(&mut self, input: &str) -> ReplOutcome {
63        let trimmed = input.trim();
64        if trimmed.is_empty() || trimmed == "## Main" {
65            return ReplOutcome::default();
66        }
67        let is_def = trimmed.starts_with("## ");
68        if is_def {
69            self.defs.push(trimmed.to_string());
70        } else {
71            self.stmts.push(trimmed.to_string());
72        }
73
74        let program = self.source();
75        let result = crate::interpret_for_ui_with_args(&program, &self.argv).await;
76        let new_lines: Vec<String> =
77            result.lines.get(self.emitted..).map(|s| s.to_vec()).unwrap_or_default();
78        match result.error {
79            Some(err) => {
80                // Roll back: the reverted program deterministically reproduces
81                // the old output prefix, so the mark must not advance.
82                if is_def {
83                    self.defs.pop();
84                } else {
85                    self.stmts.pop();
86                }
87                ReplOutcome { new_lines, error: Some(err) }
88            }
89            None => {
90                self.emitted = result.lines.len();
91                ReplOutcome { new_lines, error: None }
92            }
93        }
94    }
95
96    /// Synchronous [`eval`](Self::eval) for native callers.
97    pub fn eval_sync(&mut self, input: &str) -> ReplOutcome {
98        futures::executor::block_on(self.eval(input))
99    }
100
101    /// The session as a complete LOGOS program: definitions first, then the
102    /// Main body — always runnable as-is.
103    pub fn source(&self) -> String {
104        let mut out = String::new();
105        for def in &self.defs {
106            out.push_str(def);
107            out.push_str("\n\n");
108        }
109        out.push_str("## Main\n\n");
110        for stmt in &self.stmts {
111            out.push_str(stmt);
112            out.push('\n');
113        }
114        out
115    }
116
117    /// The session's global bindings as `(name, type, value)` rows, sorted
118    /// by name. Unavailable (empty) for concurrent programs — the
119    /// inspection replay runs on the synchronous tree-walker.
120    ///
121    /// NOTE: this **re-executes** the accumulated program (the replay
122    /// architecture) — callers that only need names for completion must use
123    /// [`binding_names`](Self::binding_names), which never executes.
124    pub fn vars(&self) -> Vec<(String, String, String)> {
125        crate::ui_bridge::repl_global_bindings(&self.source(), &self.argv).unwrap_or_default()
126    }
127
128    /// The names this session binds — a pure textual scan (`Let <name> be`,
129    /// `Set <name> to`, `## To <name>`) with **zero execution**, safe to call
130    /// after every keystroke for completion feeds.
131    pub fn binding_names(&self) -> Vec<String> {
132        let mut names: Vec<String> = Vec::new();
133        let mut push = |name: &str| {
134            let name = name.trim();
135            if !name.is_empty() && !names.iter().any(|n| n == name) {
136                names.push(name.to_string());
137            }
138        };
139        for chunk in self.defs.iter().chain(self.stmts.iter()) {
140            for line in chunk.lines() {
141                let t = line.trim_start();
142                if let Some(rest) = t.strip_prefix("Let ") {
143                    if let Some((name, _)) = rest.split_once(" be") {
144                        push(name);
145                    }
146                } else if let Some(rest) = t.strip_prefix("## To ") {
147                    let name: String = rest
148                        .chars()
149                        .take_while(|c| c.is_alphanumeric() || *c == '_')
150                        .collect();
151                    push(&name);
152                }
153            }
154        }
155        names
156    }
157
158    /// Replace the session with the contents of a saved program (the
159    /// inverse of [`source`](Self::source)). Returns the program's full
160    /// output as the outcome.
161    pub fn load_source(&mut self, src: &str) -> ReplOutcome {
162        self.reset();
163
164        // A file with no `## ` headers at all is a bare statement list —
165        // treat the whole thing as the Main body (mirroring the engine's
166        // implicit-main convention) instead of silently loading nothing.
167        let headerless = !src.lines().any(|l| l.starts_with("## "));
168
169        let mut current_def: Option<String> = None;
170        let mut main_body = String::new();
171        let mut in_main = headerless;
172        for line in src.lines() {
173            if line.trim_end() == "## Main" {
174                if let Some(def) = current_def.take() {
175                    self.defs.push(def.trim().to_string());
176                }
177                in_main = true;
178            } else if line.starts_with("## ") {
179                if let Some(def) = current_def.take() {
180                    self.defs.push(def.trim().to_string());
181                }
182                in_main = false;
183                current_def = Some(format!("{line}\n"));
184            } else if in_main {
185                main_body.push_str(line);
186                main_body.push('\n');
187            } else if let Some(def) = current_def.as_mut() {
188                def.push_str(line);
189                def.push('\n');
190            }
191        }
192        if let Some(def) = current_def.take() {
193            self.defs.push(def.trim().to_string());
194        }
195        let main_body = main_body.trim().to_string();
196        if !main_body.is_empty() {
197            self.stmts.push(main_body);
198        }
199
200        let program = self.source();
201        let result =
202            futures::executor::block_on(crate::interpret_for_ui_with_args(&program, &self.argv));
203        match result.error {
204            Some(err) => {
205                self.reset();
206                ReplOutcome { new_lines: Vec::new(), error: Some(err) }
207            }
208            None => {
209                self.emitted = result.lines.len();
210                ReplOutcome { new_lines: result.lines, error: None }
211            }
212        }
213    }
214
215    /// Clear the whole session: definitions, statements, and the output
216    /// ledger.
217    pub fn reset(&mut self) {
218        self.defs.clear();
219        self.stmts.clear();
220        self.emitted = 0;
221    }
222}