use std::io::{self, BufRead, Write};
use std::sync::mpsc::{self, Receiver, RecvTimeoutError};
use std::time::Instant;
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)),
}
}
fn run_due_tasks(vm: &mut VM) -> Result<(), String> {
let run = || crate::vm::libs::task::run_due(vm);
std::panic::catch_unwind(std::panic::AssertUnwindSafe(run)).map_err(panic_message)
}
fn spawn_line_reader() -> Receiver<io::Result<String>> {
let (tx, rx) = mpsc::channel();
std::thread::spawn(move || {
let stdin = io::stdin();
loop {
let mut line = String::new();
let res = match stdin.lock().read_line(&mut line) {
Ok(0) => return,
Ok(_) => Ok(line),
Err(e) => Err(e),
};
let failed = res.is_err();
if tx.send(res).is_err() || failed {
return;
}
}
});
rx
}
fn next_line(vm: &mut VM, lines: &Receiver<io::Result<String>>) -> Option<io::Result<String>> {
loop {
let Some(at) = crate::vm::libs::task::next_due(vm) else {
return lines.recv().ok();
};
match lines.recv_timeout(at.saturating_duration_since(Instant::now())) {
Ok(line) => return Some(line),
Err(RecvTimeoutError::Disconnected) => return None,
Err(RecvTimeoutError::Timeout) => {
if let Err(e) = run_due_tasks(vm) {
eprintln!("Error: {}", e);
}
}
}
}
}
use crate::parser::readline::ReadLineEngine;
pub fn run_repl() -> Result<(), String> {
let lines = spawn_line_reader();
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 line = match next_line(&mut vm, &lines) {
None => break,
Some(Ok(line)) => line,
Some(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 tasks_run_while_waiting_for_input() {
let mut vm = VM::new();
eval_chunk(
&mut vm,
"task.spawn(function()\nwhile task.wait(0.005) do\nrepl_ticks_xyz = (repl_ticks_xyz or 0) + 1\nend\nend)\n\
task.delay(0.01, function() repl_delayed_xyz = true end)",
)
.expect("spawn");
let (tx, rx) = mpsc::channel();
let typist = std::thread::spawn(move || {
std::thread::sleep(std::time::Duration::from_millis(100));
tx.send(Ok("next\n".to_string())).unwrap();
});
let line = next_line(&mut vm, &rx).expect("line").expect("ok");
typist.join().unwrap();
assert_eq!(line, "next\n");
let out =
eval_chunk(&mut vm, "return repl_delayed_xyz and repl_ticks_xyz >= 3").expect("check");
assert_eq!(out.as_deref(), Some("true"));
}
#[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);
}
}