You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
156 lines
5.3 KiB
156 lines
5.3 KiB
//! Interactive read-eval-print loop.
|
|
//!
|
|
//! Each line is compiled and run in exactly the same way scripts are run,
|
|
//! keeping the Lua state between lines.
|
|
|
|
use mlua::prelude::{LuaFunction, LuaMultiValue, LuaTable};
|
|
use mlua::Lua;
|
|
use rustyline::error::ReadlineError;
|
|
use rustyline::DefaultEditor;
|
|
|
|
const PROMPT: &str = ">>> ";
|
|
const CONTINUATION_PROMPT: &str = "..> ";
|
|
|
|
/// Outcome of trying to compile a piece of REPL input.
|
|
enum Compiled {
|
|
/// A ready-to-run chunk.
|
|
Chunk(LuaFunction),
|
|
/// Input is syntactically incomplete; more lines are needed.
|
|
Incomplete,
|
|
/// Input is invalid and cannot become valid by appending more lines.
|
|
Error(mlua::Error),
|
|
}
|
|
|
|
/// Compile a complete piece of REPL input.
|
|
///
|
|
/// Mirrors stock Lua's `lua.c`: first try the input wrapped in `return ( ... )`
|
|
/// so that bare expressions print their value, and fall back to running it as a
|
|
/// statement. Only the statement form's error decides whether the input is
|
|
/// merely incomplete (so a half-typed expression keeps reading more lines).
|
|
fn compile(lua: &Lua, source: &str) -> Compiled {
|
|
if let Ok(func) = lua
|
|
.load(format!("return {source}"))
|
|
.set_name("=stdin")
|
|
.into_function()
|
|
{
|
|
return Compiled::Chunk(func);
|
|
}
|
|
|
|
match lua.load(source).set_name("=stdin").into_function() {
|
|
Ok(func) => Compiled::Chunk(func),
|
|
Err(mlua::Error::SyntaxError {
|
|
incomplete_input: true,
|
|
..
|
|
}) => Compiled::Incomplete,
|
|
Err(err) => Compiled::Error(err),
|
|
}
|
|
}
|
|
|
|
/// Pretty-print the results of an evaluated chunk, one per line, using the
|
|
/// project's own `utils.dump`. Nothing is printed when the chunk returns no
|
|
/// values (e.g. a plain statement).
|
|
fn print_results(lua: &Lua, values: LuaMultiValue) -> mlua::Result<()> {
|
|
if values.is_empty() {
|
|
return Ok(());
|
|
}
|
|
let dump: LuaFunction = lua
|
|
.globals()
|
|
.get::<LuaTable>("utils")?
|
|
.get::<LuaFunction>("dump")?;
|
|
for value in values {
|
|
let rendered: String = dump.call(value)?;
|
|
println!("{rendered}");
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Run the interactive REPL against an already-sandboxed Lua state.
|
|
pub(crate) async fn run(lua: &Lua) -> Result<(), Box<dyn std::error::Error>> {
|
|
let mut editor = DefaultEditor::new()?;
|
|
let history_path = std::env::var_os("HOME").map(|home| {
|
|
let mut p = std::path::PathBuf::from(home);
|
|
p.push(".a_history");
|
|
p
|
|
});
|
|
if let Some(path) = &history_path {
|
|
// A missing history file on first run is not an error.
|
|
let _ = editor.load_history(path);
|
|
}
|
|
|
|
println!(
|
|
"a {} — interactive Lua. Ctrl-D to exit.",
|
|
env!("CARGO_PKG_VERSION")
|
|
);
|
|
|
|
// Accumulated lines of an as-yet-incomplete statement.
|
|
let mut buffer = String::new();
|
|
// Guards against an unbroken stream of read errors spinning forever (e.g. a
|
|
// reader that keeps failing without ever reaching EOF).
|
|
let mut consecutive_errors = 0u32;
|
|
|
|
loop {
|
|
let prompt = if buffer.is_empty() {
|
|
PROMPT
|
|
} else {
|
|
CONTINUATION_PROMPT
|
|
};
|
|
|
|
match editor.readline(prompt) {
|
|
Ok(line) => {
|
|
consecutive_errors = 0;
|
|
if buffer.is_empty() {
|
|
buffer = line;
|
|
} else {
|
|
buffer.push('\n');
|
|
buffer.push_str(&line);
|
|
}
|
|
|
|
match compile(lua, &buffer) {
|
|
Compiled::Incomplete => continue, // keep reading continuation lines
|
|
Compiled::Chunk(func) => {
|
|
let _ = editor.add_history_entry(buffer.as_str());
|
|
match func.call_async::<LuaMultiValue>(()).await {
|
|
Ok(values) => {
|
|
if let Err(err) = print_results(lua, values) {
|
|
eprintln!("{err}");
|
|
}
|
|
}
|
|
Err(err) => eprintln!("{err}"),
|
|
}
|
|
buffer.clear();
|
|
}
|
|
Compiled::Error(err) => {
|
|
let _ = editor.add_history_entry(buffer.as_str());
|
|
eprintln!("{err}");
|
|
buffer.clear();
|
|
}
|
|
}
|
|
}
|
|
// Ctrl-C: abandon the current input and start fresh.
|
|
Err(ReadlineError::Interrupted) => {
|
|
buffer.clear();
|
|
consecutive_errors = 0;
|
|
}
|
|
// Ctrl-D (on an empty line) or end of piped input: quit.
|
|
Err(ReadlineError::Eof) => break,
|
|
// Any other read error (e.g. a non-UTF-8 input line) is reported but
|
|
// does not end the session — a stray pasted byte shouldn't kill the
|
|
// REPL. Bail only if errors keep coming with no successful read in
|
|
// between, so a permanently broken stream can't spin forever.
|
|
Err(err) => {
|
|
eprintln!("{err}");
|
|
buffer.clear();
|
|
consecutive_errors += 1;
|
|
if consecutive_errors >= 100 {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if let Some(path) = &history_path {
|
|
let _ = editor.save_history(path);
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|