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