endbasic_std/
exec.rs

1// EndBASIC
2// Copyright 2021 Julio Merino
3//
4// Licensed under the Apache License, Version 2.0 (the "License"); you may not
5// use this file except in compliance with the License.  You may obtain a copy
6// of the License at:
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
13// License for the specific language governing permissions and limitations
14// under the License.
15
16//! Commands that manipulate the machine's state or the program's execution.
17
18use 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
32/// Category description for all symbols provided by this module.
33pub(crate) const CATEGORY: &str = "Interpreter";
34
35/// The `CLEAR` command.
36pub struct ClearCommand {
37    metadata: CallableMetadata,
38}
39
40impl ClearCommand {
41    /// Creates a new `CLEAR` command that resets the state of the machine.
42    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
74/// The `ERRMSG` function.
75pub struct ErrmsgFunction {
76    metadata: CallableMetadata,
77}
78
79impl ErrmsgFunction {
80    /// Creates a new instance of the function.
81    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
114/// Type of the sleep function used by the `SLEEP` command to actually suspend execution.
115pub type SleepFn = Box<dyn Fn(Duration, LineCol) -> BoxedLocal<CallResult>>;
116
117/// An implementation of a `SleepFn` that stops the current thread.
118fn system_sleep(d: Duration, _pos: LineCol) -> BoxedLocal<CallResult> {
119    async move {
120        thread::sleep(d);
121        Ok(())
122    }
123    .boxed_local()
124}
125
126/// The `SLEEP` command.
127pub struct SleepCommand {
128    metadata: CallableMetadata,
129    sleep_fn: SleepFn,
130}
131
132impl SleepCommand {
133    /// Creates a new instance of the command.
134    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
177/// Instantiates all REPL commands for the scripting machine and adds them to the `machine`.
178///
179/// `sleep_fn` is an async function that implements a pause given a `Duration`.  If not provided,
180/// uses the `std::thread::sleep` function.
181pub 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
186/// Instantiates all REPL commands for the interactive machine and adds them to the `machine`.
187pub 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}