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}