Lua runner with rich builtin stdlib
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.
 
 
a/src/repl.rs

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(())
}