Skip to main content

endbasic_repl/
lib.rs

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