#![allow(clippy::await_holding_refcell_ref)]
#![allow(clippy::collapsible_else_if)]
#![warn(anonymous_parameters, bad_style, missing_docs)]
#![warn(unused, unused_extern_crates, unused_import_braces, unused_qualifications)]
#![warn(unsafe_code)]
use endbasic_core::exec::{Machine, StopReason};
use endbasic_std::console::{self, refill_and_print, Console};
use endbasic_std::program::{continue_if_modified, Program, BREAK_MSG};
use endbasic_std::storage::Storage;
use std::cell::RefCell;
use std::io;
use std::rc::Rc;
pub mod demos;
pub mod editor;
pub fn print_welcome(console: Rc<RefCell<dyn Console>>) -> io::Result<()> {
let mut console = console.borrow_mut();
console.print("")?;
console.print(&format!(" EndBASIC {}", env!("CARGO_PKG_VERSION")))?;
console.print(" Copyright 2020-2022 Julio Merino")?;
console.print("")?;
console.print(" Type HELP for interactive usage information.")?;
console.print("")?;
Ok(())
}
pub async fn try_load_autoexec(
machine: &mut Machine,
console: Rc<RefCell<dyn Console>>,
storage: Rc<RefCell<Storage>>,
) -> io::Result<()> {
let code = match storage.borrow().get("AUTOEXEC.BAS").await {
Ok(code) => code,
Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
Err(e) => {
return console
.borrow_mut()
.print(&format!("AUTOEXEC.BAS exists but cannot be read: {}", e));
}
};
console.borrow_mut().print("Loading AUTOEXEC.BAS...")?;
match machine.exec(&mut code.as_bytes()).await {
Ok(_) => Ok(()),
Err(e) => {
console.borrow_mut().print(&format!("AUTOEXEC.BAS failed: {}", e))?;
Ok(())
}
}
}
pub async fn run_from_cloud(
machine: &mut Machine,
console: Rc<RefCell<dyn Console>>,
storage: Rc<RefCell<Storage>>,
program: Rc<RefCell<dyn Program>>,
username_path: &str,
will_run_repl: bool,
) -> endbasic_core::exec::Result<i32> {
let (fs_uri, path) = match username_path.split_once('/') {
Some((username, path)) => (format!("cloud://{}", username), format!("AUTORUN:/{}", path)),
None => {
let mut console = console.borrow_mut();
console.print(&format!(
"Invalid program to run '{}'; must be of the form 'username/path'",
username_path
))?;
return Ok(1);
}
};
console.borrow_mut().print(&format!("Mounting {} as AUTORUN...", fs_uri))?;
storage.borrow_mut().mount("AUTORUN", &fs_uri)?;
storage.borrow_mut().cd("AUTORUN:/")?;
console.borrow_mut().print(&format!("Loading {}...", path))?;
let content = storage.borrow().get(&path).await?;
program.borrow_mut().load(Some(&path), &content);
console.borrow_mut().print("Starting...")?;
console.borrow_mut().print("")?;
let result = machine.exec(&mut "RUN".as_bytes()).await;
let mut console = console.borrow_mut();
console.print("")?;
let code = match result {
Ok(r @ StopReason::Eof) => {
console.print("**** Program exited due to EOF ****")?;
r.as_exit_code()
}
Ok(r @ StopReason::Exited(_)) => {
let code = r.as_exit_code();
console.print(&format!("**** Program exited with code {} ****", code))?;
code
}
Ok(r @ StopReason::Break) => {
console.print("**** Program stopped due to BREAK ****")?;
r.as_exit_code()
}
Err(e) => {
console.print(&format!("**** ERROR: {} ****", e))?;
1
}
};
if will_run_repl {
console.print("")?;
refill_and_print(
&mut *console,
[
"You are now being dropped into the EndBASIC interpreter.",
"The program you asked to run is still loaded in memory and you can interact with \
it now. Use LIST to view the source code, EDIT to launch an editor on the source code, and RUN to \
execute the program again.",
"Type HELP for interactive usage information.",
],
" ",
)?;
console.print("")?;
}
Ok(code)
}
pub async fn run_repl_loop(
machine: &mut Machine,
console: Rc<RefCell<dyn Console>>,
program: Rc<RefCell<dyn Program>>,
) -> io::Result<i32> {
let mut stop_reason = StopReason::Eof;
let mut history = vec![];
while stop_reason == StopReason::Eof {
let line = {
let mut console = console.borrow_mut();
if console.is_interactive() {
console.print("Ready")?;
}
console::read_line(&mut *console, "", "", Some(&mut history)).await
};
machine.drain_signals();
match line {
Ok(line) => match machine.exec(&mut line.as_bytes()).await {
Ok(reason) => stop_reason = reason,
Err(e) => {
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(BREAK_MSG)?;
} else if e.kind() == io::ErrorKind::UnexpectedEof {
let mut console = console.borrow_mut();
console.print("End of input by CTRL-D")?;
stop_reason = StopReason::Exited(0);
} else {
stop_reason = StopReason::Exited(1);
}
}
}
match stop_reason {
StopReason::Eof => (),
StopReason::Break => {
console.borrow_mut().print("**** BREAK ****")?;
stop_reason = StopReason::Eof;
}
StopReason::Exited(_) => {
if !continue_if_modified(&*program.borrow(), &mut *console.borrow_mut()).await? {
console.borrow_mut().print("Exit aborted; resuming REPL loop.")?;
stop_reason = StopReason::Eof;
}
}
}
}
Ok(stop_reason.as_exit_code())
}
#[cfg(test)]
mod tests {
use super::*;
use endbasic_core::exec::Signal;
use endbasic_std::console::Key;
use endbasic_std::storage::{Drive, DriveFactory, InMemoryDrive};
use endbasic_std::testutils::*;
use futures_lite::future::block_on;
#[test]
fn test_autoexec_ok() {
let autoexec = "PRINT \"hello\": global_var = 3: CD \"MEMORY:/\"";
let mut tester = Tester::default().write_file("AUTOEXEC.BAS", autoexec);
let (console, storage) = (tester.get_console(), tester.get_storage());
block_on(try_load_autoexec(tester.get_machine(), console, storage)).unwrap();
tester
.run("")
.expect_var("global_var", 3)
.expect_prints(["Loading AUTOEXEC.BAS...", "hello"])
.expect_file("MEMORY:/AUTOEXEC.BAS", autoexec)
.check();
}
#[test]
fn test_autoexec_error_is_ignored() {
let autoexec = "a = 1\nb = undef: c = 2";
let mut tester = Tester::default().write_file("AUTOEXEC.BAS", autoexec);
let (console, storage) = (tester.get_console(), tester.get_storage());
block_on(try_load_autoexec(tester.get_machine(), console, storage)).unwrap();
tester
.run("after = 5")
.expect_var("a", 1)
.expect_var("after", 5)
.expect_var("0ERRMSG", "2:5: Undefined variable undef")
.expect_prints([
"Loading AUTOEXEC.BAS...",
"AUTOEXEC.BAS failed: 2:5: Undefined variable undef",
])
.expect_file("MEMORY:/AUTOEXEC.BAS", autoexec)
.check();
}
#[test]
fn test_autoexec_name_is_case_sensitive() {
let mut tester = Tester::default()
.write_file("AUTOEXEC.BAS", "a = 1")
.write_file("autoexec.bas", "a = 2");
let (console, storage) = (tester.get_console(), tester.get_storage());
block_on(try_load_autoexec(tester.get_machine(), console, storage)).unwrap();
tester
.run("")
.expect_var("a", 1)
.expect_prints(["Loading AUTOEXEC.BAS..."])
.expect_file("MEMORY:/AUTOEXEC.BAS", "a = 1")
.expect_file("MEMORY:/autoexec.bas", "a = 2")
.check();
}
#[test]
fn test_autoexec_missing() {
let mut tester = Tester::default();
let (console, storage) = (tester.get_console(), tester.get_storage());
block_on(try_load_autoexec(tester.get_machine(), console, storage)).unwrap();
tester.run("").check();
}
struct MockDriveFactory {
exp_username: &'static str,
exp_file: &'static str,
}
impl MockDriveFactory {
const SCRIPT: &'static str = r#"PRINT "Success""#;
}
impl DriveFactory for MockDriveFactory {
fn create(&self, target: &str) -> io::Result<Box<dyn Drive>> {
let mut drive = InMemoryDrive::default();
block_on(drive.put(self.exp_file, Self::SCRIPT)).unwrap();
assert_eq!(self.exp_username, target);
Ok(Box::from(drive))
}
}
#[test]
fn test_run_from_cloud_no_repl() {
let mut tester = Tester::default();
let (console, storage, program) =
(tester.get_console(), tester.get_storage(), tester.get_program());
storage.borrow_mut().register_scheme(
"cloud",
Box::from(MockDriveFactory { exp_username: "foo", exp_file: "bar.bas" }),
);
block_on(run_from_cloud(
tester.get_machine(),
console,
storage,
program,
"foo/bar.bas",
false,
))
.unwrap();
tester
.run("")
.expect_prints([
"Mounting cloud://foo as AUTORUN...",
"Loading AUTORUN:/bar.bas...",
"Starting...",
"",
])
.expect_clear()
.expect_prints(["Success", "", "**** Program exited due to EOF ****"])
.expect_program(Some("AUTORUN:/bar.bas"), MockDriveFactory::SCRIPT)
.check();
}
#[test]
fn test_run_from_cloud_repl() {
let mut tester = Tester::default();
let (console, storage, program) =
(tester.get_console(), tester.get_storage(), tester.get_program());
storage.borrow_mut().register_scheme(
"cloud",
Box::from(MockDriveFactory { exp_username: "abcd", exp_file: "the-path.bas" }),
);
block_on(run_from_cloud(
tester.get_machine(),
console,
storage,
program,
"abcd/the-path.bas",
true,
))
.unwrap();
let mut checker = tester.run("");
let output = flatten_output(checker.take_captured_out());
checker.expect_program(Some("AUTORUN:/the-path.bas"), MockDriveFactory::SCRIPT).check();
assert!(output.contains("You are now being dropped into"));
}
#[test]
fn test_run_repl_loop_signal_before_exec() {
let mut tester = Tester::default();
let (console, program) = (tester.get_console(), tester.get_program());
let signals_tx = tester.get_machine().get_signals_tx();
{
let mut console = console.borrow_mut();
console.add_input_chars("PRINT");
block_on(signals_tx.send(Signal::Break)).unwrap();
console.add_input_chars(" 123");
console.add_input_keys(&[Key::NewLine, Key::Eof]);
}
block_on(run_repl_loop(tester.get_machine(), console, program)).unwrap();
tester.run("").expect_prints([" 123", "End of input by CTRL-D"]).check();
}
}