use crate::ast::{ArgSep, Expr, Value, VarType};
use crate::console::{self, Console};
use crate::eval::{CallableMetadata, CallableMetadataBuilder};
use crate::exec::{self, BuiltinCommand, ClearCommand, Machine, MachineBuilder};
use crate::help::HelpCommand;
use crate::numerics;
use crate::program::{self, Store};
use crate::strings;
use async_trait::async_trait;
use std::cell::RefCell;
use std::io;
use std::rc::Rc;
pub struct ExitCommand {
metadata: CallableMetadata,
code: Rc<RefCell<Option<i32>>>,
}
impl ExitCommand {
pub fn new(code: Rc<RefCell<Option<i32>>>) -> Rc<Self> {
Rc::from(Self {
metadata: CallableMetadataBuilder::new("EXIT", VarType::Void)
.with_syntax("[code%]")
.with_category("Interpreter manipulation")
.with_description(
"Exits the interpreter.
The optional code indicates the return value to return to the system.",
)
.build(),
code,
})
}
}
#[async_trait(?Send)]
impl BuiltinCommand for ExitCommand {
fn metadata(&self) -> &CallableMetadata {
&self.metadata
}
async fn exec(
&self,
args: &[(Option<Expr>, ArgSep)],
machine: &mut Machine,
) -> exec::Result<()> {
let arg = match args {
[] => 0,
[(Some(expr), ArgSep::End)] => {
match expr.eval(machine.get_vars(), machine.get_functions())? {
Value::Integer(n) => {
if n < 0 {
return exec::new_usage_error("Exit code must be a positive integer");
}
if n >= 128 {
return exec::new_usage_error("Exit code cannot be larger than 127");
}
n
}
_ => return exec::new_usage_error("Exit code must be a positive integer"),
}
}
_ => return exec::new_usage_error("EXIT takes zero or one argument"),
};
let mut code = self.code.borrow_mut();
debug_assert!(code.is_none(), "EXIT called multiple times without exiting");
*code = Some(arg);
Err(io::Error::new(io::ErrorKind::UnexpectedEof, "").into())
}
}
pub async fn run_repl_loop(
console: Rc<RefCell<dyn Console>>,
store: Rc<RefCell<dyn Store>>,
) -> io::Result<i32> {
let exit_code = Rc::from(RefCell::from(None));
let mut machine = {
let mut builder = MachineBuilder::default()
.add_command(ClearCommand::new())
.add_command(ExitCommand::new(exit_code.clone()))
.add_command(HelpCommand::new(console.clone()))
.add_commands(console::all_commands(console.clone()))
.add_commands(program::all_commands(console.clone(), store))
.add_functions(strings::all_functions());
builder = numerics::add_all(builder);
builder.build()
};
{
let mut console = console.borrow_mut();
console.print("")?;
console.print(&format!(" Welcome to EndBASIC {}.", env!("CARGO_PKG_VERSION")))?;
console.print("")?;
console.print(" Type HELP for interactive usage information.")?;
console.print(" Type LOAD \"DEMO:TOUR.BAS\": RUN for a guided tour.")?;
console.print("")?;
}
while exit_code.borrow().is_none() {
let line = {
let mut console = console.borrow_mut();
if console.is_interactive() {
console.print("Ready")?;
}
console::read_line(&mut *console, "", "").await
};
match line {
Ok(line) => match machine.exec(&mut line.as_bytes()).await {
Ok(()) => (),
Err(e) => {
if exit_code.borrow().is_some() {
if let exec::Error::IoError(e) = e {
debug_assert!(e.kind() == io::ErrorKind::UnexpectedEof);
}
} else {
let mut console = console.borrow_mut();
console.print(format!("ERROR: {}", e).as_str())?;
}
}
},
Err(e) => {
if e.kind() == io::ErrorKind::Interrupted {
let mut console = console.borrow_mut();
console.print("Interrupted by CTRL-C")?;
*exit_code.borrow_mut() = Some(1);
} else if e.kind() == io::ErrorKind::UnexpectedEof {
let mut console = console.borrow_mut();
console.print("End of input by CTRL-D")?;
*exit_code.borrow_mut() = Some(0);
} else {
*exit_code.borrow_mut() = Some(1);
}
}
}
}
let exit_code = exit_code.borrow();
Ok(exit_code.expect("Some code path above did not set an exit code"))
}
#[cfg(test)]
mod tests {
use super::*;
use futures_lite::future::block_on;
fn do_error_test(input: &str, expected_err: &str) {
let exit_code = Rc::from(RefCell::from(None));
let mut machine =
MachineBuilder::default().add_command(ExitCommand::new(exit_code.clone())).build();
let err = block_on(machine.exec(&mut input.as_bytes())).unwrap_err();
assert_eq!(expected_err, format!("{}", err));
assert!(exit_code.borrow().is_none());
}
#[test]
fn test_exit_no_code() {
let exit_code = Rc::from(RefCell::from(None));
let mut machine =
MachineBuilder::default().add_command(ExitCommand::new(exit_code.clone())).build();
block_on(machine.exec(&mut b"a = 3: EXIT: a = 4".as_ref())).unwrap_err();
assert_eq!(0, exit_code.borrow().unwrap());
assert_eq!(3, machine.get_var_as_int("a").unwrap());
}
fn do_exit_with_code_test(code: i32) {
let exit_code = Rc::from(RefCell::from(None));
let mut machine =
MachineBuilder::default().add_command(ExitCommand::new(exit_code.clone())).build();
block_on(machine.exec(&mut format!("a = 3: EXIT {}: a = 4", code).as_bytes())).unwrap_err();
assert_eq!(code, exit_code.borrow().unwrap());
assert_eq!(3, machine.get_var_as_int("a").unwrap());
}
#[test]
fn text_exit_with_code() {
do_exit_with_code_test(0);
do_exit_with_code_test(1);
do_exit_with_code_test(42);
do_exit_with_code_test(127);
}
#[test]
fn test_exit_errors() {
do_error_test("EXIT 1, 2", "EXIT takes zero or one argument");
do_error_test("EXIT -3", "Exit code must be a positive integer");
do_error_test("EXIT 128", "Exit code cannot be larger than 127");
}
}