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    CallResult, Callable, CallableMetadata, CallableMetadataBuilder, ExprType, Scope,
22};
23use std::cell::RefCell;
24use std::rc::Rc;
25
26use crate::{MachineAction, MachineBuilder};
27
28/// Category description for all symbols provided by this module.
29pub(crate) const CATEGORY: &str = "Interpreter";
30
31/// The `CLEAR` command.
32pub struct ClearCommand {
33    metadata: Rc<CallableMetadata>,
34    actions: Rc<RefCell<Vec<MachineAction>>>,
35}
36
37impl ClearCommand {
38    /// Creates a new `CLEAR` command that resets the state of the machine.
39    pub fn new(actions: Rc<RefCell<Vec<MachineAction>>>) -> Rc<Self> {
40        Rc::from(Self {
41            metadata: CallableMetadataBuilder::new("CLEAR")
42                .with_async(true)
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            actions,
56        })
57    }
58}
59
60#[async_trait(?Send)]
61impl Callable for ClearCommand {
62    fn metadata(&self) -> Rc<CallableMetadata> {
63        self.metadata.clone()
64    }
65
66    async fn async_exec(&self, _scope: Scope<'_>) -> CallResult<()> {
67        self.actions.borrow_mut().push(MachineAction::Clear);
68        Ok(())
69    }
70}
71
72/// The `ERRMSG` function.
73pub struct ErrmsgFunction {
74    metadata: Rc<CallableMetadata>,
75}
76
77impl ErrmsgFunction {
78    /// Creates a new instance of the function.
79    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) -> Rc<CallableMetadata> {
99        self.metadata.clone()
100    }
101
102    fn exec(&self, scope: Scope<'_>) -> CallResult<()> {
103        debug_assert_eq!(0, scope.nargs());
104
105        let message = scope
106            .last_error()
107            .map(|(pos, message)| format!("{}: {}", pos, message))
108            .unwrap_or_default();
109        scope.return_string(message)
110    }
111}
112
113/// Instantiates all REPL commands for the scripting machine and adds them to the `machine`.
114pub fn add_scripting(machine: &mut MachineBuilder) {
115    machine.add_callable(ErrmsgFunction::new());
116}
117
118/// Instantiates all REPL commands for the interactive machine and adds them to the `machine`.
119pub fn add_interactive(machine: &mut MachineBuilder) {
120    machine.add_callable(ClearCommand::new(machine.actions()));
121}
122
123#[cfg(test)]
124mod tests {
125    use crate::testutils::*;
126    use crate::{Error, MachineBuilder, Signal, Yielder};
127    use async_trait::async_trait;
128    use futures_lite::FutureExt;
129    use futures_lite::future::block_on;
130    use std::cell::RefCell;
131    use std::rc::Rc;
132    use std::time::Duration;
133
134    #[test]
135    fn test_clear_ok() {
136        Tester::default().run("a = 1: CLEAR").expect_clear().check();
137        Tester::default()
138            .run_n(&["DIM a(2): CLEAR", "DIM a(5) AS STRING: CLEAR"])
139            .expect_clear()
140            .expect_clear()
141            .check();
142    }
143
144    #[test]
145    fn test_clear_inside_gosub_stops_execution() {
146        // TODO(jmmv): CLEAR should not stop execution; these assertions only
147        // document current behavior.
148        Tester::default().run("GOSUB @sub: END\n@sub:\nCLEAR").expect_clear().check();
149    }
150
151    #[test]
152    fn test_clear_inside_sub_stops_execution() {
153        // TODO(jmmv): CLEAR should not stop execution; PRINT 5 and the second
154        // foo call should also execute.  These assertions only document current
155        // behavior where CLEAR terminates the program.
156        Tester::default()
157            .run("SUB foo: PRINT 3: CLEAR: PRINT 5: END SUB: foo: foo")
158            .expect_prints([" 3"])
159            .expect_clear()
160            .check();
161    }
162
163    #[test]
164    fn test_clear_errors() {
165        check_stmt_compilation_err("1:1: CLEAR expected no arguments", "CLEAR 123");
166    }
167
168    #[test]
169    fn test_errmsg_before_error() {
170        check_expr_ok("", r#"ERRMSG"#);
171    }
172
173    #[test]
174    fn test_errmsg_after_error() {
175        Tester::default()
176            .run("ON ERROR RESUME NEXT: COLOR -1: PRINT \"Captured: \"; ERRMSG")
177            .expect_prints(["Captured: 1:29: Color out of range"])
178            .check();
179    }
180
181    #[test]
182    fn test_errmsg_errors() {
183        check_expr_compilation_error("1:10: ERRMSG expected no arguments", r#"ERRMSG()"#);
184        check_expr_compilation_error("1:10: ERRMSG expected no arguments", r#"ERRMSG(3)"#);
185    }
186
187    #[test]
188    fn test_break_stops_after_upcall() {
189        let (tx, rx) = async_channel::unbounded();
190        let break_tx = tx.clone();
191        let datetime = Rc::from(MockDateTime::default());
192        datetime.set_sleep_fn(Box::new(
193            move |_d: Duration| -> futures_lite::future::BoxedLocal<Result<(), String>> {
194                let break_tx = break_tx.clone();
195                async move {
196                    break_tx.send(Signal::Break).await.unwrap();
197                    Ok(())
198                }
199                .boxed_local()
200            },
201        ));
202
203        let mut machine = MachineBuilder::default()
204            .with_signals_chan((tx.clone(), rx))
205            .with_datetime(datetime)
206            .build();
207        machine.compile(&mut "DO: SLEEP 0: LOOP".as_bytes()).unwrap();
208
209        match block_on(machine.exec()) {
210            Err(Error::Break) => (),
211            r => panic!("Expected Break but got {:?}", r),
212        }
213        assert_eq!(0, tx.len());
214    }
215
216    #[test]
217    fn test_yielder_called_on_stop_reason_yield() {
218        struct CountingYielder {
219            count: Rc<RefCell<usize>>,
220        }
221
222        #[async_trait(?Send)]
223        impl Yielder for CountingYielder {
224            async fn yield_now(&mut self) {
225                *self.count.borrow_mut() += 1;
226            }
227        }
228
229        let (tx, rx) = async_channel::unbounded();
230        let yield_count = Rc::from(RefCell::from(0));
231
232        let mut machine = MachineBuilder::default()
233            .with_signals_chan((tx.clone(), rx))
234            .with_yielder(Rc::from(RefCell::from(CountingYielder { count: yield_count.clone() })))
235            .build();
236
237        block_on(tx.send(Signal::Break)).unwrap();
238        machine.compile(&mut "@here: GOTO @here".as_bytes()).unwrap();
239        match block_on(machine.exec()) {
240            Err(Error::Break) => (),
241            r => panic!("Expected Break but got {:?}", r),
242        }
243
244        assert_eq!(1, *yield_count.borrow());
245    }
246
247    #[test]
248    fn test_drain_signals_ignores_pending_break() {
249        let (tx, rx) = async_channel::unbounded();
250        let mut machine = MachineBuilder::default().with_signals_chan((tx.clone(), rx)).build();
251
252        block_on(tx.send(Signal::Break)).unwrap();
253        machine.drain_signals();
254
255        machine.compile(&mut "a = 1".as_bytes()).unwrap();
256        match block_on(machine.exec()) {
257            Ok(None) => (),
258            r => panic!("Expected Ok(None) but got {:?}", r),
259        }
260        assert_eq!(0, tx.len());
261    }
262
263    fn do_no_check_stop_test(code: &str) {
264        let (tx, rx) = async_channel::unbounded();
265        let mut machine = MachineBuilder::default().with_signals_chan((tx.clone(), rx)).build();
266
267        block_on(tx.send(Signal::Break)).unwrap();
268
269        machine.compile(&mut code.as_bytes()).unwrap();
270        match block_on(machine.exec()) {
271            Ok(None) => (),
272            r => panic!("Expected Ok(None) but got {:?}", r),
273        }
274
275        assert_eq!(1, tx.len());
276    }
277
278    fn do_check_stop_test(code: &str) {
279        let (tx, rx) = async_channel::unbounded();
280        let mut machine = MachineBuilder::default().with_signals_chan((tx.clone(), rx)).build();
281
282        block_on(tx.send(Signal::Break)).unwrap();
283
284        machine.compile(&mut code.as_bytes()).unwrap();
285        match block_on(machine.exec()) {
286            Err(Error::Break) => (),
287            r => panic!("Expected Break but got {:?}", r),
288        }
289
290        assert_eq!(0, tx.len());
291    }
292
293    #[test]
294    fn test_goto_forward_does_not_check_stop() {
295        do_no_check_stop_test("GOTO @after: a = 1: @after");
296    }
297
298    #[test]
299    fn test_if_taken_does_not_check_stop() {
300        do_no_check_stop_test("a = 3: IF a = 3 THEN b = 0 ELSE b = 1: a = 7");
301    }
302
303    #[test]
304    fn test_if_not_taken_does_not_check_stop() {
305        do_no_check_stop_test("a = 3: IF a = 5 THEN b = 0 ELSE b = 1: a = 7");
306    }
307
308    #[test]
309    fn test_goto_checks_stop() {
310        do_check_stop_test("@here: GOTO @here");
311        do_check_stop_test("@before: a = 1: GOTO @before");
312    }
313
314    #[test]
315    fn test_gosub_checks_stop() {
316        do_check_stop_test("GOTO @skip: @sub: a = 1: RETURN: @skip: GOSUB @sub: a = 1");
317    }
318
319    #[test]
320    fn test_do_checks_stop() {
321        do_check_stop_test("DO: LOOP");
322        do_check_stop_test("DO: a = 1: LOOP");
323
324        do_check_stop_test("DO UNTIL FALSE: LOOP");
325        do_check_stop_test("DO UNTIL FALSE: a = 1: LOOP");
326
327        do_check_stop_test("DO WHILE TRUE: LOOP");
328        do_check_stop_test("DO WHILE TRUE: a = 1: LOOP");
329
330        do_check_stop_test("DO: LOOP UNTIL FALSE");
331        do_check_stop_test("DO: a = 1: LOOP UNTIL FALSE");
332
333        do_check_stop_test("DO: LOOP WHILE TRUE");
334        do_check_stop_test("DO: a = 1: LOOP WHILE TRUE");
335    }
336
337    #[test]
338    fn test_for_checks_stop() {
339        do_check_stop_test("FOR a = 1 TO 10: NEXT");
340        do_check_stop_test("FOR a = 1 TO 10: b = 2: NEXT");
341    }
342
343    #[test]
344    fn test_while_checks_stop() {
345        do_check_stop_test("WHILE TRUE: WEND");
346        do_check_stop_test("WHILE TRUE: a = 1: WEND");
347    }
348}