//! 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::("utils")? .get::("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> { 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::(()).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(()) }