1use std::fs;
20use std::io::{self, BufRead, Write};
21use std::path::{Path, PathBuf};
22
23use crate::{evaluate_with_env, format_diagnostic, format_foundation_report, format_proof_report, Env, EnvOptions, RunResult};
24
25#[derive(Debug, Clone, Default)]
28pub struct ReplStep {
29 pub output: String,
30 pub error: String,
31 pub exit: bool,
32}
33
34const BUILTIN_KEYWORDS: &[&str] = &[
36 "and", "or", "not", "both", "neither", "is", "has", "probability", "range", "valence", "true",
37 "false", "unknown", "undefined", "lambda", "apply", "Pi", "Type", "Prop", "of", "type",
38];
39
40const META_COMMANDS: &[&str] = &[
42 ":help", ":reset", ":env", ":load", ":save", ":quit", ":exit",
43];
44
45const HELP_TEXT: &str = "RML REPL — meta-commands:\n :help show this help message\n :reset discard all state and start a fresh Env\n :env print declared terms / assignments / types / lambdas\n :load <file> evaluate a .lino file in the current Env\n :save <file> write the session transcript (as .lino) to <file>\n :quit exit the REPL (also :exit, Ctrl-D)\n\nLiNo input is evaluated form-by-form. Query results are printed; errors\nare reported as diagnostics with source spans.";
46
47fn format_number(n: f64) -> String {
48 let formatted = format!("{:.6}", n);
49 let formatted = formatted.trim_end_matches('0').trim_end_matches('.');
50 formatted.to_string()
51}
52
53fn format_run_result(r: &RunResult) -> String {
54 match r {
55 RunResult::Num(n) => format_number(*n),
56 RunResult::Type(s) => s.clone(),
57 RunResult::Foundation(report) => format_foundation_report(report),
58 RunResult::Proof(report) => format_proof_report(report),
59 }
60}
61
62pub struct Repl {
65 pub env: Env,
66 pub transcript: Vec<String>,
67 env_options: EnvOptions,
68 cwd: PathBuf,
69}
70
71impl Repl {
72 pub fn new(env_options: EnvOptions, cwd: Option<PathBuf>) -> Self {
73 let env = Env::new(Some(env_options.clone()));
74 Self {
75 env,
76 transcript: Vec::new(),
77 env_options,
78 cwd: cwd.unwrap_or_else(|| std::env::current_dir().unwrap_or_else(|_| PathBuf::from("."))),
79 }
80 }
81
82 pub fn reset(&mut self) {
85 self.env = Env::new(Some(self.env_options.clone()));
86 self.transcript.clear();
87 }
88
89 pub fn evaluate_source(&mut self, source: &str, file: Option<&str>) -> (String, String) {
91 let res = evaluate_with_env(source, file, &mut self.env);
92 let mut out_parts: Vec<String> = Vec::new();
93 for r in &res.results {
94 out_parts.push(format_run_result(r));
95 }
96 let mut err_parts: Vec<String> = Vec::new();
97 for d in &res.diagnostics {
98 err_parts.push(format_diagnostic(d, Some(source)));
99 }
100 (out_parts.join("\n"), err_parts.join("\n"))
101 }
102
103 pub fn feed(&mut self, line: &str) -> ReplStep {
105 let trimmed = line.trim();
106 if trimmed.is_empty() {
107 return ReplStep::default();
108 }
109 if trimmed.starts_with(':') {
110 return self.handle_meta(trimmed);
111 }
112 self.transcript.push(line.to_string());
113 let (output, error) = self.evaluate_source(line, Some("<repl>"));
114 ReplStep {
115 output,
116 error,
117 exit: false,
118 }
119 }
120
121 fn handle_meta(&mut self, line: &str) -> ReplStep {
122 let mut parts = line.splitn(2, char::is_whitespace);
123 let cmd = parts.next().unwrap_or("").trim();
124 let arg = parts.next().unwrap_or("").trim();
125 match cmd {
126 ":help" | ":?" => ReplStep {
127 output: HELP_TEXT.to_string(),
128 ..Default::default()
129 },
130 ":quit" | ":exit" => ReplStep {
131 exit: true,
132 ..Default::default()
133 },
134 ":reset" => {
135 self.reset();
136 ReplStep {
137 output: "Env reset.".to_string(),
138 ..Default::default()
139 }
140 }
141 ":env" => ReplStep {
142 output: format_env(&self.env),
143 ..Default::default()
144 },
145 ":load" => {
146 if arg.is_empty() {
147 return ReplStep {
148 error: ":load requires a file path".to_string(),
149 ..Default::default()
150 };
151 }
152 let path = self.resolve(arg);
153 let text = match fs::read_to_string(&path) {
154 Ok(t) => t,
155 Err(e) => {
156 return ReplStep {
157 error: format!(":load failed: {}", e),
158 ..Default::default()
159 };
160 }
161 };
162 self.transcript.push(format!("# :load {}", arg));
163 self.transcript.push(text.clone());
164 let (output, error) = self.evaluate_source(&text, Some(arg));
165 ReplStep {
166 output,
167 error,
168 exit: false,
169 }
170 }
171 ":save" => {
172 if arg.is_empty() {
173 return ReplStep {
174 error: ":save requires a file path".to_string(),
175 ..Default::default()
176 };
177 }
178 let path = self.resolve(arg);
179 let mut body = self.transcript.join("\n");
180 if !body.ends_with('\n') {
181 body.push('\n');
182 }
183 if let Err(e) = fs::write(&path, body) {
184 return ReplStep {
185 error: format!(":save failed: {}", e),
186 ..Default::default()
187 };
188 }
189 ReplStep {
190 output: format!(
191 "Saved {} entries to {}.",
192 self.transcript.len(),
193 arg
194 ),
195 ..Default::default()
196 }
197 }
198 other => ReplStep {
199 error: format!("Unknown meta-command: {}. Try :help.", other),
200 ..Default::default()
201 },
202 }
203 }
204
205 fn resolve(&self, p: &str) -> PathBuf {
206 let pb = Path::new(p);
207 if pb.is_absolute() || p.starts_with('~') {
208 pb.to_path_buf()
209 } else {
210 self.cwd.join(p)
211 }
212 }
213
214 pub fn completion_candidates(&self, prefix: &str) -> Vec<String> {
218 if prefix.starts_with(':') {
219 let mut hits: Vec<String> = META_COMMANDS
220 .iter()
221 .filter(|c| c.starts_with(prefix))
222 .map(|s| s.to_string())
223 .collect();
224 hits.sort();
225 return hits;
226 }
227 let mut all: Vec<String> = Vec::new();
228 for k in BUILTIN_KEYWORDS {
229 all.push((*k).to_string());
230 }
231 for t in &self.env.terms {
232 all.push(t.clone());
233 }
234 for k in self.env.ops.keys() {
235 all.push(k.clone());
236 }
237 for k in self.env.symbol_prob.keys() {
238 all.push(k.clone());
239 }
240 for k in self.env.lambdas.keys() {
241 all.push(k.clone());
242 }
243 all.sort();
244 all.dedup();
245 if prefix.is_empty() {
246 all
247 } else {
248 all.into_iter().filter(|c| c.starts_with(prefix)).collect()
249 }
250 }
251}
252
253pub fn format_env(env: &Env) -> String {
255 let mut lines: Vec<String> = Vec::new();
256 lines.push(format!("range: [{}, {}]", env.lo, env.hi));
257 let valence_str = if env.valence == 0 {
258 "continuous".to_string()
259 } else {
260 env.valence.to_string()
261 };
262 lines.push(format!("valence: {}", valence_str));
263 if !env.terms.is_empty() {
264 let mut terms: Vec<&String> = env.terms.iter().collect();
265 terms.sort();
266 let names: Vec<String> = terms.iter().map(|s| (*s).clone()).collect();
267 lines.push(format!("terms: {}", names.join(", ")));
268 }
269 if !env.lambdas.is_empty() {
270 let mut keys: Vec<&String> = env.lambdas.keys().collect();
271 keys.sort();
272 let names: Vec<String> = keys.iter().map(|s| (*s).clone()).collect();
273 lines.push(format!("lambdas: {}", names.join(", ")));
274 }
275 if !env.types.is_empty() {
276 lines.push("types:".to_string());
277 let mut entries: Vec<(&String, &String)> = env.types.iter().collect();
278 entries.sort();
279 for (k, v) in entries {
280 lines.push(format!(" {} : {}", k, v));
281 }
282 }
283 if !env.assign.is_empty() {
284 lines.push("assignments:".to_string());
285 let mut entries: Vec<(&String, &f64)> = env.assign.iter().collect();
286 entries.sort_by(|a, b| a.0.cmp(b.0));
287 for (k, v) in entries {
288 lines.push(format!(" {} = {}", k, format_number(*v)));
289 }
290 }
291 let mid = env.mid();
293 let mut user_priors: Vec<(&String, &f64)> = env
294 .symbol_prob
295 .iter()
296 .filter(|(k, v)| {
297 let kk = k.as_str();
298 if kk == "true" {
299 **v != env.hi
300 } else if kk == "false" {
301 **v != env.lo
302 } else if kk == "unknown" || kk == "undefined" {
303 **v != mid
304 } else {
305 true
306 }
307 })
308 .collect();
309 if !user_priors.is_empty() {
310 user_priors.sort_by(|a, b| a.0.cmp(b.0));
311 lines.push("symbol priors:".to_string());
312 for (k, v) in user_priors {
313 lines.push(format!(" {} = {}", k, format_number(*v)));
314 }
315 }
316 lines.join("\n")
317}
318
319pub fn run_repl(
322 env_options: EnvOptions,
323 show_prompt: bool,
324 input: &mut dyn BufRead,
325 output: &mut dyn Write,
326 err_output: &mut dyn Write,
327) -> io::Result<()> {
328 let mut repl = Repl::new(env_options, None);
329 if show_prompt {
330 writeln!(output, "RML REPL. Type :help for commands, :quit to exit.")?;
331 }
332 loop {
333 if show_prompt {
334 write!(output, "rml> ")?;
335 output.flush()?;
336 }
337 let mut line = String::new();
338 let n = input.read_line(&mut line)?;
339 if n == 0 {
340 if show_prompt {
342 writeln!(output)?;
343 }
344 break;
345 }
346 if line.ends_with('\n') {
348 line.pop();
349 if line.ends_with('\r') {
350 line.pop();
351 }
352 }
353 let step = repl.feed(&line);
354 if !step.output.is_empty() {
355 writeln!(output, "{}", step.output)?;
356 }
357 if !step.error.is_empty() {
358 writeln!(err_output, "{}", step.error)?;
359 }
360 if step.exit {
361 break;
362 }
363 }
364 Ok(())
365}
366
367#[cfg(test)]
368mod tests {
369 use super::*;
370
371 #[test]
372 fn preserves_state_across_feeds() {
373 let mut repl = Repl::new(EnvOptions::default(), None);
374 repl.feed("(a: a is a)");
375 repl.feed("((a = a) has probability 1)");
376 let step = repl.feed("(? (a = a))");
377 assert_eq!(step.output, "1");
378 assert!(step.error.is_empty(), "errors: {}", step.error);
379 }
380
381 #[test]
382 fn reset_clears_state() {
383 let mut repl = Repl::new(EnvOptions::default(), None);
384 repl.feed("(a: a is a)");
385 assert!(repl.env.terms.contains("a"));
386 repl.feed(":reset");
387 assert!(!repl.env.terms.contains("a"));
388 assert!(repl.transcript.is_empty());
389 }
390
391 #[test]
392 fn help_emits_help_text() {
393 let mut repl = Repl::new(EnvOptions::default(), None);
394 let step = repl.feed(":help");
395 assert!(step.output.contains(":load"), "got: {}", step.output);
396 }
397
398 #[test]
399 fn unknown_meta_command_reports_error() {
400 let mut repl = Repl::new(EnvOptions::default(), None);
401 let step = repl.feed(":nope");
402 assert!(step.error.contains("Unknown meta-command"));
403 }
404
405 #[test]
406 fn quit_requests_exit() {
407 let mut repl = Repl::new(EnvOptions::default(), None);
408 let step = repl.feed(":quit");
409 assert!(step.exit);
410 }
411
412 #[test]
413 fn completion_offers_terms_after_declaration() {
414 let mut repl = Repl::new(EnvOptions::default(), None);
415 repl.feed("(apple: apple is apple)");
416 let hits = repl.completion_candidates("app");
417 assert!(hits.iter().any(|s| s == "apple"), "hits: {:?}", hits);
418 }
419
420 #[test]
421 fn completion_offers_meta_commands_for_colon_prefix() {
422 let repl = Repl::new(EnvOptions::default(), None);
423 let hits = repl.completion_candidates(":lo");
424 assert!(hits.iter().any(|s| s == ":load"), "hits: {:?}", hits);
425 }
426}