steel_repl/
repl.rs

1extern crate rustyline;
2use colored::{ColoredString, Colorize};
3use rustyline::history::FileHistory;
4use rustyline::{
5    Cmd, ConditionalEventHandler, Event, EventContext, EventHandler, KeyEvent, RepeatCount,
6};
7use steel::compiler::modules::steel_home;
8use steel::rvals::{Custom, SteelString};
9
10use std::borrow::Cow;
11use std::error::Error;
12use std::fmt::{Debug, Display};
13use std::sync::atomic::{AtomicBool, Ordering};
14use std::sync::mpsc::channel;
15use std::sync::{Arc, Mutex};
16
17use rustyline::error::ReadlineError;
18
19use rustyline::{config::Configurer, Editor};
20
21use std::path::{Path, PathBuf};
22use steel::{rvals::SteelVal, steel_vm::register_fn::RegisterFn};
23
24use steel::steel_vm::engine::Engine;
25
26use std::io::Read;
27
28use std::time::Instant;
29
30use std::env;
31
32use std::fs::File;
33
34use crate::highlight::RustylineHelper;
35
36fn display_help() {
37    println!(
38        "
39        :time       -- toggles the timing of expressions
40        :? | :help  -- displays help dialog
41        :q | :quit  -- exits the REPL
42        :pwd        -- displays the current working directory
43        :load       -- loads a file
44        "
45    );
46}
47
48fn get_default_startup() -> ColoredString {
49    format!(
50        r#"
51     _____ __            __
52    / ___// /____  ___  / /          Version {} 
53    \__ \/ __/ _ \/ _ \/ /           https://github.com/mattwparas/steel
54   ___/ / /_/  __/  __/ /            :? for help
55  /____/\__/\___/\___/_/
56    "#,
57        env!("CARGO_PKG_VERSION")
58    )
59    .bright_yellow()
60    .bold()
61}
62
63fn get_default_repl_history_path() -> PathBuf {
64    if let Some(val) = steel_home() {
65        let mut parsed_path = PathBuf::from(&val);
66        parsed_path = parsed_path.canonicalize().unwrap_or(parsed_path);
67        parsed_path.push("history");
68        parsed_path
69    } else {
70        let mut default_path = env_home::env_home_dir().unwrap_or_default();
71        default_path.push(".steel/history");
72        default_path.to_string_lossy().into_owned();
73        default_path
74    }
75}
76
77fn finish_load_or_interrupt(vm: &mut Engine, exprs: String, path: PathBuf) {
78    // let file_name = path.to_str().unwrap().to_string();
79
80    let res = vm.compile_and_run_raw_program_with_path(exprs, path);
81
82    match res {
83        Ok(r) => r.into_iter().for_each(|x| match x {
84            SteelVal::Void => {}
85            SteelVal::StringV(s) => {
86                println!("{} {:?}", "=>".bright_blue().bold(), s);
87            }
88            _ => {
89                print!("{} ", "=>".bright_blue().bold());
90                vm.call_function_by_name_with_args("displayln", vec![x])
91                    .unwrap();
92            }
93        }),
94        Err(e) => {
95            vm.raise_error(e);
96        }
97    }
98}
99
100fn finish_or_interrupt(vm: &mut Engine, line: String) {
101    let values = match vm.compile_and_run_raw_program(line) {
102        Ok(values) => values,
103        Err(error) => {
104            vm.raise_error(error);
105
106            return;
107        }
108    };
109
110    let len = values.len();
111
112    for (i, value) in values.into_iter().enumerate() {
113        let last = i == len - 1;
114
115        if last {
116            vm.register_value("$1", value.clone());
117        }
118
119        match value {
120            SteelVal::Void => {}
121            SteelVal::StringV(s) => {
122                println!("{} {:?}", "=>".bright_blue().bold(), s);
123            }
124            _ => {
125                print!("{} ", "=>".bright_blue().bold());
126                vm.call_function_by_name_with_args("displayln", vec![value])
127                    .unwrap();
128            }
129        }
130    }
131}
132
133#[derive(Debug)]
134struct RustyLine(Editor<RustylineHelper, FileHistory>);
135impl Custom for RustyLine {}
136
137#[derive(Debug)]
138#[allow(unused)]
139struct RustyLineError(rustyline::error::ReadlineError);
140
141impl Custom for RustyLineError {}
142
143impl std::fmt::Display for RustyLineError {
144    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
145        write!(f, "{:?}", self)
146    }
147}
148
149impl Error for RustyLineError {}
150
151pub fn readline_module(vm: &mut Engine) {
152    let mut module = steel::steel_vm::builtin::BuiltInModule::new("#%private/steel/readline");
153
154    module
155        .register_fn("#%repl-display-startup", || {
156            println!("{}", get_default_startup())
157        })
158        .register_fn(
159            "#%repl-add-history-entry",
160            |rl: &mut RustyLine, entry: SteelString| rl.0.add_history_entry(entry.as_str()).ok(),
161        )
162        .register_fn("#%create-repl", || {
163            let mut rl = Editor::<RustylineHelper, rustyline::history::DefaultHistory>::new()
164                .expect("Unable to instantiate the repl!");
165            rl.set_check_cursor_position(true);
166
167            let history_path = get_default_repl_history_path();
168            if let Err(_) = rl.load_history(&history_path) {
169                if let Err(_) = File::create(&history_path) {
170                    eprintln!("Unable to create repl history file {:?}", history_path)
171                }
172            };
173            RustyLine(rl)
174        })
175        .register_fn("#%read-line", |rl: &mut RustyLine| {
176            let prompt = format!("{}", "λ > ".bright_green().bold().italic());
177            rl.0.readline(&prompt).map_err(RustyLineError)
178        });
179
180    vm.register_module(module);
181}
182
183struct CtrlCHandler {
184    close_on_interrupt: Arc<AtomicBool>,
185    empty_line_cancelled: AtomicBool,
186}
187
188impl CtrlCHandler {
189    fn new(close_on_interrupt: Arc<AtomicBool>) -> Self {
190        CtrlCHandler {
191            close_on_interrupt,
192            empty_line_cancelled: AtomicBool::new(false),
193        }
194    }
195}
196
197impl ConditionalEventHandler for CtrlCHandler {
198    fn handle(&self, _: &Event, _: RepeatCount, _: bool, ctx: &EventContext) -> Option<Cmd> {
199        if !ctx.line().is_empty() {
200            // if the line is not empty, reset the PREVIOUS_LINE_CANCELLED state
201            self.empty_line_cancelled.store(false, Ordering::Release);
202        } else if self.empty_line_cancelled.swap(true, Ordering::Release) {
203            self.close_on_interrupt.store(true, Ordering::Release);
204        }
205
206        Some(Cmd::Interrupt)
207    }
208}
209
210pub struct Repl<S: Display, P: AsRef<Path> + Debug> {
211    vm: Engine,
212    startup_text: Option<S>,
213    history_path: Option<P>,
214}
215
216impl Repl<String, PathBuf> {
217    pub fn new(vm: Engine) -> Self {
218        Repl {
219            vm,
220            startup_text: None,
221            history_path: None,
222        }
223    }
224}
225
226impl<S: Display, P: AsRef<Path> + Debug> Repl<S, P> {
227    pub fn with_startup<NS: Display>(self, startup_text: NS) -> Repl<NS, P> {
228        Repl {
229            vm: self.vm,
230            history_path: self.history_path,
231            startup_text: Some(startup_text),
232        }
233    }
234
235    pub fn with_history_path<NP: AsRef<Path> + Debug>(self, history_path: NP) -> Repl<S, NP> {
236        Repl {
237            vm: self.vm,
238            history_path: Some(history_path),
239            startup_text: self.startup_text,
240        }
241    }
242
243    pub fn run(mut self) -> std::io::Result<()> {
244        if let Some(startup) = self.startup_text {
245            println!("{}", startup);
246        } else {
247            println!("{}", get_default_startup());
248        }
249
250        #[cfg(target_os = "windows")]
251        let mut prompt = String::from("λ > ");
252
253        #[cfg(not(target_os = "windows"))]
254        let mut prompt = format!("{}", "λ > ".bright_green().bold().italic());
255
256        let mut rl = Editor::<RustylineHelper, rustyline::history::DefaultHistory>::new()
257            .expect("Unable to instantiate the repl!");
258        rl.set_check_cursor_position(true);
259
260        // Load repl history
261        let history_path: Cow<Path> = self
262            .history_path
263            .as_ref()
264            .map(|p| Cow::Borrowed(p.as_ref()))
265            .unwrap_or_else(|| Cow::Owned(get_default_repl_history_path()));
266
267        if let Err(_) = rl.load_history(&history_path) {
268            if let Err(_) = File::create(&history_path) {
269                eprintln!("Unable to create repl history file {:?}", history_path)
270            }
271        };
272
273        let current_dir = std::env::current_dir()?;
274
275        let mut print_time = false;
276
277        let (tx, rx) = channel();
278        let tx = std::sync::Mutex::new(tx);
279
280        let cancellation_function = move || {
281            tx.lock().unwrap().send(()).unwrap();
282        };
283
284        self.vm.register_fn("quit", cancellation_function);
285        let safepoint = self.vm.get_thread_state_controller();
286
287        let globals = Arc::new(Mutex::new(self.vm.globals().iter().copied().collect()));
288
289        rl.set_helper(Some(RustylineHelper::new(globals.clone())));
290
291        let safepoint = safepoint.clone();
292        let ctrlc_safepoint = safepoint.clone();
293
294        ctrlc::set_handler(move || {
295            ctrlc_safepoint.clone().interrupt();
296        })
297        .unwrap();
298
299        let clear_interrupted = move || {
300            safepoint.resume();
301        };
302
303        let close_on_interrupt = Arc::new(AtomicBool::new(false));
304        let ctrlc = Box::new(CtrlCHandler::new(close_on_interrupt.clone()));
305        rl.bind_sequence(KeyEvent::ctrl('c'), EventHandler::Conditional(ctrlc));
306
307        while rx.try_recv().is_err() {
308            // Update globals for highlighting
309            // TODO: Come up with some kind of subscription API?
310            let known_globals_length = globals.lock().unwrap().len();
311            let updated_globals_length = self.vm.globals().len();
312            if updated_globals_length > known_globals_length {
313                let mut guard = globals.lock().unwrap();
314                if let Some(range) = self.vm.globals().get(known_globals_length..) {
315                    for var in range {
316                        guard.insert(*var);
317                    }
318                }
319            }
320
321            let readline = self.vm.enter_safepoint(|| rl.readline(&prompt));
322
323            match readline {
324                Ok(line) => {
325                    rl.add_history_entry(line.as_str()).ok();
326                    match line.as_str().trim() {
327                        ":q" | ":quit" => return Ok(()),
328                        ":time" => {
329                            print_time = !print_time;
330                            println!(
331                                "{} {}",
332                                "Expression timer set to:".bright_purple(),
333                                print_time.to_string().bright_green()
334                            );
335                        }
336                        ":pwd" => println!("{current_dir:#?}"),
337                        ":?" | ":help" => display_help(),
338                        line if line.contains(":load") => {
339                            let line = line.trim_start_matches(":load").trim();
340                            if line.is_empty() {
341                                eprintln!("No file provided");
342                                continue;
343                            }
344
345                            let path = Path::new(line);
346
347                            let file = std::fs::File::open(path);
348
349                            if let Err(e) = file {
350                                eprintln!("{e}");
351                                continue;
352                            }
353
354                            // Update the prompt to now include the new context
355                            prompt = format!(
356                                "{}",
357                                format!("λ ({line}) > ").bright_green().bold().italic(),
358                            );
359
360                            let mut file = file?;
361
362                            let mut exprs = String::new();
363                            file.read_to_string(&mut exprs)?;
364
365                            clear_interrupted();
366
367                            finish_load_or_interrupt(&mut self.vm, exprs, path.to_path_buf());
368                        }
369                        _ => {
370                            // TODO also include this for loading files
371                            let now = Instant::now();
372
373                            clear_interrupted();
374
375                            finish_or_interrupt(&mut self.vm, line);
376
377                            if print_time {
378                                println!("Time taken: {:?}", now.elapsed());
379                            }
380                        }
381                    }
382                }
383                Err(ReadlineError::Interrupted) => {
384                    if close_on_interrupt.load(Ordering::Acquire) {
385                        break;
386                    } else {
387                        println!("CTRL-C");
388                        continue;
389                    }
390                }
391                Err(ReadlineError::Eof) => {
392                    println!("CTRL-D");
393                    break;
394                }
395                Err(err) => {
396                    println!("Error: {err:?}");
397                    break;
398                }
399            }
400        }
401        if let Err(err) = rl.save_history(&history_path) {
402            eprintln!("Failed to save REPL history: {}", err);
403        }
404
405        Ok(())
406    }
407}
408
409/// Entry point for the repl
410/// Automatically adds the prelude and contracts for the core library
411pub fn repl_base(vm: Engine) -> std::io::Result<()> {
412    let repl = Repl::new(vm);
413
414    repl.run()
415}