1use async_trait::async_trait;
19use endbasic_core::LineCol;
20use endbasic_core::ast::ExprType;
21use endbasic_core::compiler::{ArgSepSyntax, RequiredValueSyntax, SingularArgSyntax};
22use endbasic_core::exec::{Error, Machine, Result, Scope};
23use endbasic_core::syms::{Callable, CallableMetadata, CallableMetadataBuilder};
24use futures_lite::future::{BoxedLocal, FutureExt};
25use std::borrow::Cow;
26use std::rc::Rc;
27use std::thread;
28use std::time::Duration;
29
30pub(crate) const CATEGORY: &str = "Interpreter";
32
33pub struct ClearCommand {
35 metadata: CallableMetadata,
36}
37
38impl ClearCommand {
39 pub fn new() -> Rc<Self> {
41 Rc::from(Self {
42 metadata: CallableMetadataBuilder::new("CLEAR")
43 .with_syntax(&[(&[], None)])
44 .with_category(CATEGORY)
45 .with_description(
46 "Restores initial machine state but keeps the stored program.
47This command resets the machine to a semi-pristine state by clearing all user-defined variables \
48and restoring the state of shared resources. These resources include: the console, whose color \
49and video syncing bit are reset; and the GPIO pins, which are set to their default state.
50The stored program is kept in memory. To clear that too, use NEW (but don't forget to first \
51SAVE your program!).
52This command is for interactive use only.",
53 )
54 .build(),
55 })
56 }
57}
58
59#[async_trait(?Send)]
60impl Callable for ClearCommand {
61 fn metadata(&self) -> &CallableMetadata {
62 &self.metadata
63 }
64
65 async fn exec(&self, scope: Scope<'_>, machine: &mut Machine) -> Result<()> {
66 debug_assert_eq!(0, scope.nargs());
67 machine.clear();
68 Ok(())
69 }
70}
71
72pub struct ErrmsgFunction {
74 metadata: CallableMetadata,
75}
76
77impl ErrmsgFunction {
78 pub fn new() -> Rc<Self> {
80 Rc::from(Self {
81 metadata: CallableMetadataBuilder::new("ERRMSG")
82 .with_return_type(ExprType::Text)
83 .with_syntax(&[(&[], None)])
84 .with_category(CATEGORY)
85 .with_description(
86 "Returns the last captured error message.
87When used in combination of ON ERROR to set an error handler, this function returns the string \
88representation of the last captured error. If this is called before any error is captured, \
89returns the empty string.",
90 )
91 .build(),
92 })
93 }
94}
95
96#[async_trait(?Send)]
97impl Callable for ErrmsgFunction {
98 fn metadata(&self) -> &CallableMetadata {
99 &self.metadata
100 }
101
102 async fn exec(&self, scope: Scope<'_>, machine: &mut Machine) -> Result<()> {
103 debug_assert_eq!(0, scope.nargs());
104
105 match machine.last_error() {
106 Some(message) => scope.return_string(message),
107 None => scope.return_string("".to_owned()),
108 }
109 }
110}
111
112pub type SleepFn = Box<dyn Fn(Duration, LineCol) -> BoxedLocal<Result<()>>>;
114
115fn system_sleep(d: Duration, _pos: LineCol) -> BoxedLocal<Result<()>> {
117 async move {
118 thread::sleep(d);
119 Ok(())
120 }
121 .boxed_local()
122}
123
124pub struct SleepCommand {
126 metadata: CallableMetadata,
127 sleep_fn: SleepFn,
128}
129
130impl SleepCommand {
131 pub fn new(sleep_fn: SleepFn) -> Rc<Self> {
133 Rc::from(Self {
134 metadata: CallableMetadataBuilder::new("SLEEP")
135 .with_syntax(&[(
136 &[SingularArgSyntax::RequiredValue(
137 RequiredValueSyntax {
138 name: Cow::Borrowed("seconds"),
139 vtype: ExprType::Double,
140 },
141 ArgSepSyntax::End,
142 )],
143 None,
144 )])
145 .with_category(CATEGORY)
146 .with_description(
147 "Suspends program execution.
148Pauses program execution for the given number of seconds, which can be specified either as an \
149integer or as a floating point number for finer precision.",
150 )
151 .build(),
152 sleep_fn,
153 })
154 }
155}
156
157#[async_trait(?Send)]
158impl Callable for SleepCommand {
159 fn metadata(&self) -> &CallableMetadata {
160 &self.metadata
161 }
162
163 async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> {
164 debug_assert_eq!(1, scope.nargs());
165 let (n, pos) = scope.pop_double_with_pos();
166
167 if n < 0.0 {
168 return Err(Error::SyntaxError(pos, "Sleep time must be positive".to_owned()));
169 }
170
171 (self.sleep_fn)(Duration::from_secs_f64(n), pos).await
172 }
173}
174
175pub fn add_scripting(machine: &mut Machine, sleep_fn: Option<SleepFn>) {
180 machine.add_callable(ErrmsgFunction::new());
181 machine.add_callable(SleepCommand::new(sleep_fn.unwrap_or_else(|| Box::from(system_sleep))));
182}
183
184pub fn add_interactive(machine: &mut Machine) {
186 machine.add_callable(ClearCommand::new());
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192 use crate::testutils::*;
193 use std::time::Instant;
194
195 #[test]
196 fn test_clear_ok() {
197 Tester::default().run("a = 1: CLEAR").expect_clear().check();
198 Tester::default()
199 .run_n(&["DIM a(2): CLEAR", "DIM a(5) AS STRING: CLEAR"])
200 .expect_clear()
201 .expect_clear()
202 .check();
203 }
204
205 #[test]
206 fn test_clear_errors() {
207 check_stmt_compilation_err("1:1: CLEAR expected no arguments", "CLEAR 123");
208 }
209
210 #[test]
211 fn test_errmsg_before_error() {
212 check_expr_ok("", r#"ERRMSG"#);
213 }
214
215 #[test]
216 fn test_errmsg_after_error() {
217 Tester::default()
218 .run("ON ERROR RESUME NEXT: COLOR -1: PRINT \"Captured: \"; ERRMSG")
219 .expect_prints(["Captured: 1:29: Color out of range"])
220 .check();
221 }
222
223 #[test]
224 fn test_errmsg_errors() {
225 check_expr_compilation_error("1:10: ERRMSG expected no arguments", r#"ERRMSG()"#);
226 check_expr_compilation_error("1:10: ERRMSG expected no arguments", r#"ERRMSG(3)"#);
227 }
228
229 #[test]
230 fn test_sleep_ok_int() {
231 let sleep_fake = |d: Duration, pos: LineCol| -> BoxedLocal<Result<()>> {
232 async move { Err(Error::InternalError(pos, format!("Got {} ms", d.as_millis()))) }
233 .boxed_local()
234 };
235
236 let mut t = Tester::empty().add_callable(SleepCommand::new(Box::from(sleep_fake)));
237 t.run("SLEEP 123").expect_err("1:7: Got 123000 ms").check();
238 }
239
240 #[test]
241 fn test_sleep_ok_float() {
242 let sleep_fake = |d: Duration, pos: LineCol| -> BoxedLocal<Result<()>> {
243 async move {
244 let ms = d.as_millis();
245 if ms > 123095 && ms < 123105 {
246 Err(Error::InternalError(pos, "Good".to_owned()))
247 } else {
248 Err(Error::InternalError(pos, format!("Bad {}", ms)))
249 }
250 }
251 .boxed_local()
252 };
253
254 let mut t = Tester::empty().add_callable(SleepCommand::new(Box::from(sleep_fake)));
255 t.run("SLEEP 123.1").expect_err("1:7: Good").check();
256 }
257
258 #[test]
259 fn test_sleep_real() {
260 let before = Instant::now();
261 Tester::default().run("SLEEP 0.010").check();
262 assert!(before.elapsed() >= Duration::from_millis(10));
263 }
264
265 #[test]
266 fn test_sleep_errors() {
267 check_stmt_compilation_err("1:1: SLEEP expected seconds#", "SLEEP");
268 check_stmt_compilation_err("1:1: SLEEP expected seconds#", "SLEEP 2, 3");
269 check_stmt_compilation_err("1:1: SLEEP expected seconds#", "SLEEP 2; 3");
270 check_stmt_compilation_err("1:7: STRING is not a number", "SLEEP \"foo\"");
271 check_stmt_err("1:7: Sleep time must be positive", "SLEEP -1");
272 check_stmt_err("1:7: Sleep time must be positive", "SLEEP -0.001");
273 }
274}