parent
ec35dc911b
commit
27e7fc66b2
@ -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