Skip to main content

typr_cli/
repl.rs

1//! Interactive REPL for TypR
2//!
3//! Provides an interactive Read-Eval-Print-Loop with:
4//! - Syntax highlighting
5//! - Line editing with rustyline
6//! - History support
7//! - Type inference display
8
9use crate::io::execute_r_with_path2;
10use crate::project::{write_header, write_to_r_lang};
11use rustyline::completion::Completer;
12use rustyline::error::ReadlineError;
13use rustyline::highlight::CmdKind;
14use rustyline::highlight::Highlighter;
15use rustyline::hint::Hinter;
16use rustyline::validate::Validator;
17use rustyline::Helper;
18use rustyline::{Config, Editor};
19use std::borrow::Cow;
20use std::fs;
21use std::fs::OpenOptions;
22use std::io::Write;
23use std::path::PathBuf;
24use typr_core::components::context::config::Environment;
25use typr_core::components::context::Context;
26use typr_core::utils::fluent_parser::FluentParser;
27
28// ANSI color codes
29mod colors {
30    pub const RESET: &str = "\x1b[0m";
31    pub const KEYWORD: &str = "\x1b[35m"; // Magenta for keywords
32    pub const FUNCTION: &str = "\x1b[36m"; // Cyan for functions
33    pub const STRING: &str = "\x1b[32m"; // Green for strings
34    pub const NUMBER: &str = "\x1b[33m"; // Yellow for numbers
35    pub const COMMENT: &str = "\x1b[90m"; // Gray for comments
36    pub const OPERATOR: &str = "\x1b[37m"; // White for operators
37    pub const BRACKET: &str = "\x1b[93m"; // Light yellow for brackets
38    pub const ERROR: &str = "\x1b[91m"; // Red for errors
39    pub const OUTPUT: &str = "\x1b[34m"; // Blue for output
40}
41
42/// Highlighter for R/TypR language
43#[derive(Clone)]
44struct RHighlighter;
45
46impl RHighlighter {
47    fn new() -> Self {
48        RHighlighter
49    }
50
51    fn is_r_keyword(word: &str) -> bool {
52        matches!(
53            word,
54            "if" | "else"
55                | "while"
56                | "for"
57                | "in"
58                | "repeat"
59                | "break"
60                | "next"
61                | "function"
62                | "return"
63                | "TRUE"
64                | "FALSE"
65                | "true"
66                | "false"
67                | "NULL"
68                | "NA"
69                | "NaN"
70                | "Inf"
71                | "library"
72                | "require"
73                | "source"
74                | "let"
75                | "type"
76                | "fn"
77        )
78    }
79
80    fn is_r_function(word: &str) -> bool {
81        matches!(
82            word,
83            "print"
84                | "cat"
85                | "paste"
86                | "paste0"
87                | "length"
88                | "sum"
89                | "mean"
90                | "median"
91                | "sd"
92                | "var"
93                | "min"
94                | "max"
95                | "range"
96                | "c"
97                | "list"
98                | "data.frame"
99                | "matrix"
100                | "array"
101                | "factor"
102                | "as.numeric"
103                | "as.character"
104                | "as.logical"
105                | "str"
106                | "summary"
107                | "head"
108                | "tail"
109                | "dim"
110                | "nrow"
111                | "ncol"
112                | "names"
113                | "colnames"
114                | "rownames"
115                | "seq"
116                | "rep"
117                | "sort"
118                | "order"
119                | "unique"
120                | "table"
121                | "subset"
122                | "merge"
123                | "rbind"
124                | "cbind"
125                | "apply"
126                | "lapply"
127                | "sapply"
128                | "tapply"
129        )
130    }
131
132    fn highlight_code(code: &str) -> String {
133        let mut result = String::new();
134        let mut chars = code.chars().peekable();
135        let mut in_string = false;
136        let mut string_delim = ' ';
137        let mut in_comment = false;
138        let mut current_word = String::new();
139
140        while let Some(ch) = chars.next() {
141            // Handle comments
142            if ch == '#' && !in_string {
143                in_comment = true;
144                if !current_word.is_empty() {
145                    result.push_str(&Self::colorize_word(&current_word));
146                    current_word.clear();
147                }
148                result.push_str(colors::COMMENT);
149                result.push(ch);
150                continue;
151            }
152
153            if in_comment {
154                result.push(ch);
155                if ch == '\n' {
156                    result.push_str(colors::RESET);
157                    in_comment = false;
158                }
159                continue;
160            }
161
162            // Handle strings
163            if (ch == '"' || ch == '\'') && !in_string {
164                if !current_word.is_empty() {
165                    result.push_str(&Self::colorize_word(&current_word));
166                    current_word.clear();
167                }
168                in_string = true;
169                string_delim = ch;
170                result.push_str(colors::STRING);
171                result.push(ch);
172                continue;
173            }
174
175            if in_string {
176                result.push(ch);
177                if ch == string_delim && chars.peek() != Some(&'\\') {
178                    in_string = false;
179                    result.push_str(colors::RESET);
180                }
181                continue;
182            }
183
184            // Handle numbers
185            if ch.is_numeric() || (ch == '.' && chars.peek().map_or(false, |c| c.is_numeric())) {
186                if !current_word.is_empty() {
187                    result.push_str(&Self::colorize_word(&current_word));
188                    current_word.clear();
189                }
190                result.push_str(colors::NUMBER);
191                result.push(ch);
192                while let Some(&next_ch) = chars.peek() {
193                    if next_ch.is_numeric() || next_ch == '.' || next_ch == 'e' || next_ch == 'E' {
194                        result.push(chars.next().unwrap());
195                    } else {
196                        break;
197                    }
198                }
199                result.push_str(colors::RESET);
200                continue;
201            }
202
203            // Handle operators and delimiters
204            if "+-*/<>=!&|:".contains(ch) {
205                if !current_word.is_empty() {
206                    result.push_str(&Self::colorize_word(&current_word));
207                    current_word.clear();
208                }
209                result.push_str(colors::OPERATOR);
210                result.push(ch);
211                // Handle multi-character operators
212                if let Some(&next_ch) = chars.peek() {
213                    if matches!(
214                        (ch, next_ch),
215                        ('<', '-')
216                            | ('-', '>')
217                            | ('=', '=')
218                            | ('!', '=')
219                            | ('<', '=')
220                            | ('>', '=')
221                            | ('&', '&')
222                            | ('|', '|')
223                    ) {
224                        result.push(chars.next().unwrap());
225                    }
226                }
227                result.push_str(colors::RESET);
228                continue;
229            }
230
231            // Handle parentheses and brackets
232            if "()[]{}".contains(ch) {
233                if !current_word.is_empty() {
234                    result.push_str(&Self::colorize_word(&current_word));
235                    current_word.clear();
236                }
237                result.push_str(colors::BRACKET);
238                result.push(ch);
239                result.push_str(colors::RESET);
240                continue;
241            }
242
243            // Accumulate characters to form words
244            if ch.is_alphanumeric() || ch == '_' || ch == '.' {
245                current_word.push(ch);
246            } else {
247                if !current_word.is_empty() {
248                    result.push_str(&Self::colorize_word(&current_word));
249                    current_word.clear();
250                }
251                result.push(ch);
252            }
253        }
254
255        // Process the last word
256        if !current_word.is_empty() {
257            result.push_str(&Self::colorize_word(&current_word));
258        }
259
260        if in_comment {
261            result.push_str(colors::RESET);
262        }
263
264        result
265    }
266
267    fn colorize_word(word: &str) -> String {
268        if Self::is_r_keyword(word) {
269            format!("{}{}{}", colors::KEYWORD, word, colors::RESET)
270        } else if Self::is_r_function(word) {
271            format!("{}{}{}", colors::FUNCTION, word, colors::RESET)
272        } else {
273            word.to_string()
274        }
275    }
276}
277
278impl Highlighter for RHighlighter {
279    fn highlight<'l>(&self, line: &'l str, _pos: usize) -> Cow<'l, str> {
280        Cow::Owned(Self::highlight_code(line))
281    }
282
283    fn highlight_char(&self, _line: &str, _pos: usize, _cmd_kind: CmdKind) -> bool {
284        true
285    }
286}
287
288impl Hinter for RHighlighter {
289    type Hint = String;
290}
291
292impl Completer for RHighlighter {
293    type Candidate = String;
294}
295
296impl Validator for RHighlighter {}
297
298impl Helper for RHighlighter {}
299
300/// Result of executing an R command
301#[derive(Debug, Clone)]
302pub struct ExecutionResult {
303    pub output: Vec<String>,
304}
305
306/// State of user input
307#[derive(Debug, Clone, Copy, PartialEq)]
308enum InputState {
309    Normal,
310    MultiLine,
311}
312
313/// CLI interface manager with Rustyline
314pub struct CliInterface {
315    editor: Editor<RHighlighter, rustyline::history::DefaultHistory>,
316    input_state: InputState,
317    command_buffer: String,
318    history_file: String,
319}
320
321impl CliInterface {
322    /// Create a new CLI interface
323    pub fn new() -> Result<Self, Box<dyn std::error::Error>> {
324        let config = Config::builder().auto_add_history(true).build();
325
326        let highlighter = RHighlighter::new();
327        let mut editor = Editor::with_config(config)?;
328        editor.set_helper(Some(highlighter));
329
330        // History file
331        let history_file = std::env::var("HOME")
332            .or_else(|_| std::env::var("USERPROFILE"))
333            .map(|home| format!("{}/.r_repl_history", home))
334            .unwrap_or_else(|_| ".r_repl_history".to_string());
335
336        // Load existing history
337        let _ = editor.load_history(&history_file);
338
339        Ok(CliInterface {
340            editor,
341            input_state: InputState::Normal,
342            command_buffer: String::new(),
343            history_file,
344        })
345    }
346
347    /// Display welcome message
348    pub fn show_welcome(&self) {
349        println!(
350            "{}TypR REPL{}: 'exit' to quit",
351            colors::KEYWORD,
352            colors::RESET
353        );
354    }
355
356    /// Read a line with appropriate prompt
357    pub fn read_line(&mut self) -> Result<String, ReadlineError> {
358        let prompt = match self.input_state {
359            InputState::Normal => format!("{}TypR>{} ", colors::KEYWORD, colors::RESET),
360            InputState::MultiLine => format!("{}...{} ", colors::OPERATOR, colors::RESET),
361        };
362
363        self.editor.readline(&prompt)
364    }
365
366    /// Process user input and return a command if complete
367    pub fn process_input(&mut self, input: &str) -> Option<MyCommand> {
368        let trimmed = input.trim();
369
370        // Special commands in normal mode
371        if self.input_state == InputState::Normal {
372            match trimmed {
373                "exit" | "quit" => return Some(MyCommand::Exit),
374                "clear" => return Some(MyCommand::Clear),
375                "" => return Some(MyCommand::Empty),
376                _ => {}
377            }
378        }
379
380        // Multi-line buffer management
381        if self.input_state == InputState::MultiLine {
382            self.command_buffer.push('\n');
383        }
384        self.command_buffer.push_str(trimmed);
385
386        // Check if the command is complete
387        if self.is_command_complete(&self.command_buffer) {
388            let cmd = self.command_buffer.clone();
389            self.command_buffer.clear();
390            self.input_state = InputState::Normal;
391            Some(MyCommand::Execute(cmd))
392        } else {
393            self.input_state = InputState::MultiLine;
394            None
395        }
396    }
397
398    /// Check if a command is complete (all blocks closed)
399    fn is_command_complete(&self, cmd: &str) -> bool {
400        let open_braces = cmd.matches('{').count();
401        let close_braces = cmd.matches('}').count();
402        let open_parens = cmd.matches('(').count();
403        let close_parens = cmd.matches(')').count();
404        let open_brackets = cmd.matches('[').count();
405        let close_brackets = cmd.matches(']').count();
406
407        open_braces == close_braces
408            && open_parens == close_parens
409            && open_brackets == close_brackets
410    }
411
412    /// Display an execution result with colors
413    pub fn display_result(&self, result: &ExecutionResult) {
414        for line in &result.output {
415            println!("{}{}{}", colors::OUTPUT, line, colors::RESET);
416        }
417    }
418
419    /// Display an error message
420    pub fn display_error(&self, error: &str) {
421        eprintln!("{}Error: {}{}", colors::ERROR, error, colors::RESET);
422    }
423
424    /// Clear the screen
425    pub fn clear_screen(&mut self) {
426        self.editor.clear_screen().ok();
427    }
428
429    /// Save history
430    pub fn save_history(&mut self) {
431        if let Err(e) = self.editor.save_history(&self.history_file) {
432            eprintln!("Warning: Unable to save history: {}", e);
433        }
434    }
435
436    /// Reset multi-line state (useful after Ctrl-C)
437    pub fn reset_multiline_state(&mut self) {
438        self.input_state = InputState::Normal;
439        self.command_buffer.clear();
440    }
441}
442
443/// Commands interpreted by the CLI
444#[derive(Debug)]
445pub enum MyCommand {
446    Execute(String),
447    Exit,
448    Clear,
449    Empty,
450}
451
452#[derive(Debug, Clone)]
453struct TypRExecutor {
454    api: FluentParser,
455}
456
457impl TypRExecutor {
458    pub fn new() -> Self {
459        TypRExecutor {
460            api: FluentParser::new().set_context(Context::default()),
461        }
462    }
463
464    fn get_r_code(self, cmd: &str) -> (Self, String, String) {
465        let (r_code, api) = self.api.push(cmd).run().next_r_code().unwrap();
466        let r_type = api.get_last_type().pretty2();
467        let res = Self {
468            api: api.clone(),
469            ..self
470        };
471        let saved_code = format!("{}\n{}", api.get_saved_r_code(), r_code);
472        (res, saved_code, r_type)
473    }
474
475    fn run_r_code(context: Context, r_code: &str, r_type: &str) -> String {
476        let dir = PathBuf::from(".");
477        let r_file_name = ".repl.R";
478        let _ = fs::remove_file(r_file_name);
479        let mut file = OpenOptions::new()
480            .write(true)
481            .create(true)
482            .open(r_file_name)
483            .unwrap();
484        let _ = file.write_all("source('a_std.R')\n".as_bytes());
485        write_header(context, &dir, Environment::Repl);
486        write_to_r_lang(r_code.to_string(), &dir, r_file_name, Environment::Repl);
487        println!("{}{}{}", colors::NUMBER, r_type, colors::RESET);
488        let res = execute_r_with_path2(&dir, r_file_name);
489        res
490    }
491
492    fn execute(self, cmd: &str) -> Result<(Self, ExecutionResult), String> {
493        let (new, r_code, r_type) = Self::get_r_code(self, cmd);
494        let res = Self::run_r_code(new.api.context.clone(), &r_code, &r_type);
495        Ok((new, ExecutionResult { output: vec![res] }))
496    }
497}
498
499/// Main REPL that orchestrates the executor and CLI interface
500pub struct RRepl {
501    executor: TypRExecutor,
502    cli: CliInterface,
503}
504
505impl RRepl {
506    /// Create a new REPL
507    pub fn new() -> Result<Self, Box<dyn std::error::Error>> {
508        let executor = TypRExecutor::new();
509        let cli = CliInterface::new()?;
510
511        Ok(RRepl { executor, cli })
512    }
513
514    /// Run the main REPL loop
515    pub fn run(&mut self) -> Result<(), Box<dyn std::error::Error>> {
516        self.cli.show_welcome();
517
518        loop {
519            match self.cli.read_line() {
520                Ok(line) => {
521                    if let Some(command) = self.cli.process_input(&line) {
522                        match command {
523                            MyCommand::Execute(cmd) => match self.executor.clone().execute(&cmd) {
524                                Ok((executor, result)) => {
525                                    self.executor = executor;
526                                    self.cli.display_result(&result)
527                                }
528                                Err(e) => {
529                                    self.cli.display_error(&format!("Execution failed: {}", e))
530                                }
531                            },
532                            MyCommand::Exit => {
533                                println!("\nexiting...");
534                                break;
535                            }
536                            MyCommand::Clear => {
537                                self.cli.clear_screen();
538                            }
539                            MyCommand::Empty => {
540                                // Empty line, do nothing
541                            }
542                        }
543                    }
544                }
545                Err(ReadlineError::Interrupted) => {
546                    // Ctrl-C - Exit gracefully
547                    println!("\n^C");
548                    self.cli.reset_multiline_state();
549                    println!("exiting...");
550                    break;
551                }
552                Err(ReadlineError::Eof) => {
553                    // Ctrl-D - Exit gracefully
554                    println!("\nexiting...");
555                    break;
556                }
557                Err(err) => {
558                    self.cli.display_error(&format!("Read error: {}", err));
559                    break;
560                }
561            }
562        }
563
564        // Save history before exiting
565        self.cli.save_history();
566
567        Ok(())
568    }
569}
570
571/// Start the REPL
572pub fn start() {
573    match RRepl::new() {
574        Ok(mut repl) => {
575            if let Err(e) = repl.run() {
576                eprintln!("{}REPL error: {}{}", colors::ERROR, e, colors::RESET);
577                std::process::exit(1);
578            }
579        }
580        Err(e) => {
581            eprintln!(
582                "{}Unable to start R process: {}{}",
583                colors::ERROR,
584                e,
585                colors::RESET
586            );
587            eprintln!("   Check that R is installed and in PATH");
588            std::process::exit(1);
589        }
590    }
591}