Skip to main content

oxilean_parse/repl_parser/
functions.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use super::types::{ExtendedReplCommand, ReplCommand, ReplEvent, ReplInputKind};
6
7/// Check if input is a complete statement.
8///
9/// Returns `true` when all brackets (`()`, `[]`, `{}`) are balanced and
10/// no depths have gone negative (which would indicate a syntax error in
11/// the input so far).
12#[allow(missing_docs)]
13pub fn is_complete(input: &str) -> bool {
14    let mut paren_depth: i32 = 0;
15    let mut bracket_depth: i32 = 0;
16    let mut brace_depth: i32 = 0;
17    for ch in input.chars() {
18        match ch {
19            '(' => paren_depth += 1,
20            ')' => {
21                paren_depth -= 1;
22                if paren_depth < 0 {
23                    return false;
24                }
25            }
26            '[' => bracket_depth += 1,
27            ']' => {
28                bracket_depth -= 1;
29                if bracket_depth < 0 {
30                    return false;
31                }
32            }
33            '{' => brace_depth += 1,
34            '}' => {
35                brace_depth -= 1;
36                if brace_depth < 0 {
37                    return false;
38                }
39            }
40            _ => {}
41        }
42    }
43    paren_depth == 0 && bracket_depth == 0 && brace_depth == 0
44}
45#[cfg(test)]
46mod tests {
47    use super::*;
48    use crate::repl_parser::*;
49    #[test]
50    fn test_parse_quit() {
51        let parser = ReplParser::new(":quit".to_string());
52        let cmd = parser.parse().expect("parsing should succeed");
53        assert_eq!(cmd, ReplCommand::Quit);
54    }
55    #[test]
56    fn test_parse_quit_short() {
57        let parser = ReplParser::new(":q".to_string());
58        let cmd = parser.parse().expect("parsing should succeed");
59        assert_eq!(cmd, ReplCommand::Quit);
60    }
61    #[test]
62    fn test_parse_help() {
63        let parser = ReplParser::new(":help".to_string());
64        let cmd = parser.parse().expect("parsing should succeed");
65        assert_eq!(cmd, ReplCommand::Help);
66    }
67    #[test]
68    fn test_parse_show_env() {
69        let parser = ReplParser::new(":env".to_string());
70        let cmd = parser.parse().expect("parsing should succeed");
71        assert_eq!(cmd, ReplCommand::ShowEnv);
72    }
73    #[test]
74    fn test_parse_clear() {
75        let parser = ReplParser::new(":clear".to_string());
76        let cmd = parser.parse().expect("parsing should succeed");
77        assert_eq!(cmd, ReplCommand::Clear);
78    }
79    #[test]
80    fn test_parse_load() {
81        let parser = ReplParser::new(":load test.lean".to_string());
82        let cmd = parser.parse().expect("parsing should succeed");
83        assert!(matches!(cmd, ReplCommand::Load(_)));
84    }
85    #[test]
86    fn test_is_complete_balanced() {
87        assert!(is_complete("(1 + 2)"));
88        assert!(is_complete("{x : Nat}"));
89        assert!(is_complete("[1, 2, 3]"));
90        assert!(is_complete("simp only [h1, h2]"));
91    }
92    #[test]
93    fn test_is_complete_unbalanced() {
94        assert!(!is_complete("(1 + 2"));
95        assert!(!is_complete("{x : Nat"));
96        assert!(!is_complete("[1, 2, 3"));
97    }
98    #[test]
99    fn test_is_complete_negative_depth() {
100        assert!(!is_complete(")foo"));
101        assert!(!is_complete("]foo"));
102        assert!(!is_complete("}foo"));
103    }
104    #[test]
105    fn test_parse_eval() {
106        let parser = ReplParser::new("42".to_string());
107        let cmd = parser.parse().expect("parsing should succeed");
108        assert!(matches!(cmd, ReplCommand::Eval(_)));
109    }
110}
111/// Parse an extended REPL command from a string slice.
112///
113/// Extended commands include `:set`, `:get`, `:history`, `:undo`,
114/// `:reduce`, `:stats`, `:search`, and `:print`.
115#[allow(missing_docs)]
116pub fn parse_extended_command(
117    cmd: &str,
118    rest: &str,
119) -> Result<Option<ExtendedReplCommand>, String> {
120    match cmd {
121        "set" => {
122            let parts: Vec<&str> = rest.splitn(2, char::is_whitespace).collect();
123            if parts.len() < 2 {
124                return Err("Usage: :set <option> <value>".to_string());
125            }
126            Ok(Some(ExtendedReplCommand::SetOption(
127                parts[0].trim().to_string(),
128                parts[1].trim().to_string(),
129            )))
130        }
131        "get" => {
132            let name = rest.trim();
133            if name.is_empty() {
134                return Err("Usage: :get <option>".to_string());
135            }
136            Ok(Some(ExtendedReplCommand::GetOption(name.to_string())))
137        }
138        "history" | "hist" => Ok(Some(ExtendedReplCommand::History)),
139        "undo" | "u" => Ok(Some(ExtendedReplCommand::Undo)),
140        "stats" => Ok(Some(ExtendedReplCommand::Stats)),
141        "search" | "s" => {
142            let pattern = rest.trim();
143            if pattern.is_empty() {
144                return Err("Usage: :search <pattern>".to_string());
145            }
146            Ok(Some(ExtendedReplCommand::Search(pattern.to_string())))
147        }
148        "print" | "p" => {
149            let name = rest.trim();
150            if name.is_empty() {
151                return Err("Usage: :print <name>".to_string());
152            }
153            Ok(Some(ExtendedReplCommand::Print(name.to_string())))
154        }
155        _ => Ok(None),
156    }
157}
158/// Provide command completions for a given prefix.
159///
160/// Returns a list of possible completions sorted alphabetically.
161#[allow(missing_docs)]
162pub fn complete_command(prefix: &str) -> Vec<String> {
163    let commands = [
164        ":quit", ":q", ":exit", ":help", ":h", ":?", ":env", ":show", ":clear", ":reset", ":type",
165        ":t", ":load", ":l", ":check", ":c", ":set", ":get", ":history", ":hist", ":undo", ":u",
166        ":stats", ":search", ":s", ":print", ":p", ":reduce",
167    ];
168    let mut completions: Vec<String> = commands
169        .iter()
170        .filter(|cmd| cmd.starts_with(prefix))
171        .map(|s| s.to_string())
172        .collect();
173    completions.sort();
174    completions
175}
176/// Return the help text for the REPL.
177#[allow(missing_docs)]
178pub fn help_text() -> &'static str {
179    "OxiLean REPL Commands:
180  <expr>          Evaluate an expression
181  theorem/def     Elaborate a declaration
182  :type <expr>    Show type of expression        (:t)
183  :check <decl>   Check a declaration            (:c)
184  :load <file>    Load a file                    (:l)
185  :env            Show environment               (:show)
186  :clear          Clear environment              (:reset)
187  :set <opt> <v>  Set a REPL option
188  :get <opt>      Get a REPL option value
189  :history        Show command history           (:hist)
190  :undo           Undo last declaration          (:u)
191  :stats          Show elaboration statistics
192  :search <pat>   Search for declarations        (:s)
193  :print <name>   Print declaration info         (:p)
194  :reduce <expr>  Print normal form of expr
195  :help           Show this help                 (:h, :?)
196  :quit           Exit REPL                      (:q, :exit)"
197}
198/// Normalize input by trimming and collapsing internal whitespace.
199#[allow(missing_docs)]
200pub fn normalize_input(input: &str) -> String {
201    input.split_whitespace().collect::<Vec<_>>().join(" ")
202}
203/// Check if an input string is a meta-command (starts with `:`).
204#[allow(missing_docs)]
205pub fn is_meta_command(input: &str) -> bool {
206    input.trim_start().starts_with(':')
207}
208/// Check if an input string is empty or only whitespace.
209#[allow(missing_docs)]
210pub fn is_empty_input(input: &str) -> bool {
211    input.trim().is_empty()
212}
213/// Parse a boolean option value.
214///
215/// Accepts `"true"`, `"false"`, `"yes"`, `"no"`, `"1"`, `"0"`.
216#[allow(missing_docs)]
217pub fn parse_bool(s: &str) -> Result<bool, String> {
218    match s.trim().to_lowercase().as_str() {
219        "true" | "yes" | "1" | "on" => Ok(true),
220        "false" | "no" | "0" | "off" => Ok(false),
221        other => Err(format!("Expected boolean, got '{}'", other)),
222    }
223}
224#[cfg(test)]
225mod extended_repl_tests {
226    use super::*;
227    use crate::repl_parser::*;
228    #[test]
229    fn test_repl_options_default() {
230        let opts = ReplOptions::default();
231        assert!(opts.print_types);
232        assert!(!opts.show_timing);
233        assert_eq!(opts.max_history, 100);
234    }
235    #[test]
236    fn test_repl_options_set_get() {
237        let mut opts = ReplOptions::new();
238        opts.set("timing", "true")
239            .expect("test operation should succeed");
240        assert_eq!(opts.get("timing"), Some("true".to_string()));
241        opts.set("timing", "false")
242            .expect("test operation should succeed");
243        assert_eq!(opts.get("timing"), Some("false".to_string()));
244    }
245    #[test]
246    fn test_repl_options_unknown_option() {
247        let mut opts = ReplOptions::new();
248        let result = opts.set("nonexistent", "value");
249        assert!(result.is_err());
250    }
251    #[test]
252    fn test_command_history_push() {
253        let mut h = CommandHistory::new(10);
254        h.push("foo".to_string());
255        h.push("bar".to_string());
256        assert_eq!(h.len(), 2);
257    }
258    #[test]
259    fn test_command_history_no_duplicates() {
260        let mut h = CommandHistory::new(10);
261        h.push("foo".to_string());
262        h.push("foo".to_string());
263        assert_eq!(h.len(), 1);
264    }
265    #[test]
266    fn test_command_history_search() {
267        let mut h = CommandHistory::new(10);
268        h.push("theorem foo : Nat".to_string());
269        h.push("def bar : Type".to_string());
270        h.push("theorem baz : Prop".to_string());
271        let results = h.search("theorem");
272        assert_eq!(results.len(), 2);
273    }
274    #[test]
275    fn test_multiline_state_complete() {
276        let mut m = MultilineState::new();
277        m.push_line("(1 + 2)");
278        assert!(m.is_complete());
279    }
280    #[test]
281    fn test_multiline_state_incomplete() {
282        let mut m = MultilineState::new();
283        m.push_line("(1 + 2");
284        assert!(!m.is_complete());
285    }
286    #[test]
287    fn test_multiline_state_reset() {
288        let mut m = MultilineState::new();
289        m.push_line("(foo");
290        m.reset();
291        assert_eq!(m.depth(), 0);
292        assert_eq!(m.line_count(), 0);
293    }
294    #[test]
295    fn test_complete_command() {
296        let completions = complete_command(":q");
297        assert!(completions.contains(&":quit".to_string()));
298        assert!(completions.contains(&":q".to_string()));
299    }
300    #[test]
301    fn test_complete_command_help() {
302        let completions = complete_command(":h");
303        assert!(!completions.is_empty());
304        assert!(completions.iter().any(|c| c.starts_with(":h")));
305    }
306    #[test]
307    fn test_parse_bool_true_variants() {
308        assert_eq!(parse_bool("true"), Ok(true));
309        assert_eq!(parse_bool("yes"), Ok(true));
310        assert_eq!(parse_bool("1"), Ok(true));
311        assert_eq!(parse_bool("on"), Ok(true));
312    }
313    #[test]
314    fn test_parse_bool_false_variants() {
315        assert_eq!(parse_bool("false"), Ok(false));
316        assert_eq!(parse_bool("no"), Ok(false));
317        assert_eq!(parse_bool("0"), Ok(false));
318        assert_eq!(parse_bool("off"), Ok(false));
319    }
320    #[test]
321    fn test_parse_bool_invalid() {
322        assert!(parse_bool("maybe").is_err());
323        assert!(parse_bool("").is_err());
324    }
325    #[test]
326    fn test_is_meta_command() {
327        assert!(is_meta_command(":quit"));
328        assert!(is_meta_command("  :help"));
329        assert!(!is_meta_command("42"));
330        assert!(!is_meta_command("def foo"));
331    }
332    #[test]
333    fn test_is_empty_input() {
334        assert!(is_empty_input(""));
335        assert!(is_empty_input("   "));
336        assert!(!is_empty_input("x"));
337    }
338    #[test]
339    fn test_normalize_input() {
340        let n = normalize_input("  foo   bar  baz ");
341        assert_eq!(n, "foo bar baz");
342    }
343    #[test]
344    fn test_help_text_nonempty() {
345        assert!(!help_text().is_empty());
346        assert!(help_text().contains(":quit"));
347        assert!(help_text().contains(":help"));
348    }
349    #[test]
350    fn test_repl_stats_success_rate() {
351        let mut stats = ReplStats::new();
352        stats.record_success();
353        stats.record_success();
354        stats.record_error();
355        let rate = stats.success_rate();
356        assert!((rate - 2.0 / 3.0).abs() < 1e-10);
357    }
358    #[test]
359    fn test_repl_session_process_quit() {
360        let mut session = ReplSession::new();
361        let cmd = session
362            .process(":quit")
363            .expect("test operation should succeed");
364        assert_eq!(cmd, ReplCommand::Quit);
365    }
366    #[test]
367    fn test_repl_session_history_tracking() {
368        let mut session = ReplSession::new();
369        let _ = session.process(":help");
370        let _ = session.process(":quit");
371        assert_eq!(session.history.len(), 2);
372    }
373    #[test]
374    fn test_parse_extended_set() {
375        let result =
376            parse_extended_command("set", "timing true").expect("test operation should succeed");
377        assert!(matches!(result, Some(ExtendedReplCommand::SetOption(_, _))));
378    }
379    #[test]
380    fn test_parse_extended_history() {
381        let result = parse_extended_command("history", "").expect("test operation should succeed");
382        assert!(matches!(result, Some(ExtendedReplCommand::History)));
383    }
384    #[test]
385    fn test_parse_extended_search() {
386        let result =
387            parse_extended_command("search", "Nat").expect("test operation should succeed");
388        assert!(matches!(result, Some(ExtendedReplCommand::Search(s)) if s == "Nat"));
389    }
390    #[test]
391    fn test_parse_extended_unknown() {
392        let result =
393            parse_extended_command("nonexistent", "").expect("test operation should succeed");
394        assert!(result.is_none());
395    }
396}
397/// Format a REPL prompt based on state.
398#[allow(missing_docs)]
399pub fn format_prompt(depth: i32, session_num: u64) -> String {
400    if depth > 0 {
401        format!("  {}> ", "  ".repeat(depth as usize))
402    } else {
403        format!("oxilean[{}]> ", session_num)
404    }
405}
406/// Syntax-highlight a short token for REPL display.
407///
408/// Returns the token with ANSI escape codes if `use_color` is `true`.
409#[allow(missing_docs)]
410pub fn highlight_keyword(token: &str, use_color: bool) -> String {
411    if !use_color {
412        return token.to_string();
413    }
414    let keywords = [
415        "theorem",
416        "def",
417        "axiom",
418        "inductive",
419        "fun",
420        "let",
421        "in",
422        "match",
423        "with",
424    ];
425    if keywords.contains(&token) {
426        format!("\x1b[1;34m{}\x1b[0m", token)
427    } else {
428        token.to_string()
429    }
430}
431#[cfg(test)]
432mod repl_extended_tests {
433    use super::*;
434    use crate::repl_parser::*;
435    #[test]
436    fn test_repl_option_from_name() {
437        assert_eq!(ReplOption::from_name("timing"), Some(ReplOption::Timing));
438        assert_eq!(ReplOption::from_name("verbose"), Some(ReplOption::Verbose));
439        assert_eq!(ReplOption::from_name("unknown"), None);
440    }
441    #[test]
442    fn test_repl_option_name() {
443        assert_eq!(ReplOption::Timing.name(), "timing");
444        assert_eq!(ReplOption::PrettyPrint.name(), "pretty_print");
445    }
446    #[test]
447    fn test_option_store_set_get() {
448        let mut store = OptionStore::new();
449        store.set("timing", "true");
450        assert_eq!(store.get("timing"), Some("true"));
451        assert!(store.has("timing"));
452    }
453    #[test]
454    fn test_option_store_get_bool() {
455        let mut store = OptionStore::new();
456        store.set("verbose", "yes");
457        assert!(store.get_bool("verbose"));
458        assert!(!store.get_bool("timing"));
459    }
460    #[test]
461    fn test_option_store_get_u64() {
462        let mut store = OptionStore::new();
463        store.set("max_lines", "50");
464        assert_eq!(store.get_u64("max_lines", 100), 50);
465        assert_eq!(store.get_u64("other", 42), 42);
466    }
467    #[test]
468    fn test_option_store_remove() {
469        let mut store = OptionStore::new();
470        store.set("k", "v");
471        assert!(store.remove("k"));
472        assert!(!store.has("k"));
473    }
474    #[test]
475    fn test_option_store_len() {
476        let mut store = OptionStore::new();
477        store.set("a", "1");
478        store.set("b", "2");
479        assert_eq!(store.len(), 2);
480    }
481    #[test]
482    fn test_input_splitter_complete() {
483        let mut s = InputSplitter::new();
484        s.push("(1 + 2)");
485        assert!(s.is_complete());
486    }
487    #[test]
488    fn test_input_splitter_incomplete() {
489        let mut s = InputSplitter::new();
490        s.push("(1 + 2");
491        assert!(!s.is_complete());
492    }
493    #[test]
494    fn test_input_splitter_flush() {
495        let mut s = InputSplitter::new();
496        s.push("hello");
497        let out = s.flush();
498        assert_eq!(out, "hello");
499        assert!(s.is_empty());
500    }
501    #[test]
502    fn test_input_splitter_multiline() {
503        let mut s = InputSplitter::new();
504        s.push("fun x =>");
505        s.push("  x + 1");
506        assert_eq!(s.line_count(), 2);
507        let out = s.flush();
508        assert!(out.contains("fun x =>"));
509        assert!(out.contains("x + 1"));
510    }
511    #[test]
512    fn test_format_prompt_toplevel() {
513        let p = format_prompt(0, 1);
514        assert!(p.contains("oxilean[1]"));
515    }
516    #[test]
517    fn test_format_prompt_nested() {
518        let p = format_prompt(2, 5);
519        assert!(p.contains(">"));
520        assert!(!p.contains("oxilean"));
521    }
522    #[test]
523    fn test_highlight_keyword_no_color() {
524        let s = highlight_keyword("theorem", false);
525        assert_eq!(s, "theorem");
526    }
527    #[test]
528    fn test_highlight_keyword_with_color() {
529        let s = highlight_keyword("theorem", true);
530        assert!(s.contains("theorem"));
531        assert!(s.contains("\x1b["));
532    }
533    #[test]
534    fn test_highlight_non_keyword() {
535        let s = highlight_keyword("myFunc", true);
536        assert_eq!(s, "myFunc");
537    }
538}
539/// A REPL event listener trait.
540#[allow(dead_code)]
541#[allow(missing_docs)]
542pub trait ReplEventListener {
543    /// Called when a REPL event occurs.
544    fn on_event(&mut self, event: &ReplEvent);
545}
546/// A REPL filter that preprocesses input before parsing.
547#[allow(dead_code)]
548#[allow(missing_docs)]
549pub trait InputFilter {
550    /// Filter/transform the input string.
551    fn filter(&self, input: &str) -> String;
552}
553/// Count words in an input string.
554#[allow(dead_code)]
555#[allow(missing_docs)]
556pub fn word_count(input: &str) -> usize {
557    input.split_whitespace().count()
558}
559/// Check if a line starts a tactic block (starts with `by`).
560#[allow(dead_code)]
561#[allow(missing_docs)]
562pub fn is_tactic_block_start(input: &str) -> bool {
563    let trimmed = input.trim();
564    trimmed == "by" || trimmed.starts_with("by ") || trimmed.starts_with("by\t")
565}
566/// Extract command name and rest from a meta-command string.
567#[allow(dead_code)]
568#[allow(missing_docs)]
569pub fn split_meta_command(cmd: &str) -> (&str, &str) {
570    let parts: Vec<&str> = cmd.splitn(2, char::is_whitespace).collect();
571    let name = parts.first().copied().unwrap_or("");
572    let rest = parts.get(1).copied().unwrap_or("").trim_start();
573    (name, rest)
574}
575/// Determine if a REPL command is "safe" (non-destructive).
576#[allow(dead_code)]
577#[allow(missing_docs)]
578pub fn is_safe_command(cmd: &ReplCommand) -> bool {
579    matches!(
580        cmd,
581        ReplCommand::Eval(_) | ReplCommand::Type(_) | ReplCommand::Help | ReplCommand::ShowEnv
582    )
583}
584/// Produce a one-line summary of a REPL command.
585#[allow(dead_code)]
586#[allow(missing_docs)]
587pub fn command_summary(cmd: &ReplCommand) -> &'static str {
588    match cmd {
589        ReplCommand::Eval(_) => "evaluate expression",
590        ReplCommand::Type(_) => "show type",
591        ReplCommand::Check(_) => "check declaration",
592        ReplCommand::Load(_) => "load file",
593        ReplCommand::ShowEnv => "show environment",
594        ReplCommand::Clear => "clear environment",
595        ReplCommand::Help => "show help",
596        ReplCommand::Quit => "quit REPL",
597    }
598}
599/// Extended option names.
600#[allow(dead_code)]
601#[allow(missing_docs)]
602pub fn all_option_names() -> Vec<&'static str> {
603    vec![
604        "timing",
605        "types",
606        "color",
607        "verbose",
608        "history",
609        "max_lines",
610        "pretty_print",
611    ]
612}
613/// Suggest an option name for a misspelled option (Levenshtein heuristic).
614#[allow(dead_code)]
615#[allow(missing_docs)]
616pub fn suggest_option(input: &str) -> Option<&'static str> {
617    let names = all_option_names();
618    names
619        .into_iter()
620        .min_by_key(|name| simple_distance(input, name))
621        .filter(|name| simple_distance(input, name) <= 3)
622}
623/// A simple character-level distance function.
624#[allow(dead_code)]
625pub(super) fn simple_distance(a: &str, b: &str) -> usize {
626    let a: Vec<char> = a.chars().collect();
627    let b: Vec<char> = b.chars().collect();
628    let m = a.len();
629    let n = b.len();
630    let mut dp = vec![vec![0usize; n + 1]; m + 1];
631    for (i, row) in dp.iter_mut().enumerate().take(m + 1) {
632        row[0] = i;
633    }
634    for (j, cell) in dp[0].iter_mut().enumerate().take(n + 1) {
635        *cell = j;
636    }
637    for i in 1..=m {
638        for j in 1..=n {
639            if a[i - 1] == b[j - 1] {
640                dp[i][j] = dp[i - 1][j - 1];
641            } else {
642                dp[i][j] = 1 + dp[i - 1][j].min(dp[i][j - 1]).min(dp[i - 1][j - 1]);
643            }
644        }
645    }
646    dp[m][n]
647}
648#[cfg(test)]
649mod repl_extra_tests {
650    use super::*;
651    use crate::repl_parser::*;
652    #[test]
653    fn test_repl_mode_name() {
654        assert_eq!(ReplMode::Normal.name(), "normal");
655        assert_eq!(ReplMode::Tactic.name(), "tactic");
656    }
657    #[test]
658    fn test_repl_mode_from_name() {
659        assert_eq!(ReplMode::from_name("tactic"), Some(ReplMode::Tactic));
660        assert_eq!(ReplMode::from_name("unknown"), None);
661    }
662    #[test]
663    fn test_repl_mode_display() {
664        assert_eq!(format!("{}", ReplMode::Search), "search");
665    }
666    #[test]
667    fn test_repl_event_display() {
668        let e = ReplEvent::Parsed("foo".to_string());
669        let s = format!("{}", e);
670        assert!(s.contains("foo"));
671    }
672    #[test]
673    fn test_event_log_listener() {
674        let mut log = EventLog::new();
675        log.on_event(&ReplEvent::Reset);
676        log.on_event(&ReplEvent::Exit);
677        assert_eq!(log.len(), 2);
678    }
679    #[test]
680    fn test_event_log_clear() {
681        let mut log = EventLog::new();
682        log.on_event(&ReplEvent::Reset);
683        log.clear();
684        assert!(log.is_empty());
685    }
686    #[test]
687    fn test_command_alias_new() {
688        let a = CommandAlias::new("q", ":quit");
689        assert_eq!(a.from, "q");
690        assert_eq!(a.to, ":quit");
691    }
692    #[test]
693    fn test_alias_registry_expand() {
694        let mut reg = AliasRegistry::new();
695        reg.register("quit", ":quit");
696        assert_eq!(reg.expand("quit"), ":quit");
697        assert_eq!(reg.expand("help"), "help");
698    }
699    #[test]
700    fn test_alias_registry_names() {
701        let mut reg = AliasRegistry::new();
702        reg.register("a", "b");
703        reg.register("c", "d");
704        let names = reg.names();
705        assert!(names.contains(&"a"));
706        assert!(names.contains(&"c"));
707    }
708    #[test]
709    fn test_strip_semicolon_filter() {
710        let f = StripSemicolonFilter;
711        assert_eq!(f.filter("foo;"), "foo");
712        assert_eq!(f.filter("foo"), "foo");
713    }
714    #[test]
715    fn test_lowercase_command_filter() {
716        let f = LowercaseCommandFilter;
717        let out = f.filter(":QUIT");
718        assert!(out.starts_with(":quit"));
719    }
720    #[test]
721    fn test_filter_pipeline_apply() {
722        let mut pipeline = FilterPipeline::new();
723        pipeline.add(StripSemicolonFilter);
724        let result = pipeline.apply("foo;");
725        assert_eq!(result, "foo");
726    }
727    #[test]
728    fn test_filter_pipeline_len() {
729        let mut pipeline = FilterPipeline::new();
730        assert_eq!(pipeline.len(), 0);
731        pipeline.add(StripSemicolonFilter);
732        assert_eq!(pipeline.len(), 1);
733    }
734    #[test]
735    fn test_repl_completer_complete() {
736        let completer = ReplCompleter::new();
737        let results = completer.complete(":q");
738        assert!(!results.is_empty());
739        assert!(results.iter().any(|c| c.text == ":quit"));
740    }
741    #[test]
742    fn test_repl_completer_add() {
743        let mut completer = ReplCompleter::new();
744        let initial = completer.len();
745        completer.add(CompletionItem::new(
746            "myCmd",
747            "custom",
748            CompletionKind::Command,
749        ));
750        assert_eq!(completer.len(), initial + 1);
751    }
752    #[test]
753    fn test_repl_formatter_success() {
754        let fmt = ReplFormatter::new(false, 80);
755        let s = fmt.success("all good");
756        assert!(s.contains("all good"));
757    }
758    #[test]
759    fn test_repl_formatter_error() {
760        let fmt = ReplFormatter::new(false, 80);
761        let s = fmt.error("bad input");
762        assert!(s.contains("bad input"));
763    }
764    #[test]
765    fn test_repl_formatter_truncate() {
766        let fmt = ReplFormatter::new(false, 5);
767        let s = fmt.truncate("hello world");
768        assert!(s.len() <= 6);
769    }
770    #[test]
771    fn test_repl_formatter_list() {
772        let fmt = ReplFormatter::new(false, 80);
773        let items = vec!["foo".to_string(), "bar".to_string()];
774        let s = fmt.list(&items);
775        assert!(s.contains("foo"));
776        assert!(s.contains("bar"));
777    }
778    #[test]
779    fn test_configurable_repl_parser_normalize() {
780        let config = ReplParserConfig::new();
781        let p = ConfigurableReplParser::new(config);
782        let out = p.preprocess("  foo   bar  ");
783        assert_eq!(out, "  foo   bar  ");
784    }
785    #[test]
786    fn test_configurable_repl_parser_with_normalize() {
787        let config = ReplParserConfig {
788            normalize_whitespace: true,
789            ..Default::default()
790        };
791        let p = ConfigurableReplParser::new(config);
792        let out = p.preprocess("  foo   bar  ");
793        assert_eq!(out, "foo bar");
794    }
795    #[test]
796    fn test_configurable_repl_parser_strip_comments() {
797        let config = ReplParserConfig {
798            strip_comments: true,
799            ..Default::default()
800        };
801        let p = ConfigurableReplParser::new(config);
802        let out = p.preprocess("def foo -- this is a comment");
803        assert!(!out.contains("comment"));
804    }
805    #[test]
806    fn test_command_tally_record() {
807        let mut tally = CommandTally::new();
808        tally.record(&ReplCommand::Help);
809        tally.record(&ReplCommand::Quit);
810        assert_eq!(tally.helps, 1);
811        assert_eq!(tally.quits, 1);
812        assert_eq!(tally.total(), 2);
813    }
814    #[test]
815    fn test_word_count() {
816        assert_eq!(word_count("hello world foo"), 3);
817        assert_eq!(word_count(""), 0);
818        assert_eq!(word_count("  one  "), 1);
819    }
820    #[test]
821    fn test_is_tactic_block_start() {
822        assert!(is_tactic_block_start("by"));
823        assert!(is_tactic_block_start("  by intro h"));
824        assert!(!is_tactic_block_start("theorem foo"));
825    }
826    #[test]
827    fn test_split_meta_command() {
828        let (cmd, rest) = split_meta_command("set timing true");
829        assert_eq!(cmd, "set");
830        assert_eq!(rest, "timing true");
831    }
832    #[test]
833    fn test_split_meta_command_no_rest() {
834        let (cmd, rest) = split_meta_command("history");
835        assert_eq!(cmd, "history");
836        assert_eq!(rest, "");
837    }
838    #[test]
839    fn test_is_safe_command() {
840        assert!(is_safe_command(&ReplCommand::Help));
841        assert!(is_safe_command(&ReplCommand::ShowEnv));
842        assert!(!is_safe_command(&ReplCommand::Clear));
843        assert!(!is_safe_command(&ReplCommand::Quit));
844    }
845    #[test]
846    fn test_command_summary() {
847        assert_eq!(command_summary(&ReplCommand::Help), "show help");
848        assert_eq!(command_summary(&ReplCommand::Quit), "quit REPL");
849    }
850    #[test]
851    fn test_suggest_option_close() {
852        let s = suggest_option("timng");
853        assert!(s.is_some());
854        assert_eq!(s.expect("test operation should succeed"), "timing");
855    }
856    #[test]
857    fn test_suggest_option_no_match() {
858        let s = suggest_option("zzzzzzz");
859        let _ = s;
860    }
861    #[test]
862    fn test_all_option_names_nonempty() {
863        let names = all_option_names();
864        assert!(!names.is_empty());
865        assert!(names.contains(&"timing"));
866        assert!(names.contains(&"verbose"));
867    }
868    #[test]
869    fn test_repl_session_default() {
870        let session = ReplSession::default();
871        assert!(session.history.is_empty());
872        assert_eq!(session.stats.commands_run, 0);
873    }
874    #[test]
875    fn test_option_store_keys() {
876        let mut store = OptionStore::new();
877        store.set("a", "1");
878        store.set("b", "2");
879        let keys = store.keys();
880        assert_eq!(keys.len(), 2);
881    }
882    #[test]
883    fn test_command_history_prev() {
884        let mut h = CommandHistory::new(10);
885        h.push("first".to_string());
886        h.push("second".to_string());
887        let prev = h.prev();
888        assert_eq!(prev, Some("second"));
889    }
890    #[test]
891    fn test_command_history_max() {
892        let mut h = CommandHistory::new(2);
893        h.push("a".to_string());
894        h.push("b".to_string());
895        h.push("c".to_string());
896        assert_eq!(h.len(), 2);
897        assert_eq!(h.entries()[0], "b");
898    }
899    #[test]
900    fn test_simple_distance_equal() {
901        assert_eq!(simple_distance("hello", "hello"), 0);
902    }
903    #[test]
904    fn test_simple_distance_insert() {
905        assert_eq!(simple_distance("", "a"), 1);
906    }
907    #[test]
908    fn test_simple_distance_substitute() {
909        assert_eq!(simple_distance("cat", "bat"), 1);
910    }
911}
912#[cfg(test)]
913mod repl_history_tests {
914    use super::*;
915    use crate::repl_parser::*;
916    #[test]
917    fn test_repl_history() {
918        let mut h = ReplHistory::new();
919        h.push("#check Nat", true);
920        h.push("bad input!", false);
921        assert_eq!(h.len(), 2);
922        assert!(h.entries[0].parse_ok);
923        assert!(!h.entries[1].parse_ok);
924    }
925}
926/// Classify a REPL input string.
927#[allow(dead_code)]
928#[allow(missing_docs)]
929pub fn classify_repl_input(input: &str) -> ReplInputKind {
930    let trimmed = input.trim();
931    if trimmed.starts_with("def ")
932        || trimmed.starts_with("theorem ")
933        || trimmed.starts_with("lemma ")
934    {
935        ReplInputKind::Definition
936    } else if trimmed.starts_with('#') {
937        ReplInputKind::Command
938    } else if trimmed.starts_with("by ") || trimmed.ends_with(":= by") {
939        ReplInputKind::Tactic
940    } else if trimmed.starts_with("fun ")
941        || trimmed.starts_with("let ")
942        || trimmed.starts_with("match ")
943    {
944        ReplInputKind::Term
945    } else if trimmed.ends_with('(') || trimmed.ends_with(',') || trimmed.ends_with("->") {
946        ReplInputKind::Incomplete
947    } else {
948        ReplInputKind::Term
949    }
950}
951#[cfg(test)]
952mod repl_classify_tests {
953    use super::*;
954    use crate::repl_parser::*;
955    #[test]
956    fn test_classify_repl_input() {
957        assert_eq!(
958            classify_repl_input("def foo := 1"),
959            ReplInputKind::Definition
960        );
961        assert_eq!(classify_repl_input("#check Nat"), ReplInputKind::Command);
962        assert_eq!(classify_repl_input("1 + 2"), ReplInputKind::Term);
963        assert_eq!(classify_repl_input("fun x ->"), ReplInputKind::Term);
964    }
965}
966/// Returns true if a REPL input looks like an incomplete expression.
967#[allow(dead_code)]
968#[allow(missing_docs)]
969pub fn is_incomplete_repl_input(s: &str) -> bool {
970    let t = s.trim();
971    t.is_empty() || t.ends_with('(') || t.ends_with(',') || t.ends_with("->") || t.ends_with(':')
972}
973#[cfg(test)]
974mod repl_pad {
975    use super::*;
976    use crate::repl_parser::*;
977    #[test]
978    fn test_is_incomplete_repl_input() {
979        assert!(is_incomplete_repl_input("fun x ->"));
980        assert!(!is_incomplete_repl_input("fun x -> x"));
981        assert!(is_incomplete_repl_input(""));
982    }
983}