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 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 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 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 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 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 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
409pub fn repl_base(vm: Engine) -> std::io::Result<()> {
412 let repl = Repl::new(vm);
413
414 repl.run()
415}