Skip to main content

endbasic_repl/
lib.rs

1// EndBASIC
2// Copyright 2020 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//! Interactive interpreter for the EndBASIC language.
17
18use endbasic_core::exec::{Machine, StopReason};
19use endbasic_std::console::{self, Console, is_narrow, refill_and_print};
20use endbasic_std::program::{BREAK_MSG, Program, continue_if_modified};
21use endbasic_std::storage::Storage;
22use std::cell::RefCell;
23use std::io;
24use std::rc::Rc;
25
26pub mod demos;
27pub mod editor;
28
29/// Prints the EndBASIC welcome message to the given console.
30pub fn print_welcome(console: Rc<RefCell<dyn Console>>) -> io::Result<()> {
31    let mut console = console.borrow_mut();
32
33    if is_narrow(&*console) {
34        console.print(&format!("EndBASIC {}", env!("CARGO_PKG_VERSION")))?;
35    } else {
36        console.print("")?;
37        console.print(&format!("    EndBASIC {}", env!("CARGO_PKG_VERSION")))?;
38        console.print("    Copyright 2020-2026 Julio Merino")?;
39        console.print("")?;
40        console.print("    Type HELP for interactive usage information.")?;
41    }
42    console.print("")?;
43
44    Ok(())
45}
46
47/// Loads the `AUTOEXEC.BAS` file if it exists in the `drive`.
48///
49/// Failures to process the file are logged to the `console` but are ignored.  Other failures are
50/// returned.
51pub async fn try_load_autoexec(
52    machine: &mut Machine,
53    console: Rc<RefCell<dyn Console>>,
54    storage: Rc<RefCell<Storage>>,
55) -> io::Result<()> {
56    let code = match storage.borrow().get("AUTOEXEC.BAS").await {
57        Ok(code) => code,
58        Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
59        Err(e) => {
60            return console
61                .borrow_mut()
62                .print(&format!("AUTOEXEC.BAS exists but cannot be read: {}", e));
63        }
64    };
65
66    match machine.exec(&mut code.as_slice()).await {
67        Ok(_) => Ok(()),
68        Err(e) => {
69            console.borrow_mut().print(&format!("AUTOEXEC.BAS failed: {}", e))?;
70            Ok(())
71        }
72    }
73}
74
75/// Loads the program given in `username_path` pair (which is of the form `user/path`) from the
76/// cloud and executes it on the `machine`.
77pub async fn run_from_cloud(
78    machine: &mut Machine,
79    console: Rc<RefCell<dyn Console>>,
80    storage: Rc<RefCell<Storage>>,
81    program: Rc<RefCell<dyn Program>>,
82    username_path: &str,
83    will_run_repl: bool,
84) -> io::Result<i32> {
85    let (fs_uri, path) = match username_path.split_once('/') {
86        Some((username, path)) => (format!("cloud://{}", username), format!("AUTORUN:/{}", path)),
87        None => {
88            let mut console = console.borrow_mut();
89            console.print(&format!(
90                "Invalid program to run '{}'; must be of the form 'username/path'",
91                username_path
92            ))?;
93            return Ok(1);
94        }
95    };
96
97    console.borrow_mut().print(&format!("Mounting {} as AUTORUN...", fs_uri))?;
98    storage.borrow_mut().mount("AUTORUN", &fs_uri)?;
99    storage.borrow_mut().cd("AUTORUN:/")?;
100
101    console.borrow_mut().print(&format!("Loading {}...", path))?;
102    let content = storage.borrow().get(&path).await?;
103    let content = match String::from_utf8(content) {
104        Ok(text) => text,
105        Err(e) => {
106            let mut console = console.borrow_mut();
107            console.print(&format!("Invalid program to run '{}': {}", path, e))?;
108            return Ok(1);
109        }
110    };
111    program.borrow_mut().load(Some(&path), &content);
112
113    console.borrow_mut().print("Starting...")?;
114    console.borrow_mut().print("")?;
115
116    let result = machine.exec(&mut "RUN".as_bytes()).await;
117
118    let mut console = console.borrow_mut();
119
120    console.print("")?;
121    let code = match result {
122        Ok(r @ StopReason::Eof) => {
123            console.print("**** Program exited due to EOF ****")?;
124            r.as_exit_code()
125        }
126        Ok(r @ StopReason::Exited(_)) => {
127            let code = r.as_exit_code();
128            console.print(&format!("**** Program exited with code {} ****", code))?;
129            code
130        }
131        Ok(r @ StopReason::Break) => {
132            console.print("**** Program stopped due to BREAK ****")?;
133            r.as_exit_code()
134        }
135        Err(e) => {
136            console.print(&format!("**** ERROR: {} ****", e))?;
137            1
138        }
139    };
140
141    if will_run_repl {
142        console.print("")?;
143        refill_and_print(
144            &mut *console,
145            [
146                "You are now being dropped into the EndBASIC interpreter.",
147                "The program you asked to run is still loaded in memory and you can interact with \
148it now.  Use LIST to view the source code, EDIT to launch an editor on the source code, and RUN to \
149execute the program again.",
150                "Type HELP for interactive usage information.",
151            ],
152            "   ",
153        )?;
154        console.print("")?;
155    }
156
157    Ok(code)
158}
159
160/// Enters the interactive interpreter.
161///
162/// The `console` provided here is used for the REPL prompt interaction and should match the
163/// console that's in use by the machine (if any).  They don't necessarily have to match though.
164pub async fn run_repl_loop(
165    machine: &mut Machine,
166    console: Rc<RefCell<dyn Console>>,
167    program: Rc<RefCell<dyn Program>>,
168) -> io::Result<i32> {
169    let mut stop_reason = StopReason::Eof;
170    let mut history = vec![];
171    while stop_reason == StopReason::Eof {
172        let line = {
173            let mut console = console.borrow_mut();
174            if console.is_interactive() {
175                console.print("Ready")?;
176            }
177            console::read_line(&mut *console, "", "", Some(&mut history)).await
178        };
179
180        // Any signals entered during console input should not impact upcoming execution.  Drain
181        // them all.
182        machine.drain_signals();
183
184        match line {
185            Ok(line) => match machine.exec(&mut line.as_bytes()).await {
186                Ok(reason) => stop_reason = reason,
187                Err(e) => {
188                    let mut console = console.borrow_mut();
189                    console.print(format!("ERROR: {}", e).as_str())?;
190                }
191            },
192            Err(e) => {
193                if e.kind() == io::ErrorKind::Interrupted {
194                    let mut console = console.borrow_mut();
195                    console.print(BREAK_MSG)?;
196                    // Do not exit the interpreter.  Other REPLs, such as Python's, do not do so,
197                    // and it is actually pretty annoying to exit the REPL when one may be furiously
198                    // pressing CTRL+C to stop a program inside of it.
199                } else if e.kind() == io::ErrorKind::UnexpectedEof {
200                    let mut console = console.borrow_mut();
201                    console.print("End of input by CTRL-D")?;
202                    stop_reason = StopReason::Exited(0);
203                } else {
204                    stop_reason = StopReason::Exited(1);
205                }
206            }
207        }
208
209        match stop_reason {
210            StopReason::Eof => (),
211            StopReason::Break => {
212                console.borrow_mut().print("**** BREAK ****")?;
213                stop_reason = StopReason::Eof;
214            }
215            StopReason::Exited(_) => {
216                if !continue_if_modified(&*program.borrow(), &mut *console.borrow_mut()).await? {
217                    console.borrow_mut().print("Exit aborted; resuming REPL loop.")?;
218                    stop_reason = StopReason::Eof;
219                }
220            }
221        }
222    }
223    Ok(stop_reason.as_exit_code())
224}
225
226#[cfg(test)]
227mod tests {
228    use super::*;
229    use endbasic_core::exec::Signal;
230    use endbasic_std::console::{CharsXY, Key};
231    use endbasic_std::storage::{Drive, DriveFactory, InMemoryDrive};
232    use endbasic_std::testutils::*;
233    use futures_lite::future::block_on;
234    use std::convert::TryFrom;
235
236    /// Runs `print_welcome` against a console that is `console_width` in height and returns
237    /// whether the narrow welcome message was printed or not, and the maximum width of all
238    /// printed messages.
239    fn check_is_narrow_welcome(console_width: u16) -> (bool, usize) {
240        let console = Rc::from(RefCell::from(MockConsole::default()));
241        console.borrow_mut().set_size_chars(CharsXY::new(console_width, 1));
242        print_welcome(console.clone()).unwrap();
243
244        let mut console = console.borrow_mut();
245        let mut found = false;
246        let mut max_length = 0;
247        for output in console.take_captured_out() {
248            match output {
249                CapturedOut::Print(msg) => {
250                    if msg.contains("Type HELP") {
251                        found = true;
252                        max_length = std::cmp::max(max_length, msg.len());
253                    }
254                }
255                _ => panic!("Unexpected console operation: {:?}", output),
256            }
257        }
258        (!found, max_length)
259    }
260
261    #[test]
262    fn test_print_welcome_wide_console() {
263        assert!(!check_is_narrow_welcome(50).0, "Long welcome not found");
264    }
265
266    #[test]
267    fn test_print_welcome_narrow_console() {
268        assert!(check_is_narrow_welcome(10).0, "Long welcome found");
269    }
270
271    #[test]
272    fn test_print_welcome_and_is_narrow_agree() {
273        let (narrow, max_length) = check_is_narrow_welcome(1000);
274        assert!(!narrow, "Long message not found");
275
276        for i in 0..max_length {
277            assert!(check_is_narrow_welcome(u16::try_from(i).unwrap()).0, "Long message found");
278        }
279    }
280
281    #[test]
282    fn test_autoexec_ok() {
283        // The code in the autoexec test file should access, in a mutable fashion, all the resources
284        // that the try_load_autoexec function uses to ensure the function's code doesn't hold onto
285        // references while executing the autoexec code and causing a borrowing violation.
286        let autoexec = "PRINT \"hello\": global_var = 3: CD \"MEMORY:/\"";
287        let mut tester = Tester::default().write_file("AUTOEXEC.BAS", autoexec);
288        let (console, storage) = (tester.get_console(), tester.get_storage());
289        block_on(try_load_autoexec(tester.get_machine(), console, storage)).unwrap();
290        tester
291            .run("")
292            .expect_var("global_var", 3)
293            .expect_prints(["hello"])
294            .expect_file("MEMORY:/AUTOEXEC.BAS", autoexec)
295            .check();
296    }
297
298    #[test]
299    fn test_autoexec_compilation_error_is_ignored() {
300        let autoexec = "a = 1\nb = undef: c = 2";
301        let mut tester = Tester::default().write_file("AUTOEXEC.BAS", autoexec);
302        let (console, storage) = (tester.get_console(), tester.get_storage());
303        block_on(try_load_autoexec(tester.get_machine(), console, storage)).unwrap();
304        tester
305            .run("after = 5")
306            .expect_var("after", 5)
307            .expect_prints(["AUTOEXEC.BAS failed: 2:5: Undefined symbol undef"])
308            .expect_file("MEMORY:/AUTOEXEC.BAS", autoexec)
309            .check();
310    }
311
312    #[test]
313    fn test_autoexec_execution_error_is_ignored() {
314        let autoexec = "a = 1\nb = 3 >> -1: c = 2";
315        let mut tester = Tester::default().write_file("AUTOEXEC.BAS", autoexec);
316        let (console, storage) = (tester.get_console(), tester.get_storage());
317        block_on(try_load_autoexec(tester.get_machine(), console, storage)).unwrap();
318        tester
319            .run("after = 5")
320            .expect_var("a", 1)
321            .expect_var("after", 5)
322            .expect_prints(["AUTOEXEC.BAS failed: 2:7: Number of bits to >> (-1) must be positive"])
323            .expect_file("MEMORY:/AUTOEXEC.BAS", autoexec)
324            .check();
325    }
326
327    #[test]
328    fn test_autoexec_name_is_case_sensitive() {
329        let mut tester = Tester::default()
330            .write_file("AUTOEXEC.BAS", "a = 1")
331            .write_file("autoexec.bas", "a = 2");
332        let (console, storage) = (tester.get_console(), tester.get_storage());
333        block_on(try_load_autoexec(tester.get_machine(), console, storage)).unwrap();
334        tester
335            .run("")
336            .expect_var("a", 1)
337            .expect_file("MEMORY:/AUTOEXEC.BAS", "a = 1")
338            .expect_file("MEMORY:/autoexec.bas", "a = 2")
339            .check();
340    }
341
342    #[test]
343    fn test_autoexec_missing() {
344        let mut tester = Tester::default();
345        let (console, storage) = (tester.get_console(), tester.get_storage());
346        block_on(try_load_autoexec(tester.get_machine(), console, storage)).unwrap();
347        tester.run("").check();
348    }
349
350    /// Factory for drives that mimic the behavior of a cloud drive with fixed contents.
351    struct MockDriveFactory {
352        exp_username: &'static str,
353        exp_file: &'static str,
354    }
355
356    impl MockDriveFactory {
357        /// Verbatim contents of the single file included in the mock drives.
358        const SCRIPT: &'static str = r#"PRINT "Success""#;
359    }
360
361    impl DriveFactory for MockDriveFactory {
362        fn create(&self, target: &str) -> io::Result<Box<dyn Drive>> {
363            let mut drive = InMemoryDrive::default();
364            block_on(drive.put(self.exp_file, Self::SCRIPT.as_bytes())).unwrap();
365            assert_eq!(self.exp_username, target);
366            Ok(Box::from(drive))
367        }
368    }
369
370    #[test]
371    fn test_run_from_cloud_no_repl() {
372        let mut tester = Tester::default();
373        let (console, storage, program) =
374            (tester.get_console(), tester.get_storage(), tester.get_program());
375
376        storage.borrow_mut().register_scheme(
377            "cloud",
378            Box::from(MockDriveFactory { exp_username: "foo", exp_file: "bar.bas" }),
379        );
380
381        block_on(run_from_cloud(
382            tester.get_machine(),
383            console,
384            storage,
385            program,
386            "foo/bar.bas",
387            false,
388        ))
389        .unwrap();
390        tester
391            .run("")
392            .expect_prints([
393                "Mounting cloud://foo as AUTORUN...",
394                "Loading AUTORUN:/bar.bas...",
395                "Starting...",
396                "",
397            ])
398            .expect_clear()
399            .expect_prints(["Success", "", "**** Program exited due to EOF ****"])
400            .expect_program(Some("AUTORUN:/bar.bas"), MockDriveFactory::SCRIPT)
401            .check();
402    }
403
404    #[test]
405    fn test_run_from_cloud_repl() {
406        let mut tester = Tester::default();
407        let (console, storage, program) =
408            (tester.get_console(), tester.get_storage(), tester.get_program());
409
410        storage.borrow_mut().register_scheme(
411            "cloud",
412            Box::from(MockDriveFactory { exp_username: "abcd", exp_file: "the-path.bas" }),
413        );
414
415        block_on(run_from_cloud(
416            tester.get_machine(),
417            console,
418            storage,
419            program,
420            "abcd/the-path.bas",
421            true,
422        ))
423        .unwrap();
424        let mut checker = tester.run("");
425        let output = flatten_output(checker.take_captured_out());
426        checker.expect_program(Some("AUTORUN:/the-path.bas"), MockDriveFactory::SCRIPT).check();
427
428        assert!(output.contains("You are now being dropped into"));
429    }
430
431    #[test]
432    fn test_run_repl_loop_signal_before_exec() {
433        let mut tester = Tester::default();
434        let (console, program) = (tester.get_console(), tester.get_program());
435        let signals_tx = tester.get_machine().get_signals_tx();
436
437        {
438            let mut console = console.borrow_mut();
439            console.add_input_chars("PRINT");
440            block_on(signals_tx.send(Signal::Break)).unwrap();
441            console.add_input_chars(" 123");
442            console.add_input_keys(&[Key::NewLine, Key::Eof]);
443        }
444        block_on(run_repl_loop(tester.get_machine(), console, program)).unwrap();
445        tester.run("").expect_prints([" 123", "End of input by CTRL-D"]).check();
446    }
447}