use async_trait::async_trait;
use endbasic_core::ast::{ArgSep, ArgSpan, BuiltinCallSpan, FunctionCallSpan, Value, VarType};
use endbasic_core::exec::Machine;
use endbasic_core::syms::{
CallError, CallableMetadata, CallableMetadataBuilder, Command, CommandResult, Function,
FunctionResult, Symbol, Symbols,
};
use endbasic_core::LineCol;
use futures_lite::future::{BoxedLocal, FutureExt};
use std::rc::Rc;
use std::thread;
use std::time::Duration;
pub(crate) const CATEGORY: &str = "Interpreter";
pub struct ClearCommand {
metadata: CallableMetadata,
}
impl ClearCommand {
pub fn new() -> Rc<Self> {
Rc::from(Self {
metadata: CallableMetadataBuilder::new("CLEAR", VarType::Void)
.with_syntax("")
.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!).",
)
.build(),
})
}
}
#[async_trait(?Send)]
impl Command for ClearCommand {
fn metadata(&self) -> &CallableMetadata {
&self.metadata
}
async fn exec(&self, span: &BuiltinCallSpan, machine: &mut Machine) -> CommandResult {
if !span.args.is_empty() {
return Err(CallError::SyntaxError);
}
machine.clear();
Ok(())
}
}
pub struct ErrmsgFunction {
metadata: CallableMetadata,
}
impl ErrmsgFunction {
pub fn new() -> Rc<Self> {
Rc::from(Self {
metadata: CallableMetadataBuilder::new("ERRMSG", VarType::Text)
.with_syntax("")
.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 Function for ErrmsgFunction {
fn metadata(&self) -> &CallableMetadata {
&self.metadata
}
async fn exec(&self, span: &FunctionCallSpan, symbols: &mut Symbols) -> FunctionResult {
if !span.args.is_empty() {
return Err(CallError::SyntaxError);
}
match symbols.get_auto("0errmsg") {
Some(Symbol::Variable(v @ Value::Text(_))) => Ok(v.clone()),
Some(_) => panic!("Internal symbol must be of a specific type"),
None => Ok(Value::Text("".to_owned())),
}
}
}
pub type SleepFn = Box<dyn Fn(Duration, LineCol) -> BoxedLocal<CommandResult>>;
fn system_sleep(d: Duration, _pos: LineCol) -> BoxedLocal<CommandResult> {
async move {
thread::sleep(d);
Ok(())
}
.boxed_local()
}
pub struct SleepCommand {
metadata: CallableMetadata,
sleep_fn: SleepFn,
}
impl SleepCommand {
pub fn new(sleep_fn: SleepFn) -> Rc<Self> {
Rc::from(Self {
metadata: CallableMetadataBuilder::new("SLEEP", VarType::Void)
.with_syntax("seconds<%|#>")
.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 Command for SleepCommand {
fn metadata(&self) -> &CallableMetadata {
&self.metadata
}
async fn exec(&self, span: &BuiltinCallSpan, machine: &mut Machine) -> CommandResult {
let (duration, pos) = match span.args.as_slice() {
[ArgSpan { expr: Some(expr), sep: ArgSep::End, .. }] => {
let value = expr.eval(machine.get_mut_symbols()).await?;
let n = value
.as_f64()
.map_err(|e| CallError::ArgumentError(expr.start_pos(), format!("{}", e)))?;
if n < 0.0 {
return Err(CallError::ArgumentError(
expr.start_pos(),
"Sleep time must be positive".to_owned(),
));
}
(Duration::from_secs_f64(n), expr.start_pos())
}
_ => return Err(CallError::SyntaxError),
};
(self.sleep_fn)(duration, pos).await
}
}
pub fn add_all(machine: &mut Machine, sleep_fn: Option<SleepFn>) {
machine.add_command(ClearCommand::new());
machine.add_function(ErrmsgFunction::new());
machine.add_command(SleepCommand::new(sleep_fn.unwrap_or_else(|| Box::from(system_sleep))));
}
#[cfg(test)]
mod tests {
use super::*;
use crate::testutils::*;
use std::time::Instant;
#[test]
fn test_clear_ok() {
Tester::default().run("a = 1: CLEAR").expect_clear().check();
Tester::default()
.run("DIM a(2): CLEAR: DIM a(5) AS STRING: CLEAR")
.expect_clear()
.expect_clear()
.check();
}
#[test]
fn test_clear_errors() {
check_stmt_err("1:1: In call to 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: PRINT a: PRINT \"Captured: \"; ERRMSG")
.expect_var("0ERRMSG", "1:29: Undefined variable a")
.expect_prints(["Captured: 1:29: Undefined variable a"])
.check();
}
#[test]
fn test_errmsg_errors() {
check_expr_error(
"1:10: In call to ERRMSG: expected no arguments nor parenthesis",
r#"ERRMSG()"#,
);
check_expr_error(
"1:10: In call to ERRMSG: expected no arguments nor parenthesis",
r#"ERRMSG(3)"#,
);
}
#[test]
fn test_sleep_ok_int() {
let sleep_fake = |d: Duration, pos: LineCol| -> BoxedLocal<CommandResult> {
async move { Err(CallError::InternalError(pos, format!("Got {} ms", d.as_millis()))) }
.boxed_local()
};
let mut t = Tester::empty().add_command(SleepCommand::new(Box::from(sleep_fake)));
t.run("SLEEP 123").expect_err("1:1: In call to SLEEP: 1:7: Got 123000 ms").check();
}
#[test]
fn test_sleep_ok_float() {
let sleep_fake = |d: Duration, pos: LineCol| -> BoxedLocal<CommandResult> {
async move {
let ms = d.as_millis();
if ms > 123095 && ms < 123105 {
Err(CallError::InternalError(pos, "Good".to_owned()))
} else {
Err(CallError::InternalError(pos, format!("Bad {}", ms)))
}
}
.boxed_local()
};
let mut t = Tester::empty().add_command(SleepCommand::new(Box::from(sleep_fake)));
t.run("SLEEP 123.1").expect_err("1:1: In call to SLEEP: 1:7: Good").check();
}
#[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_err("1:1: In call to SLEEP: expected seconds<%|#>", "SLEEP");
check_stmt_err("1:1: In call to SLEEP: expected seconds<%|#>", "SLEEP 2, 3");
check_stmt_err("1:1: In call to SLEEP: expected seconds<%|#>", "SLEEP 2; 3");
check_stmt_err("1:1: In call to SLEEP: 1:7: \"foo\" is not a number", "SLEEP \"foo\"");
check_stmt_err("1:1: In call to SLEEP: 1:7: Sleep time must be positive", "SLEEP -1");
check_stmt_err("1:1: In call to SLEEP: 1:7: Sleep time must be positive", "SLEEP -0.001");
}
}