Skip to main content

endbasic_std/
exec.rs

1// EndBASIC
2// Copyright 2021 Julio Merino
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU Affero General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU Affero General Public License for more details.
13//
14// You should have received a copy of the GNU Affero General Public License
15// along with this program.  If not, see <https://www.gnu.org/licenses/>.
16
17//! Commands that manipulate the machine's state or the program's execution.
18
19use 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
33/// Category description for all symbols provided by this module.
34pub(crate) const CATEGORY: &str = "Interpreter";
35
36/// The `CLEAR` command.
37pub struct ClearCommand {
38    metadata: Rc<CallableMetadata>,
39    actions: Rc<RefCell<Vec<MachineAction>>>,
40}
41
42impl ClearCommand {
43    /// Creates a new `CLEAR` command that resets the state of the machine.
44    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
77/// The `ERRMSG` function.
78pub struct ErrmsgFunction {
79    metadata: Rc<CallableMetadata>,
80}
81
82impl ErrmsgFunction {
83    /// Creates a new instance of the function.
84    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
118/// Type of the sleep function used by the `SLEEP` command to actually suspend execution.
119pub type SleepFn = Box<dyn Fn(Duration) -> BoxedLocal<Result<(), String>>>;
120
121/// An implementation of a `SleepFn` that stops the current thread.
122fn system_sleep(d: Duration) -> BoxedLocal<Result<(), String>> {
123    async move {
124        thread::sleep(d);
125        Ok(())
126    }
127    .boxed_local()
128}
129
130/// The `SLEEP` command.
131pub struct SleepCommand {
132    metadata: Rc<CallableMetadata>,
133    sleep_fn: SleepFn,
134}
135
136impl SleepCommand {
137    /// Creates a new instance of the command.
138    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
187/// Instantiates all REPL commands for the scripting machine and adds them to the `machine`.
188///
189/// `sleep_fn` is an async function that implements a pause given a `Duration`.  If not provided,
190/// uses the `std::thread::sleep` function.
191pub 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
196/// Instantiates all REPL commands for the interactive machine and adds them to the `machine`.
197pub 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        // TODO(jmmv): CLEAR should not stop execution; these assertions only
224        // document current behavior.
225        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        // TODO(jmmv): CLEAR should not stop execution; PRINT 5 and the second
231        // foo call should also execute.  These assertions only document current
232        // behavior where CLEAR terminates the program.
233        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}