1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
//! A REPL (read–eval–print–loop) for an mruby interpreter exposed by the
//! [`mruby`] crate.
//!
//! The REPL is readline enabled, but does not save history. The REPL supports
//! multi-line Ruby expressions, CTRL-C to break out of an expression, and can
//! inspect return values and exception backtraces.

use mruby::eval::{EvalContext, MrbEval};
use mruby::gc::MrbGarbageCollection;
use mruby::sys;
use mruby::MrbError;
use rustyline::error::ReadlineError;
use rustyline::Editor;
use std::io::{self, Write};

use crate::parser::{self, Parser, State};

const REPL_FILENAME: &str = "(rirb)";

/// REPL errors.
#[derive(Debug)]
pub enum Error {
    /// Fatal error.
    Fatal,
    /// Could not initialize REPL.
    ReplInit,
    /// Unrecoverable [`Parser`] error.
    ReplParse(parser::Error),
    /// Unrecoverable [`MrbError`]. [`MrbError::Exec`] are handled gracefully
    /// by the REPL. All other `MrbError`s are fatal.
    Ruby(MrbError),
    /// IO error when writing to output or error streams.
    Io(io::Error),
}

/// Configuration for the REPL readline prompt.
pub struct PromptConfig {
    /// Basic prompt for start of a new expression.
    pub simple: String,
    /// Altered prompt when an expression is not terminated.
    pub continued: String,
    /// Prefix for the result of `$expression.inspect`. A newline is printed
    /// after the Ruby result.
    pub result_prefix: String,
}

impl Default for PromptConfig {
    fn default() -> Self {
        Self {
            simple: ">>> ".to_owned(),
            continued: "... ".to_owned(),
            result_prefix: "=> ".to_owned(),
        }
    }
}

fn preamble() -> Result<String, Error> {
    let mut buf = String::new();
    let metadata = rustc_version::version_meta().map_err(|_| Error::ReplInit)?;
    buf.push_str(sys::mruby_sys_version(true).as_str());
    buf.push('\n');
    buf.push('[');
    buf.push_str(format!("Compiled with rustc {}", metadata.semver).as_str());
    if let Some(mut commit) = metadata.commit_hash {
        commit.truncate(7);
        buf.push(' ');
        buf.push_str(commit.as_str());
    }
    if let Some(date) = metadata.commit_date {
        buf.push(' ');
        buf.push_str(date.as_str());
    }
    buf.push(']');
    Ok(buf)
}

/// Run a REPL for the mruby interpreter exposed by the `mruby` crate.
pub fn run(
    mut output: impl Write,
    mut error: impl Write,
    config: Option<PromptConfig>,
) -> Result<(), Error> {
    writeln!(output, "{}", preamble()?).map_err(Error::Io)?;
    let config = config.unwrap_or_else(Default::default);
    let interp = mruby::interpreter().map_err(Error::Ruby)?;

    // load gems
    mruby_gems::rubygems::rack::init(&interp).map_err(Error::Ruby)?;
    mruby_gems::rubygems::mustermann::init(&interp).map_err(Error::Ruby)?;

    let parser = Parser::new(&interp).ok_or(Error::ReplInit)?;
    interp.push_context(EvalContext::new(REPL_FILENAME));
    unsafe {
        let api = interp.borrow();
        (*api.ctx).lineno = 1;
    }

    let mut rl = Editor::<()>::new();
    // If a code block is open, accumulate code from multiple readlines in this
    // mutable `String` buffer.
    let mut buf = String::new();
    let mut parser_state = State::default();
    loop {
        // Allow shell users to identify that they have an open code block.
        let prompt = if parser_state.is_code_block_open() {
            config.continued.as_str()
        } else {
            config.simple.as_str()
        };

        let readline = rl.readline(prompt);
        match readline {
            Ok(line) => {
                buf.push_str(line.as_str());
                parser_state = parser.parse(buf.as_str()).map_err(Error::ReplParse)?;
                if parser_state.is_code_block_open() {
                    buf.push('\n');
                    continue;
                }
                match interp.eval(buf.as_str()) {
                    Ok(value) => writeln!(output, "{}{}", config.result_prefix, value.inspect())
                        .map_err(Error::Io)?,
                    Err(MrbError::Exec(backtrace)) => {
                        writeln!(error, "Backtrace:").map_err(Error::Io)?;
                        for frame in backtrace.lines() {
                            writeln!(error, "    {}", frame).map_err(Error::Io)?;
                        }
                    }
                    Err(err) => return Err(Error::Ruby(err)),
                }
                for line in buf.lines() {
                    rl.add_history_entry(line);
                    unsafe {
                        let api = interp.borrow();
                        (*api.ctx).lineno += 1;
                    }
                }
                // mruby eval successful, so reset the REPL state for the
                // next expression.
                interp.incremental_gc();
                buf.clear();
            }
            // Reset the buf and present the user with a fresh prompt
            Err(ReadlineError::Interrupted) => {
                // Reset buffered code
                buf.clear();
                // clear parser state
                parser_state = State::default();
                writeln!(output, "^C").map_err(Error::Io)?;
                continue;
            }
            // Gracefully exit on CTRL-D EOF
            Err(ReadlineError::Eof) => break,
            Err(_) => return Err(Error::Fatal),
        };
    }
    Ok(())
}