1use async_trait::async_trait;
20use endbasic_core::{
21 ArgSepSyntax, CallError, CallResult, Callable, CallableMetadata, CallableMetadataBuilder,
22 ExprType, RequiredValueSyntax, Scope, SingularArgSyntax,
23};
24use futures_lite::future::{BoxedLocal, FutureExt};
25use std::borrow::Cow;
26use std::cell::RefCell;
27use std::rc::Rc;
28use std::thread;
29use std::time::Duration;
30
31use crate::{MachineAction, MachineBuilder};
32
33pub(crate) const CATEGORY: &str = "Interpreter";
35
36pub struct ClearCommand {
38 metadata: Rc<CallableMetadata>,
39 actions: Rc<RefCell<Vec<MachineAction>>>,
40}
41
42impl ClearCommand {
43 pub fn new(actions: Rc<RefCell<Vec<MachineAction>>>) -> Rc<Self> {
45 Rc::from(Self {
46 metadata: CallableMetadataBuilder::new("CLEAR")
47 .with_async(true)
48 .with_syntax(&[(&[], None)])
49 .with_category(CATEGORY)
50 .with_description(
51 "Restores initial machine state but keeps the stored program.
52This command resets the machine to a semi-pristine state by clearing all user-defined variables \
53and restoring the state of shared resources. These resources include: the console, whose color \
54and video syncing bit are reset; and the GPIO pins, which are set to their default state.
55The stored program is kept in memory. To clear that too, use NEW (but don't forget to first \
56SAVE your program!).
57This command is for interactive use only.",
58 )
59 .build(),
60 actions,
61 })
62 }
63}
64
65#[async_trait(?Send)]
66impl Callable for ClearCommand {
67 fn metadata(&self) -> Rc<CallableMetadata> {
68 self.metadata.clone()
69 }
70
71 async fn async_exec(&self, _scope: Scope<'_>) -> CallResult<()> {
72 self.actions.borrow_mut().push(MachineAction::Clear);
73 Ok(())
74 }
75}
76
77pub struct ErrmsgFunction {
79 metadata: Rc<CallableMetadata>,
80}
81
82impl ErrmsgFunction {
83 pub fn new() -> Rc<Self> {
85 Rc::from(Self {
86 metadata: CallableMetadataBuilder::new("ERRMSG")
87 .with_return_type(ExprType::Text)
88 .with_syntax(&[(&[], None)])
89 .with_category(CATEGORY)
90 .with_description(
91 "Returns the last captured error message.
92When used in combination of ON ERROR to set an error handler, this function returns the string \
93representation of the last captured error. If this is called before any error is captured, \
94returns the empty string.",
95 )
96 .build(),
97 })
98 }
99}
100
101#[async_trait(?Send)]
102impl Callable for ErrmsgFunction {
103 fn metadata(&self) -> Rc<CallableMetadata> {
104 self.metadata.clone()
105 }
106
107 fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
108 debug_assert_eq!(0, scope.nargs());
109
110 let message = scope
111 .last_error()
112 .map(|(pos, message)| format!("{}: {}", pos, message))
113 .unwrap_or_default();
114 scope.return_string(message)
115 }
116}
117
118pub type SleepFn = Box<dyn Fn(Duration) -> BoxedLocal<Result<(), String>>>;
120
121fn system_sleep(d: Duration) -> BoxedLocal<Result<(), String>> {
123 async move {
124 thread::sleep(d);
125 Ok(())
126 }
127 .boxed_local()
128}
129
130pub struct SleepCommand {
132 metadata: Rc<CallableMetadata>,
133 sleep_fn: SleepFn,
134}
135
136impl SleepCommand {
137 pub fn new(sleep_fn: SleepFn) -> Rc<Self> {
139 Rc::from(Self {
140 metadata: CallableMetadataBuilder::new("SLEEP")
141 .with_async(true)
142 .with_syntax(&[(
143 &[SingularArgSyntax::RequiredValue(
144 RequiredValueSyntax {
145 name: Cow::Borrowed("seconds"),
146 vtype: ExprType::Double,
147 },
148 ArgSepSyntax::End,
149 )],
150 None,
151 )])
152 .with_category(CATEGORY)
153 .with_description(
154 "Suspends program execution.
155Pauses program execution for the given number of seconds, which can be specified either as an \
156integer or as a floating point number for finer precision.",
157 )
158 .build(),
159 sleep_fn,
160 })
161 }
162}
163
164#[async_trait(?Send)]
165impl Callable for SleepCommand {
166 fn metadata(&self) -> Rc<CallableMetadata> {
167 self.metadata.clone()
168 }
169
170 async fn async_exec(&self, scope: Scope<'_>) -> CallResult<()> {
171 debug_assert_eq!(1, scope.nargs());
172 let n = scope.get_double(0);
173
174 if n < 0.0 {
175 return Err(CallError::Syntax(
176 scope.get_pos(0),
177 "Sleep time must be positive".to_owned(),
178 ));
179 }
180
181 (self.sleep_fn)(Duration::from_secs_f64(n))
182 .await
183 .map_err(|e| CallError::Syntax(scope.get_pos(0), e))
184 }
185}
186
187pub fn add_scripting(machine: &mut MachineBuilder, sleep_fn: Option<SleepFn>) {
192 machine.add_callable(ErrmsgFunction::new());
193 machine.add_callable(SleepCommand::new(sleep_fn.unwrap_or_else(|| Box::from(system_sleep))));
194}
195
196pub fn add_interactive(machine: &mut MachineBuilder) {
198 machine.add_callable(ClearCommand::new(machine.actions()));
199}
200
201#[cfg(test)]
202mod tests {
203 use super::*;
204 use crate::testutils::*;
205 use crate::{Error, MachineBuilder, Signal, Yielder};
206 use futures_lite::future::block_on;
207 use std::cell::RefCell;
208 use std::rc::Rc;
209 use std::time::Instant;
210
211 #[test]
212 fn test_clear_ok() {
213 Tester::default().run("a = 1: CLEAR").expect_clear().check();
214 Tester::default()
215 .run_n(&["DIM a(2): CLEAR", "DIM a(5) AS STRING: CLEAR"])
216 .expect_clear()
217 .expect_clear()
218 .check();
219 }
220
221 #[test]
222 fn test_clear_inside_gosub_stops_execution() {
223 Tester::default().run("GOSUB @sub: END\n@sub:\nCLEAR").expect_clear().check();
226 }
227
228 #[test]
229 fn test_clear_inside_sub_stops_execution() {
230 Tester::default()
234 .run("SUB foo: PRINT 3: CLEAR: PRINT 5: END SUB: foo: foo")
235 .expect_prints([" 3"])
236 .expect_clear()
237 .check();
238 }
239
240 #[test]
241 fn test_clear_errors() {
242 check_stmt_compilation_err("1:1: CLEAR expected no arguments", "CLEAR 123");
243 }
244
245 #[test]
246 fn test_errmsg_before_error() {
247 check_expr_ok("", r#"ERRMSG"#);
248 }
249
250 #[test]
251 fn test_errmsg_after_error() {
252 Tester::default()
253 .run("ON ERROR RESUME NEXT: COLOR -1: PRINT \"Captured: \"; ERRMSG")
254 .expect_prints(["Captured: 1:29: Color out of range"])
255 .check();
256 }
257
258 #[test]
259 fn test_errmsg_errors() {
260 check_expr_compilation_error("1:10: ERRMSG expected no arguments", r#"ERRMSG()"#);
261 check_expr_compilation_error("1:10: ERRMSG expected no arguments", r#"ERRMSG(3)"#);
262 }
263
264 #[test]
265 fn test_sleep_ok_int() {
266 let sleep_fake = |d: Duration| -> BoxedLocal<Result<(), String>> {
267 async move { Err(format!("Got {} ms", d.as_millis())) }.boxed_local()
268 };
269
270 let mut machine = MachineBuilder::default().with_sleep_fn(Box::from(sleep_fake)).build();
271 machine.compile(&mut "SLEEP 123".as_bytes()).unwrap();
272 assert_eq!("1:7: Got 123000 ms", format!("{}", block_on(machine.exec()).unwrap_err()));
273 }
274
275 #[test]
276 fn test_sleep_ok_float() {
277 let sleep_fake = |d: Duration| -> BoxedLocal<Result<(), String>> {
278 async move {
279 let ms = d.as_millis();
280 if ms > 123095 && ms < 123105 {
281 Err("Good".to_owned())
282 } else {
283 Err(format!("Bad {}", ms))
284 }
285 }
286 .boxed_local()
287 };
288
289 let mut machine = MachineBuilder::default().with_sleep_fn(Box::from(sleep_fake)).build();
290 machine.compile(&mut "SLEEP 123.1".as_bytes()).unwrap();
291 assert_eq!("1:7: Good", format!("{}", block_on(machine.exec()).unwrap_err()));
292 }
293
294 #[test]
295 fn test_sleep_real() {
296 let before = Instant::now();
297 Tester::default().run("SLEEP 0.010").check();
298 assert!(before.elapsed() >= Duration::from_millis(10));
299 }
300
301 #[test]
302 fn test_sleep_errors() {
303 check_stmt_compilation_err("1:1: SLEEP expected seconds#", "SLEEP");
304 check_stmt_compilation_err("1:1: SLEEP expected seconds#", "SLEEP 2, 3");
305 check_stmt_compilation_err("1:1: SLEEP expected seconds#", "SLEEP 2; 3");
306 check_stmt_compilation_err("1:7: STRING is not a number", "SLEEP \"foo\"");
307 check_stmt_err("1:7: Sleep time must be positive", "SLEEP -1");
308 check_stmt_err("1:7: Sleep time must be positive", "SLEEP -0.001");
309 }
310
311 #[test]
312 fn test_break_stops_after_upcall() {
313 let (tx, rx) = async_channel::unbounded();
314 let break_tx = tx.clone();
315 let sleep_fake = move |_d: Duration| -> BoxedLocal<Result<(), String>> {
316 let break_tx = break_tx.clone();
317 async move {
318 break_tx.send(Signal::Break).await.unwrap();
319 Ok(())
320 }
321 .boxed_local()
322 };
323
324 let mut machine = MachineBuilder::default()
325 .with_signals_chan((tx.clone(), rx))
326 .with_sleep_fn(Box::from(sleep_fake))
327 .build();
328 machine.compile(&mut "DO: SLEEP 0: LOOP".as_bytes()).unwrap();
329
330 match block_on(machine.exec()) {
331 Err(Error::Break) => (),
332 r => panic!("Expected Break but got {:?}", r),
333 }
334 assert_eq!(0, tx.len());
335 }
336
337 #[test]
338 fn test_yielder_called_on_stop_reason_yield() {
339 struct CountingYielder {
340 count: Rc<RefCell<usize>>,
341 }
342
343 #[async_trait(?Send)]
344 impl Yielder for CountingYielder {
345 async fn yield_now(&mut self) {
346 *self.count.borrow_mut() += 1;
347 }
348 }
349
350 let (tx, rx) = async_channel::unbounded();
351 let yield_count = Rc::from(RefCell::from(0));
352
353 let mut machine = MachineBuilder::default()
354 .with_signals_chan((tx.clone(), rx))
355 .with_yielder(Box::new(CountingYielder { count: yield_count.clone() }))
356 .build();
357
358 block_on(tx.send(Signal::Break)).unwrap();
359 machine.compile(&mut "@here: GOTO @here".as_bytes()).unwrap();
360 match block_on(machine.exec()) {
361 Err(Error::Break) => (),
362 r => panic!("Expected Break but got {:?}", r),
363 }
364
365 assert_eq!(1, *yield_count.borrow());
366 }
367
368 #[test]
369 fn test_drain_signals_ignores_pending_break() {
370 let (tx, rx) = async_channel::unbounded();
371 let mut machine = MachineBuilder::default().with_signals_chan((tx.clone(), rx)).build();
372
373 block_on(tx.send(Signal::Break)).unwrap();
374 machine.drain_signals();
375
376 machine.compile(&mut "a = 1".as_bytes()).unwrap();
377 match block_on(machine.exec()) {
378 Ok(None) => (),
379 r => panic!("Expected Ok(None) but got {:?}", r),
380 }
381 assert_eq!(0, tx.len());
382 }
383
384 fn do_no_check_stop_test(code: &str) {
385 let (tx, rx) = async_channel::unbounded();
386 let mut machine = MachineBuilder::default().with_signals_chan((tx.clone(), rx)).build();
387
388 block_on(tx.send(Signal::Break)).unwrap();
389
390 machine.compile(&mut code.as_bytes()).unwrap();
391 match block_on(machine.exec()) {
392 Ok(None) => (),
393 r => panic!("Expected Ok(None) but got {:?}", r),
394 }
395
396 assert_eq!(1, tx.len());
397 }
398
399 fn do_check_stop_test(code: &str) {
400 let (tx, rx) = async_channel::unbounded();
401 let mut machine = MachineBuilder::default().with_signals_chan((tx.clone(), rx)).build();
402
403 block_on(tx.send(Signal::Break)).unwrap();
404
405 machine.compile(&mut code.as_bytes()).unwrap();
406 match block_on(machine.exec()) {
407 Err(Error::Break) => (),
408 r => panic!("Expected Break but got {:?}", r),
409 }
410
411 assert_eq!(0, tx.len());
412 }
413
414 #[test]
415 fn test_goto_forward_does_not_check_stop() {
416 do_no_check_stop_test("GOTO @after: a = 1: @after");
417 }
418
419 #[test]
420 fn test_if_taken_does_not_check_stop() {
421 do_no_check_stop_test("a = 3: IF a = 3 THEN b = 0 ELSE b = 1: a = 7");
422 }
423
424 #[test]
425 fn test_if_not_taken_does_not_check_stop() {
426 do_no_check_stop_test("a = 3: IF a = 5 THEN b = 0 ELSE b = 1: a = 7");
427 }
428
429 #[test]
430 fn test_goto_checks_stop() {
431 do_check_stop_test("@here: GOTO @here");
432 do_check_stop_test("@before: a = 1: GOTO @before");
433 }
434
435 #[test]
436 fn test_gosub_checks_stop() {
437 do_check_stop_test("GOTO @skip: @sub: a = 1: RETURN: @skip: GOSUB @sub: a = 1");
438 }
439
440 #[test]
441 fn test_do_checks_stop() {
442 do_check_stop_test("DO: LOOP");
443 do_check_stop_test("DO: a = 1: LOOP");
444
445 do_check_stop_test("DO UNTIL FALSE: LOOP");
446 do_check_stop_test("DO UNTIL FALSE: a = 1: LOOP");
447
448 do_check_stop_test("DO WHILE TRUE: LOOP");
449 do_check_stop_test("DO WHILE TRUE: a = 1: LOOP");
450
451 do_check_stop_test("DO: LOOP UNTIL FALSE");
452 do_check_stop_test("DO: a = 1: LOOP UNTIL FALSE");
453
454 do_check_stop_test("DO: LOOP WHILE TRUE");
455 do_check_stop_test("DO: a = 1: LOOP WHILE TRUE");
456 }
457
458 #[test]
459 fn test_for_checks_stop() {
460 do_check_stop_test("FOR a = 1 TO 10: NEXT");
461 do_check_stop_test("FOR a = 1 TO 10: b = 2: NEXT");
462 }
463
464 #[test]
465 fn test_while_checks_stop() {
466 do_check_stop_test("WHILE TRUE: WEND");
467 do_check_stop_test("WHILE TRUE: a = 1: WEND");
468 }
469}