Skip to main content

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::LineCol;
20use endbasic_core::ast::ExprType;
21use endbasic_core::compiler::{ArgSepSyntax, RequiredValueSyntax, SingularArgSyntax};
22use endbasic_core::exec::{Error, Machine, Result, Scope};
23use endbasic_core::syms::{Callable, CallableMetadata, CallableMetadataBuilder};
24use futures_lite::future::{BoxedLocal, FutureExt};
25use std::borrow::Cow;
26use std::rc::Rc;
27use std::thread;
28use std::time::Duration;
29
30/// Category description for all symbols provided by this module.
31pub(crate) const CATEGORY: &str = "Interpreter";
32
33/// The `CLEAR` command.
34pub struct ClearCommand {
35    metadata: CallableMetadata,
36}
37
38impl ClearCommand {
39    /// Creates a new `CLEAR` command that resets the state of the machine.
40    pub fn new() -> Rc<Self> {
41        Rc::from(Self {
42            metadata: CallableMetadataBuilder::new("CLEAR")
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        })
56    }
57}
58
59#[async_trait(?Send)]
60impl Callable for ClearCommand {
61    fn metadata(&self) -> &CallableMetadata {
62        &self.metadata
63    }
64
65    async fn exec(&self, scope: Scope<'_>, machine: &mut Machine) -> Result<()> {
66        debug_assert_eq!(0, scope.nargs());
67        machine.clear();
68        Ok(())
69    }
70}
71
72/// The `ERRMSG` function.
73pub struct ErrmsgFunction {
74    metadata: 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) -> &CallableMetadata {
99        &self.metadata
100    }
101
102    async fn exec(&self, scope: Scope<'_>, machine: &mut Machine) -> Result<()> {
103        debug_assert_eq!(0, scope.nargs());
104
105        match machine.last_error() {
106            Some(message) => scope.return_string(message),
107            None => scope.return_string("".to_owned()),
108        }
109    }
110}
111
112/// Type of the sleep function used by the `SLEEP` command to actually suspend execution.
113pub type SleepFn = Box<dyn Fn(Duration, LineCol) -> BoxedLocal<Result<()>>>;
114
115/// An implementation of a `SleepFn` that stops the current thread.
116fn system_sleep(d: Duration, _pos: LineCol) -> BoxedLocal<Result<()>> {
117    async move {
118        thread::sleep(d);
119        Ok(())
120    }
121    .boxed_local()
122}
123
124/// The `SLEEP` command.
125pub struct SleepCommand {
126    metadata: CallableMetadata,
127    sleep_fn: SleepFn,
128}
129
130impl SleepCommand {
131    /// Creates a new instance of the command.
132    pub fn new(sleep_fn: SleepFn) -> Rc<Self> {
133        Rc::from(Self {
134            metadata: CallableMetadataBuilder::new("SLEEP")
135                .with_syntax(&[(
136                    &[SingularArgSyntax::RequiredValue(
137                        RequiredValueSyntax {
138                            name: Cow::Borrowed("seconds"),
139                            vtype: ExprType::Double,
140                        },
141                        ArgSepSyntax::End,
142                    )],
143                    None,
144                )])
145                .with_category(CATEGORY)
146                .with_description(
147                    "Suspends program execution.
148Pauses program execution for the given number of seconds, which can be specified either as an \
149integer or as a floating point number for finer precision.",
150                )
151                .build(),
152            sleep_fn,
153        })
154    }
155}
156
157#[async_trait(?Send)]
158impl Callable for SleepCommand {
159    fn metadata(&self) -> &CallableMetadata {
160        &self.metadata
161    }
162
163    async fn exec(&self, mut scope: Scope<'_>, _machine: &mut Machine) -> Result<()> {
164        debug_assert_eq!(1, scope.nargs());
165        let (n, pos) = scope.pop_double_with_pos();
166
167        if n < 0.0 {
168            return Err(Error::SyntaxError(pos, "Sleep time must be positive".to_owned()));
169        }
170
171        (self.sleep_fn)(Duration::from_secs_f64(n), pos).await
172    }
173}
174
175/// Instantiates all REPL commands for the scripting machine and adds them to the `machine`.
176///
177/// `sleep_fn` is an async function that implements a pause given a `Duration`.  If not provided,
178/// uses the `std::thread::sleep` function.
179pub fn add_scripting(machine: &mut Machine, sleep_fn: Option<SleepFn>) {
180    machine.add_callable(ErrmsgFunction::new());
181    machine.add_callable(SleepCommand::new(sleep_fn.unwrap_or_else(|| Box::from(system_sleep))));
182}
183
184/// Instantiates all REPL commands for the interactive machine and adds them to the `machine`.
185pub fn add_interactive(machine: &mut Machine) {
186    machine.add_callable(ClearCommand::new());
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192    use crate::testutils::*;
193    use std::time::Instant;
194
195    #[test]
196    fn test_clear_ok() {
197        Tester::default().run("a = 1: CLEAR").expect_clear().check();
198        Tester::default()
199            .run_n(&["DIM a(2): CLEAR", "DIM a(5) AS STRING: CLEAR"])
200            .expect_clear()
201            .expect_clear()
202            .check();
203    }
204
205    #[test]
206    fn test_clear_errors() {
207        check_stmt_compilation_err("1:1: CLEAR expected no arguments", "CLEAR 123");
208    }
209
210    #[test]
211    fn test_errmsg_before_error() {
212        check_expr_ok("", r#"ERRMSG"#);
213    }
214
215    #[test]
216    fn test_errmsg_after_error() {
217        Tester::default()
218            .run("ON ERROR RESUME NEXT: COLOR -1: PRINT \"Captured: \"; ERRMSG")
219            .expect_prints(["Captured: 1:29: Color out of range"])
220            .check();
221    }
222
223    #[test]
224    fn test_errmsg_errors() {
225        check_expr_compilation_error("1:10: ERRMSG expected no arguments", r#"ERRMSG()"#);
226        check_expr_compilation_error("1:10: ERRMSG expected no arguments", r#"ERRMSG(3)"#);
227    }
228
229    #[test]
230    fn test_sleep_ok_int() {
231        let sleep_fake = |d: Duration, pos: LineCol| -> BoxedLocal<Result<()>> {
232            async move { Err(Error::InternalError(pos, format!("Got {} ms", d.as_millis()))) }
233                .boxed_local()
234        };
235
236        let mut t = Tester::empty().add_callable(SleepCommand::new(Box::from(sleep_fake)));
237        t.run("SLEEP 123").expect_err("1:7: Got 123000 ms").check();
238    }
239
240    #[test]
241    fn test_sleep_ok_float() {
242        let sleep_fake = |d: Duration, pos: LineCol| -> BoxedLocal<Result<()>> {
243            async move {
244                let ms = d.as_millis();
245                if ms > 123095 && ms < 123105 {
246                    Err(Error::InternalError(pos, "Good".to_owned()))
247                } else {
248                    Err(Error::InternalError(pos, format!("Bad {}", ms)))
249                }
250            }
251            .boxed_local()
252        };
253
254        let mut t = Tester::empty().add_callable(SleepCommand::new(Box::from(sleep_fake)));
255        t.run("SLEEP 123.1").expect_err("1:7: Good").check();
256    }
257
258    #[test]
259    fn test_sleep_real() {
260        let before = Instant::now();
261        Tester::default().run("SLEEP 0.010").check();
262        assert!(before.elapsed() >= Duration::from_millis(10));
263    }
264
265    #[test]
266    fn test_sleep_errors() {
267        check_stmt_compilation_err("1:1: SLEEP expected seconds#", "SLEEP");
268        check_stmt_compilation_err("1:1: SLEEP expected seconds#", "SLEEP 2, 3");
269        check_stmt_compilation_err("1:1: SLEEP expected seconds#", "SLEEP 2; 3");
270        check_stmt_compilation_err("1:7: STRING is not a number", "SLEEP \"foo\"");
271        check_stmt_err("1:7: Sleep time must be positive", "SLEEP -1");
272        check_stmt_err("1:7: Sleep time must be positive", "SLEEP -0.001");
273    }
274}