commit
8160815e3f
@ -0,0 +1,145 @@ |
||||
//! Interactive read-eval-print loop.
|
||||
//!
|
||||
//! Started when `a` is run with no script path. Each line is compiled and run in
|
||||
//! the same sandboxed Lua state used for scripts, so the full stdlib (including
|
||||
//! the async http/sqlite/os.sleep calls) is available. Evaluation uses the async
|
||||
//! executor, matching how files are run.
|
||||
|
||||
use mlua::prelude::{LuaFunction, LuaMultiValue, LuaTable}; |
||||
use mlua::Lua; |
||||
use rustyline::error::ReadlineError; |
||||
use rustyline::DefaultEditor; |
||||
|
||||
const PROMPT: &str = "a> "; |
||||
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(); |
||||
|
||||
loop { |
||||
let prompt = if buffer.is_empty() { |
||||
PROMPT |
||||
} else { |
||||
CONTINUATION_PROMPT |
||||
}; |
||||
|
||||
match editor.readline(prompt) { |
||||
Ok(line) => { |
||||
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(); |
||||
} |
||||
// Ctrl-D (on an empty line) or end of piped input: quit.
|
||||
Err(ReadlineError::Eof) => break, |
||||
Err(err) => { |
||||
eprintln!("{err}"); |
||||
break; |
||||
} |
||||
} |
||||
} |
||||
|
||||
if let Some(path) = &history_path { |
||||
let _ = editor.save_history(path); |
||||
} |
||||
|
||||
Ok(()) |
||||
} |
||||
Loading…
Reference in new issue