use std::io::{self, BufRead, Write};
use crate::vm::machine::VM;
use crate::vm::value::Value;
pub fn is_incomplete_input(err: &str) -> bool {
err.contains("Unfinished")
|| err.contains("unfinished")
|| err.contains("got \"\"")
|| (err.contains("expected ") && !err.contains("got \""))
}
fn panic_message(payload: Box<dyn std::any::Any + Send>) -> String {
if let Some(s) = payload.downcast_ref::<String>() {
s.clone()
} else if let Some(s) = payload.downcast_ref::<&str>() {
s.to_string()
} else {
"internal error".to_string()
}
}
pub fn eval_chunk(vm: &mut VM, source: &str) -> Result<Option<String>, String> {
let run = || -> Result<Option<String>, String> {
let proto =
crate::compiler::compile_program(source, "repl", crate::compiler::Options::UNCHECKED)?;
let val = vm.execute(proto)?;
crate::vm::libs::task::run_due(vm);
Ok(match val {
Value::Nil => None,
v => Some(v.to_string()),
})
};
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(run)) {
Ok(res) => res,
Err(payload) => Err(panic_message(payload)),
}
}
use crate::parser::readline::ReadLineEngine;
pub fn run_repl() -> Result<(), String> {
let stdin = io::stdin();
let mut out = io::stdout();
let mut vm = VM::new();
let mut rl = ReadLineEngine::new();
loop {
write!(out, "{}", rl.prompt()).map_err(|e| format!("output error: {}", e))?;
out.flush().map_err(|e| format!("output error: {}", e))?;
let mut line = String::new();
match stdin.lock().read_line(&mut line) {
Ok(0) => break,
Ok(_) => {}
Err(e) => return Err(format!("input error: {}", e)),
}
let status = rl.feed_line(&line);
if matches!(
status,
crate::parser::readline::ChunkStatus::Incomplete { .. }
) {
continue;
}
let chunk = rl.take_chunk();
if chunk.trim().is_empty() {
continue;
}
match eval_chunk(&mut vm, &chunk) {
Ok(echo) => {
if let Some(s) = echo {
writeln!(out, "{}", s).map_err(|e| format!("output error: {}", e))?;
}
}
Err(e) if is_incomplete_input(&e) => {
rl.feed_line(&chunk);
}
Err(e) => {
writeln!(out, "Error: {}", e).map_err(|e| format!("output error: {}", e))?;
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn parse_err(src: &str) -> String {
crate::compiler::compile_source(src).unwrap_err()
}
#[test]
fn incomplete_truncated_inputs_continue() {
for src in [
"function f(",
"if true then",
"local x = {1, 2",
"print(\"hi\"",
"local x = 1 +",
"f(",
] {
assert!(
is_incomplete_input(&parse_err(src)),
"should continue: {:?}",
src
);
}
}
#[test]
fn complete_errors_report_immediately() {
let src = "local = 1";
assert!(
!is_incomplete_input(&parse_err(src)),
"should report: {:?}",
src
);
}
#[test]
fn eval_chunk_reports_incomplete_with_rendered_diagnostic() {
let mut vm = VM::new();
let err = eval_chunk(&mut vm, "function f(").unwrap_err();
assert!(
is_incomplete_input(&err),
"eval_chunk error must be recognized as incomplete input"
);
}
#[test]
fn eval_chunk_keeps_globals_across_chunks() {
let mut vm = VM::new();
eval_chunk(&mut vm, "repl_probe_xyz = 41").expect("chunk 1");
let out = eval_chunk(&mut vm, "return repl_probe_xyz + 1").expect("chunk 2");
assert_eq!(out.as_deref(), Some("42"));
}
#[test]
fn eval_chunk_function_via_bare_assignment_persists() {
let mut vm = VM::new();
eval_chunk(&mut vm, "f_repl_xyz = function(x)\nreturn x * 2\nend").expect("def");
let out = eval_chunk(&mut vm, "return f_repl_xyz(21)").expect("call");
assert_eq!(out.as_deref(), Some("42"));
}
#[test]
fn eval_chunk_echo() {
let mut vm = VM::new();
assert_eq!(
eval_chunk(&mut vm, "return 1 + 2")
.expect("expr")
.as_deref(),
Some("3")
);
assert_eq!(eval_chunk(&mut vm, "local q = 1").expect("stmt"), None);
}
}