artichoke/
repl.rs

1//! A REPL (read–eval–print–loop) for an Artichoke interpreter.
2//!
3//! The REPL is readline enabled, but does not save history. The REPL supports
4//! multi-line Ruby expressions, CTRL-C to break out of an expression, and can
5//! inspect return values and exception backtraces.
6
7use std::error;
8use std::fmt;
9use std::io;
10use std::sync::PoisonError;
11
12use artichoke_readline::{get_readline_edit_mode, rl_read_init_file};
13use artichoke_repl_history::repl_history_file;
14use rustyline::Editor;
15use rustyline::config::Builder;
16use rustyline::error::ReadlineError;
17use rustyline::history::FileHistory;
18use termcolor::WriteColor;
19
20use crate::backend::state::parser::Context;
21use crate::backtrace;
22use crate::filename::REPL;
23use crate::parser::repl::Parser;
24use crate::prelude::{Parser as _, *};
25
26/// Failed to initialize parser during REPL boot.
27///
28/// The parser is needed to properly enter and exit multi-line editing mode.
29#[derive(Default, Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
30pub struct ParserAllocError {
31    _private: (),
32}
33
34impl ParserAllocError {
35    /// Constructs a new, default `ParserAllocError`.
36    #[must_use]
37    pub const fn new() -> Self {
38        Self { _private: () }
39    }
40}
41
42impl fmt::Display for ParserAllocError {
43    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
44        f.write_str("Failed to initialize Ruby parser")
45    }
46}
47
48impl error::Error for ParserAllocError {}
49
50/// Parser processed too many lines of input.
51#[derive(Default, Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
52pub struct ParserLineCountError {
53    _private: (),
54}
55
56impl ParserLineCountError {
57    /// Constructs a new, default `ParserLineCountError`.
58    #[must_use]
59    pub const fn new() -> Self {
60        Self { _private: () }
61    }
62}
63
64impl fmt::Display for ParserLineCountError {
65    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
66        f.write_str("The interpreter has parsed too many lines and must exit")
67    }
68}
69
70impl error::Error for ParserLineCountError {}
71
72/// Internal fatal parser error.
73///
74/// This is usually an unknown FFI to Rust translation.
75#[derive(Default, Debug, Clone, Copy, Hash, PartialEq, Eq, PartialOrd, Ord)]
76pub struct ParserInternalError {
77    _private: (),
78}
79
80impl ParserInternalError {
81    /// Constructs a new, default `ParserInternalError`.
82    #[must_use]
83    pub const fn new() -> Self {
84        Self { _private: () }
85    }
86}
87
88impl fmt::Display for ParserInternalError {
89    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90        f.write_str("A fatal parsing error occurred")
91    }
92}
93
94impl error::Error for ParserInternalError {}
95
96/// The input loop encountered an unknown error condition.
97#[derive(Debug)]
98struct UnhandledReadlineError(ReadlineError);
99
100impl fmt::Display for UnhandledReadlineError {
101    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102        write!(f, "Unhandled REPL Readline error: {}", self.0)
103    }
104}
105
106impl error::Error for UnhandledReadlineError {
107    fn source(&self) -> Option<&(dyn error::Error + 'static)> {
108        Some(&self.0)
109    }
110}
111
112/// Configuration for the REPL readline prompt.
113#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord)]
114pub struct PromptConfig<'a, 'b, 'c> {
115    /// Basic prompt for start of a new expression.
116    pub simple: &'a str,
117    /// Altered prompt when an expression is not terminated.
118    pub continued: &'b str,
119    /// Prefix for the result of `$expression.inspect`. A newline is printed
120    /// after the Ruby result.
121    pub result_prefix: &'c str,
122}
123
124impl Default for PromptConfig<'_, '_, '_> {
125    fn default() -> Self {
126        Self::new()
127    }
128}
129
130impl PromptConfig<'_, '_, '_> {
131    /// Create a new, default REPL prompt.
132    ///
133    /// # Default configuration
134    ///
135    /// The `PromptConfig` is setup with the following literals:
136    ///
137    /// - `simple`: `>>> `
138    /// - `continued`: `... `
139    /// - `result_prefix`: `=> `
140    ///
141    /// # Examples
142    ///
143    /// ```
144    /// # use artichoke::repl::PromptConfig;
145    /// let config = PromptConfig {
146    ///     simple: ">>> ",
147    ///     continued: "... ",
148    ///     result_prefix: "=> ",
149    /// };
150    /// assert_eq!(config, PromptConfig::new());
151    /// assert_eq!(config, PromptConfig::default());
152    /// ```
153    #[must_use]
154    pub const fn new() -> Self {
155        Self {
156            simple: ">>> ",
157            continued: "... ",
158            result_prefix: "=> ",
159        }
160    }
161}
162
163// Generate a preamble or welcome message when first booting the REPL.
164//
165// The preamble includes the contents of the `RUBY_DESCRIPTION` and
166// `ARTICHOKE_COMPILER_VERSION` constants embedded in the Artichoke Ruby
167// runtime.
168fn preamble(interp: &mut Artichoke) -> Result<String, Error> {
169    let description = interp.eval(b"RUBY_DESCRIPTION")?.try_convert_into_mut::<&str>(interp)?;
170    let compiler = interp
171        .eval(b"ARTICHOKE_COMPILER_VERSION")?
172        .try_convert_into_mut::<&str>(interp)?;
173    let mut buf = String::with_capacity(description.len() + 2 + compiler.len() + 1);
174    buf.push_str(description);
175    buf.push_str("\n[");
176    buf.push_str(compiler);
177    buf.push(']');
178    Ok(buf)
179}
180
181/// Initialize an [`Artichoke`] interpreter for a REPL environment.
182///
183/// This function also prints out the preamble for the environment.
184fn init<W>(interp: &mut Artichoke, mut output: W) -> Result<(), Box<dyn error::Error>>
185where
186    W: io::Write,
187{
188    writeln!(&mut output, "{}", preamble(interp)?)?;
189
190    interp.reset_parser()?;
191    // SAFETY: `REPL` has no NUL bytes (asserted by tests).
192    let context = unsafe { Context::new_unchecked(REPL.to_vec()) };
193    interp.push_context(context)?;
194
195    Ok(())
196}
197
198/// Run a REPL for the [`Artichoke`] interpreter exposed by the
199/// `artichoke-backend` crate.
200///
201/// # Errors
202///
203/// If printing the interpreter copyright or compiler metadata fails, an error
204/// is returned.
205///
206/// If initializing the Ruby parser fails, an error is returned.
207///
208/// If an exception is raised on the interpreter, then an error is returned.
209///
210/// If writing expression results or exception backtraces to stdout and stderr
211/// fails, an error is returned.
212///
213/// If an unhandled readline state is encountered, a fatal error is returned.
214pub fn run<Wout, Werr>(
215    output: Wout,
216    error: Werr,
217    config: Option<PromptConfig<'_, '_, '_>>,
218) -> Result<(), Box<dyn error::Error>>
219where
220    Wout: io::Write,
221    Werr: io::Write + WriteColor,
222{
223    let mut interp = crate::interpreter()?;
224    // All operations using the interpreter must occur behind a function
225    // boundary so we can catch all errors and ensure we call `interp.close()`.
226    //
227    // Allowing the `?` operator to be used in the containing `run` function
228    // would result in a memory leak of the interpreter and its heap.
229    let result = entrypoint(&mut interp, output, error, config);
230    // Cleanup and deallocate.
231    interp.close();
232    result
233}
234
235fn entrypoint<Wout, Werr>(
236    interp: &mut Artichoke,
237    mut output: Wout,
238    error: Werr,
239    config: Option<PromptConfig<'_, '_, '_>>,
240) -> Result<(), Box<dyn error::Error>>
241where
242    Wout: io::Write,
243    Werr: io::Write + WriteColor,
244{
245    // Initialize interpreter and write preamble.
246    init(interp, &mut output)?;
247
248    // Try to parse readline-native inputrc to detect user preference for
249    // `editing-mode`.
250    let mut editor_config = Builder::new();
251    if let Some(inputrc_config) = rl_read_init_file() {
252        if let Some(edit_mode) = get_readline_edit_mode(inputrc_config) {
253            editor_config = editor_config.edit_mode(edit_mode.into());
254        }
255    }
256
257    // Initialize REPL I/O harness.
258    let mut rl =
259        Editor::<Parser<'_>, FileHistory>::with_config(editor_config.build()).map_err(UnhandledReadlineError)?;
260
261    // Set the readline input validator.
262    //
263    // The `Parser` works with the `rustyline::Editor` to determine whether a
264    // line is valid Ruby code using the mruby parser.
265    //
266    // If the code is invalid (for example a code block or string literal is
267    // unterminated), rustyline will switch to multiline editing mode. This
268    // ensures that rustyline only yields valid Ruby code to the `repl_loop`
269    // below.
270    let parser = Parser::new(interp).ok_or_else(ParserAllocError::new)?;
271    rl.set_helper(Some(parser));
272
273    // Attempt to load REPL history from the history file.
274    let hist_file = repl_history_file();
275    if let Some(ref hist_file) = hist_file {
276        // History can fail to load if the file does not exist and is a
277        // non-blocking error.
278        let _ignored = rl.load_history(hist_file);
279    }
280
281    // Run the REPL until the user exits.
282    let result = repl_loop(&mut rl, output, error, &config.unwrap_or_default());
283
284    // Attempt to save history to the REPL history file.
285    if let Some(ref hist_file) = hist_file {
286        // Saving history is not critical and should not abort the REPL if it
287        // fails.
288        let _ignored = rl.save_history(hist_file);
289    }
290
291    result
292}
293
294fn repl_loop<Wout, Werr>(
295    rl: &mut Editor<Parser<'_>, FileHistory>,
296    mut output: Wout,
297    mut error: Werr,
298    config: &PromptConfig<'_, '_, '_>,
299) -> Result<(), Box<dyn error::Error>>
300where
301    Wout: io::Write,
302    Werr: io::Write + WriteColor,
303{
304    loop {
305        let readline = rl.readline(config.simple);
306        match readline {
307            Ok(input) if input.is_empty() => {}
308            // simulate `Kernel#exit`.
309            Ok(input) if input == "exit" || input == "exit()" => {
310                rl.add_history_entry(input)?;
311                break;
312            }
313            Ok(input) => {
314                // scope lock and borrows of the readline editor to a function
315                // call to facilitate unlocking and unborrowing.
316                eval_single_input(rl, &mut output, &mut error, config, &input)?;
317                rl.add_history_entry(input)?;
318            }
319            // Reset and present the user with a fresh prompt.
320            Err(ReadlineError::Interrupted) => {
321                writeln!(output, "^C")?;
322            }
323            // Gracefully exit on CTRL-D EOF
324            Err(ReadlineError::Eof) => break,
325            Err(err) => return Err(Box::new(UnhandledReadlineError(err))),
326        };
327    }
328    Ok(())
329}
330
331fn eval_single_input<Wout, Werr>(
332    rl: &mut Editor<Parser<'_>, FileHistory>,
333    mut output: Wout,
334    error: Werr,
335    config: &PromptConfig<'_, '_, '_>,
336    input: &str,
337) -> Result<(), Box<dyn error::Error>>
338where
339    Wout: io::Write,
340    Werr: io::Write + WriteColor,
341{
342    let parser = rl.helper().ok_or_else(ParserAllocError::new)?;
343    let mut lock = parser.inner.lock().unwrap_or_else(PoisonError::into_inner);
344    let interp = lock.interp();
345
346    match interp.eval(input.as_bytes()) {
347        // As of IRB v1.10.0 (included in Ruby v3.3.0), users can omit return
348        // value inspection by ending an input with `;`.
349        //
350        // See:https://railsatscale.com/2023-12-19-irb-for-ruby-3-3/#omitting-return-value-inspection-with-
351        //
352        // # Example
353        //
354        // ```console
355        // irb(main):001> long_string = "foo" * 10000;
356        // irb(main):002> long_string.size
357        // => 30000
358        // ```
359        Ok(_) if input.bytes().last() == Some(b';') => {}
360        // Return value inspection: print a `=> ` and the value of `_.inspect`
361        // after evaluating the given input.
362        //
363        // # Example
364        //
365        // ```
366        // [3.2.2] > s = "abc"
367        // => "abc"
368        // ```
369        Ok(value) => {
370            let result = value.inspect(interp);
371            output.write_all(config.result_prefix.as_bytes())?;
372            output.write_all(result.as_slice())?;
373            output.write_all(b"\n")?;
374        }
375        Err(ref exc) => backtrace::format_repl_trace_into(error, interp, exc)?,
376    }
377
378    interp
379        .add_fetch_lineno(input.lines().count())
380        .map_err(|_| ParserLineCountError::new())?;
381
382    // Eval successful, so reset the REPL state for the next expression.
383    interp.incremental_gc()?;
384
385    Ok(())
386}