use async_trait::async_trait;
use endbasic_core::{
ArgSepSyntax, CallError, CallResult, Callable, CallableMetadata, CallableMetadataBuilder,
ExprType, RequiredValueSyntax, Scope, SingularArgSyntax,
};
use futures_lite::future::{BoxedLocal, FutureExt};
use std::borrow::Cow;
use std::cell::RefCell;
use std::rc::Rc;
use std::thread;
use std::time::Duration;
use crate::{MachineAction, MachineBuilder};
pub(crate) const CATEGORY: &str = "Interpreter";
pub struct ClearCommand {
metadata: Rc<CallableMetadata>,
actions: Rc<RefCell<Vec<MachineAction>>>,
}
impl ClearCommand {
pub fn new(actions: Rc<RefCell<Vec<MachineAction>>>) -> Rc<Self> {
Rc::from(Self {
metadata: CallableMetadataBuilder::new("CLEAR")
.with_async(true)
.with_syntax(&[(&[], None)])
.with_category(CATEGORY)
.with_description(
"Restores initial machine state but keeps the stored program.
This command resets the machine to a semi-pristine state by clearing all user-defined variables \
and restoring the state of shared resources. These resources include: the console, whose color \
and video syncing bit are reset; and the GPIO pins, which are set to their default state.
The stored program is kept in memory. To clear that too, use NEW (but don't forget to first \
SAVE your program!).
This command is for interactive use only.",
)
.build(),
actions,
})
}
}
#[async_trait(?Send)]
impl Callable for ClearCommand {
fn metadata(&self) -> Rc<CallableMetadata> {
self.metadata.clone()
}
async fn async_exec(&self, _scope: Scope<'_>) -> CallResult<()> {
self.actions.borrow_mut().push(MachineAction::Clear);
Ok(())
}
}
pub struct ErrmsgFunction {
metadata: Rc<CallableMetadata>,
}
impl ErrmsgFunction {
pub fn new() -> Rc<Self> {
Rc::from(Self {
metadata: CallableMetadataBuilder::new("ERRMSG")
.with_return_type(ExprType::Text)
.with_syntax(&[(&[], None)])
.with_category(CATEGORY)
.with_description(
"Returns the last captured error message.
When used in combination of ON ERROR to set an error handler, this function returns the string \
representation of the last captured error. If this is called before any error is captured, \
returns the empty string.",
)
.build(),
})
}
}
#[async_trait(?Send)]
impl Callable for ErrmsgFunction {
fn metadata(&self) -> Rc<CallableMetadata> {
self.metadata.clone()
}
fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
debug_assert_eq!(0, scope.nargs());
let message = scope
.last_error()
.map(|(pos, message)| format!("{}: {}", pos, message))
.unwrap_or_default();
scope.return_string(message)
}
}
pub type SleepFn = Box<dyn Fn(Duration) -> BoxedLocal<Result<(), String>>>;
fn system_sleep(d: Duration) -> BoxedLocal<Result<(), String>> {
async move {
thread::sleep(d);
Ok(())
}
.boxed_local()
}
pub struct SleepCommand {
metadata: Rc<CallableMetadata>,
sleep_fn: SleepFn,
}
impl SleepCommand {
pub fn new(sleep_fn: SleepFn) -> Rc<Self> {
Rc::from(Self {
metadata: CallableMetadataBuilder::new("SLEEP")
.with_async(true)
.with_syntax(&[(
&[SingularArgSyntax::RequiredValue(
RequiredValueSyntax {
name: Cow::Borrowed("seconds"),
vtype: ExprType::Double,
},
ArgSepSyntax::End,
)],
None,
)])
.with_category(CATEGORY)
.with_description(
"Suspends program execution.
Pauses program execution for the given number of seconds, which can be specified either as an \
integer or as a floating point number for finer precision.",
)
.build(),
sleep_fn,
})
}
}
#[async_trait(?Send)]
impl Callable for SleepCommand {
fn metadata(&self) -> Rc<CallableMetadata> {
self.metadata.clone()
}
async fn async_exec(&self, scope: Scope<'_>) -> CallResult<()> {
debug_assert_eq!(1, scope.nargs());
let n = scope.get_double(0);
if n < 0.0 {
return Err(CallError::Syntax(
scope.get_pos(0),
"Sleep time must be positive".to_owned(),
));
}
(self.sleep_fn)(Duration::from_secs_f64(n))
.await
.map_err(|e| CallError::Syntax(scope.get_pos(0), e))
}
}
pub fn add_scripting(machine: &mut MachineBuilder, sleep_fn: Option<SleepFn>) {
machine.add_callable(ErrmsgFunction::new());
machine.add_callable(SleepCommand::new(sleep_fn.unwrap_or_else(|| Box::from(system_sleep))));
}
pub fn add_interactive(machine: &mut MachineBuilder) {
machine.add_callable(ClearCommand::new(machine.actions()));
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testutils::*;
use crate::{Error, MachineBuilder, Signal, Yielder};
use futures_lite::future::block_on;
use std::cell::RefCell;
use std::rc::Rc;
use std::time::Instant;
#[test]
fn test_clear_ok() {
Tester::default().run("a = 1: CLEAR").expect_clear().check();
Tester::default()
.run_n(&["DIM a(2): CLEAR", "DIM a(5) AS STRING: CLEAR"])
.expect_clear()
.expect_clear()
.check();
}
#[test]
fn test_clear_inside_gosub_stops_execution() {
Tester::default().run("GOSUB @sub: END\n@sub:\nCLEAR").expect_clear().check();
}
#[test]
fn test_clear_inside_sub_stops_execution() {
Tester::default()
.run("SUB foo: PRINT 3: CLEAR: PRINT 5: END SUB: foo: foo")
.expect_prints([" 3"])
.expect_clear()
.check();
}
#[test]
fn test_clear_errors() {
check_stmt_compilation_err("1:1: CLEAR expected no arguments", "CLEAR 123");
}
#[test]
fn test_errmsg_before_error() {
check_expr_ok("", r#"ERRMSG"#);
}
#[test]
fn test_errmsg_after_error() {
Tester::default()
.run("ON ERROR RESUME NEXT: COLOR -1: PRINT \"Captured: \"; ERRMSG")
.expect_prints(["Captured: 1:29: Color out of range"])
.check();
}
#[test]
fn test_errmsg_errors() {
check_expr_compilation_error("1:10: ERRMSG expected no arguments", r#"ERRMSG()"#);
check_expr_compilation_error("1:10: ERRMSG expected no arguments", r#"ERRMSG(3)"#);
}
#[test]
fn test_sleep_ok_int() {
let sleep_fake = |d: Duration| -> BoxedLocal<Result<(), String>> {
async move { Err(format!("Got {} ms", d.as_millis())) }.boxed_local()
};
let mut machine = MachineBuilder::default().with_sleep_fn(Box::from(sleep_fake)).build();
machine.compile(&mut "SLEEP 123".as_bytes()).unwrap();
assert_eq!("1:7: Got 123000 ms", format!("{}", block_on(machine.exec()).unwrap_err()));
}
#[test]
fn test_sleep_ok_float() {
let sleep_fake = |d: Duration| -> BoxedLocal<Result<(), String>> {
async move {
let ms = d.as_millis();
if ms > 123095 && ms < 123105 {
Err("Good".to_owned())
} else {
Err(format!("Bad {}", ms))
}
}
.boxed_local()
};
let mut machine = MachineBuilder::default().with_sleep_fn(Box::from(sleep_fake)).build();
machine.compile(&mut "SLEEP 123.1".as_bytes()).unwrap();
assert_eq!("1:7: Good", format!("{}", block_on(machine.exec()).unwrap_err()));
}
#[test]
fn test_sleep_real() {
let before = Instant::now();
Tester::default().run("SLEEP 0.010").check();
assert!(before.elapsed() >= Duration::from_millis(10));
}
#[test]
fn test_sleep_errors() {
check_stmt_compilation_err("1:1: SLEEP expected seconds#", "SLEEP");
check_stmt_compilation_err("1:1: SLEEP expected seconds#", "SLEEP 2, 3");
check_stmt_compilation_err("1:1: SLEEP expected seconds#", "SLEEP 2; 3");
check_stmt_compilation_err("1:7: STRING is not a number", "SLEEP \"foo\"");
check_stmt_err("1:7: Sleep time must be positive", "SLEEP -1");
check_stmt_err("1:7: Sleep time must be positive", "SLEEP -0.001");
}
#[test]
fn test_break_stops_after_upcall() {
let (tx, rx) = async_channel::unbounded();
let break_tx = tx.clone();
let sleep_fake = move |_d: Duration| -> BoxedLocal<Result<(), String>> {
let break_tx = break_tx.clone();
async move {
break_tx.send(Signal::Break).await.unwrap();
Ok(())
}
.boxed_local()
};
let mut machine = MachineBuilder::default()
.with_signals_chan((tx.clone(), rx))
.with_sleep_fn(Box::from(sleep_fake))
.build();
machine.compile(&mut "DO: SLEEP 0: LOOP".as_bytes()).unwrap();
match block_on(machine.exec()) {
Err(Error::Break) => (),
r => panic!("Expected Break but got {:?}", r),
}
assert_eq!(0, tx.len());
}
#[test]
fn test_yielder_called_on_stop_reason_yield() {
struct CountingYielder {
count: Rc<RefCell<usize>>,
}
#[async_trait(?Send)]
impl Yielder for CountingYielder {
async fn yield_now(&mut self) {
*self.count.borrow_mut() += 1;
}
}
let (tx, rx) = async_channel::unbounded();
let yield_count = Rc::from(RefCell::from(0));
let mut machine = MachineBuilder::default()
.with_signals_chan((tx.clone(), rx))
.with_yielder(Box::new(CountingYielder { count: yield_count.clone() }))
.build();
block_on(tx.send(Signal::Break)).unwrap();
machine.compile(&mut "@here: GOTO @here".as_bytes()).unwrap();
match block_on(machine.exec()) {
Err(Error::Break) => (),
r => panic!("Expected Break but got {:?}", r),
}
assert_eq!(1, *yield_count.borrow());
}
#[test]
fn test_drain_signals_ignores_pending_break() {
let (tx, rx) = async_channel::unbounded();
let mut machine = MachineBuilder::default().with_signals_chan((tx.clone(), rx)).build();
block_on(tx.send(Signal::Break)).unwrap();
machine.drain_signals();
machine.compile(&mut "a = 1".as_bytes()).unwrap();
match block_on(machine.exec()) {
Ok(None) => (),
r => panic!("Expected Ok(None) but got {:?}", r),
}
assert_eq!(0, tx.len());
}
fn do_no_check_stop_test(code: &str) {
let (tx, rx) = async_channel::unbounded();
let mut machine = MachineBuilder::default().with_signals_chan((tx.clone(), rx)).build();
block_on(tx.send(Signal::Break)).unwrap();
machine.compile(&mut code.as_bytes()).unwrap();
match block_on(machine.exec()) {
Ok(None) => (),
r => panic!("Expected Ok(None) but got {:?}", r),
}
assert_eq!(1, tx.len());
}
fn do_check_stop_test(code: &str) {
let (tx, rx) = async_channel::unbounded();
let mut machine = MachineBuilder::default().with_signals_chan((tx.clone(), rx)).build();
block_on(tx.send(Signal::Break)).unwrap();
machine.compile(&mut code.as_bytes()).unwrap();
match block_on(machine.exec()) {
Err(Error::Break) => (),
r => panic!("Expected Break but got {:?}", r),
}
assert_eq!(0, tx.len());
}
#[test]
fn test_goto_forward_does_not_check_stop() {
do_no_check_stop_test("GOTO @after: a = 1: @after");
}
#[test]
fn test_if_taken_does_not_check_stop() {
do_no_check_stop_test("a = 3: IF a = 3 THEN b = 0 ELSE b = 1: a = 7");
}
#[test]
fn test_if_not_taken_does_not_check_stop() {
do_no_check_stop_test("a = 3: IF a = 5 THEN b = 0 ELSE b = 1: a = 7");
}
#[test]
fn test_goto_checks_stop() {
do_check_stop_test("@here: GOTO @here");
do_check_stop_test("@before: a = 1: GOTO @before");
}
#[test]
fn test_gosub_checks_stop() {
do_check_stop_test("GOTO @skip: @sub: a = 1: RETURN: @skip: GOSUB @sub: a = 1");
}
#[test]
fn test_do_checks_stop() {
do_check_stop_test("DO: LOOP");
do_check_stop_test("DO: a = 1: LOOP");
do_check_stop_test("DO UNTIL FALSE: LOOP");
do_check_stop_test("DO UNTIL FALSE: a = 1: LOOP");
do_check_stop_test("DO WHILE TRUE: LOOP");
do_check_stop_test("DO WHILE TRUE: a = 1: LOOP");
do_check_stop_test("DO: LOOP UNTIL FALSE");
do_check_stop_test("DO: a = 1: LOOP UNTIL FALSE");
do_check_stop_test("DO: LOOP WHILE TRUE");
do_check_stop_test("DO: a = 1: LOOP WHILE TRUE");
}
#[test]
fn test_for_checks_stop() {
do_check_stop_test("FOR a = 1 TO 10: NEXT");
do_check_stop_test("FOR a = 1 TO 10: b = 2: NEXT");
}
#[test]
fn test_while_checks_stop() {
do_check_stop_test("WHILE TRUE: WEND");
do_check_stop_test("WHILE TRUE: a = 1: WEND");
}
}