1#![allow(clippy::await_holding_refcell_ref)]
20#![allow(clippy::collapsible_else_if)]
21#![warn(anonymous_parameters, bad_style, missing_docs)]
22#![warn(unused, unused_extern_crates, unused_import_braces, unused_qualifications)]
23#![warn(unsafe_code)]
24
25use endbasic_core::exec::{Machine, StopReason};
26use endbasic_std::console::{self, is_narrow, refill_and_print, Console};
27use endbasic_std::program::{continue_if_modified, Program, BREAK_MSG};
28use endbasic_std::storage::Storage;
29use std::cell::RefCell;
30use std::io;
31use std::rc::Rc;
32
33pub mod demos;
34pub mod editor;
35
36pub fn print_welcome(console: Rc<RefCell<dyn Console>>) -> io::Result<()> {
38 let mut console = console.borrow_mut();
39
40 if is_narrow(&*console) {
41 console.print(&format!("EndBASIC {}", env!("CARGO_PKG_VERSION")))?;
42 } else {
43 console.print("")?;
44 console.print(&format!(" EndBASIC {}", env!("CARGO_PKG_VERSION")))?;
45 console.print(" Copyright 2020-2024 Julio Merino")?;
46 console.print("")?;
47 console.print(" Type HELP for interactive usage information.")?;
48 }
49 console.print("")?;
50
51 Ok(())
52}
53
54pub async fn try_load_autoexec(
59 machine: &mut Machine,
60 console: Rc<RefCell<dyn Console>>,
61 storage: Rc<RefCell<Storage>>,
62) -> io::Result<()> {
63 let code = match storage.borrow().get("AUTOEXEC.BAS").await {
64 Ok(code) => code,
65 Err(e) if e.kind() == io::ErrorKind::NotFound => return Ok(()),
66 Err(e) => {
67 return console
68 .borrow_mut()
69 .print(&format!("AUTOEXEC.BAS exists but cannot be read: {}", e));
70 }
71 };
72
73 console.borrow_mut().print("Loading AUTOEXEC.BAS...")?;
74 match machine.exec(&mut code.as_bytes()).await {
75 Ok(_) => Ok(()),
76 Err(e) => {
77 console.borrow_mut().print(&format!("AUTOEXEC.BAS failed: {}", e))?;
78 Ok(())
79 }
80 }
81}
82
83pub async fn run_from_cloud(
86 machine: &mut Machine,
87 console: Rc<RefCell<dyn Console>>,
88 storage: Rc<RefCell<Storage>>,
89 program: Rc<RefCell<dyn Program>>,
90 username_path: &str,
91 will_run_repl: bool,
92) -> endbasic_core::exec::Result<i32> {
93 let (fs_uri, path) = match username_path.split_once('/') {
94 Some((username, path)) => (format!("cloud://{}", username), format!("AUTORUN:/{}", path)),
95 None => {
96 let mut console = console.borrow_mut();
97 console.print(&format!(
98 "Invalid program to run '{}'; must be of the form 'username/path'",
99 username_path
100 ))?;
101 return Ok(1);
102 }
103 };
104
105 console.borrow_mut().print(&format!("Mounting {} as AUTORUN...", fs_uri))?;
106 storage.borrow_mut().mount("AUTORUN", &fs_uri)?;
107 storage.borrow_mut().cd("AUTORUN:/")?;
108
109 console.borrow_mut().print(&format!("Loading {}...", path))?;
110 let content = storage.borrow().get(&path).await?;
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
160pub 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 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 } 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 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 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(["Loading AUTOEXEC.BAS...", "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([
308 "Loading AUTOEXEC.BAS...",
309 "AUTOEXEC.BAS failed: 2:5: Undefined variable undef",
310 ])
311 .expect_file("MEMORY:/AUTOEXEC.BAS", autoexec)
312 .check();
313 }
314
315 #[test]
316 fn test_autoexec_execution_error_is_ignored() {
317 let autoexec = "a = 1\nb = 3 >> -1: c = 2";
318 let mut tester = Tester::default().write_file("AUTOEXEC.BAS", autoexec);
319 let (console, storage) = (tester.get_console(), tester.get_storage());
320 block_on(try_load_autoexec(tester.get_machine(), console, storage)).unwrap();
321 tester
322 .run("after = 5")
323 .expect_var("a", 1)
324 .expect_var("after", 5)
325 .expect_prints([
326 "Loading AUTOEXEC.BAS...",
327 "AUTOEXEC.BAS failed: 2:7: Number of bits to >> (-1) must be positive",
328 ])
329 .expect_file("MEMORY:/AUTOEXEC.BAS", autoexec)
330 .check();
331 }
332
333 #[test]
334 fn test_autoexec_name_is_case_sensitive() {
335 let mut tester = Tester::default()
336 .write_file("AUTOEXEC.BAS", "a = 1")
337 .write_file("autoexec.bas", "a = 2");
338 let (console, storage) = (tester.get_console(), tester.get_storage());
339 block_on(try_load_autoexec(tester.get_machine(), console, storage)).unwrap();
340 tester
341 .run("")
342 .expect_var("a", 1)
343 .expect_prints(["Loading AUTOEXEC.BAS..."])
344 .expect_file("MEMORY:/AUTOEXEC.BAS", "a = 1")
345 .expect_file("MEMORY:/autoexec.bas", "a = 2")
346 .check();
347 }
348
349 #[test]
350 fn test_autoexec_missing() {
351 let mut tester = Tester::default();
352 let (console, storage) = (tester.get_console(), tester.get_storage());
353 block_on(try_load_autoexec(tester.get_machine(), console, storage)).unwrap();
354 tester.run("").check();
355 }
356
357 struct MockDriveFactory {
359 exp_username: &'static str,
360 exp_file: &'static str,
361 }
362
363 impl MockDriveFactory {
364 const SCRIPT: &'static str = r#"PRINT "Success""#;
366 }
367
368 impl DriveFactory for MockDriveFactory {
369 fn create(&self, target: &str) -> io::Result<Box<dyn Drive>> {
370 let mut drive = InMemoryDrive::default();
371 block_on(drive.put(self.exp_file, Self::SCRIPT)).unwrap();
372 assert_eq!(self.exp_username, target);
373 Ok(Box::from(drive))
374 }
375 }
376
377 #[test]
378 fn test_run_from_cloud_no_repl() {
379 let mut tester = Tester::default();
380 let (console, storage, program) =
381 (tester.get_console(), tester.get_storage(), tester.get_program());
382
383 storage.borrow_mut().register_scheme(
384 "cloud",
385 Box::from(MockDriveFactory { exp_username: "foo", exp_file: "bar.bas" }),
386 );
387
388 block_on(run_from_cloud(
389 tester.get_machine(),
390 console,
391 storage,
392 program,
393 "foo/bar.bas",
394 false,
395 ))
396 .unwrap();
397 tester
398 .run("")
399 .expect_prints([
400 "Mounting cloud://foo as AUTORUN...",
401 "Loading AUTORUN:/bar.bas...",
402 "Starting...",
403 "",
404 ])
405 .expect_clear()
406 .expect_prints(["Success", "", "**** Program exited due to EOF ****"])
407 .expect_program(Some("AUTORUN:/bar.bas"), MockDriveFactory::SCRIPT)
408 .check();
409 }
410
411 #[test]
412 fn test_run_from_cloud_repl() {
413 let mut tester = Tester::default();
414 let (console, storage, program) =
415 (tester.get_console(), tester.get_storage(), tester.get_program());
416
417 storage.borrow_mut().register_scheme(
418 "cloud",
419 Box::from(MockDriveFactory { exp_username: "abcd", exp_file: "the-path.bas" }),
420 );
421
422 block_on(run_from_cloud(
423 tester.get_machine(),
424 console,
425 storage,
426 program,
427 "abcd/the-path.bas",
428 true,
429 ))
430 .unwrap();
431 let mut checker = tester.run("");
432 let output = flatten_output(checker.take_captured_out());
433 checker.expect_program(Some("AUTORUN:/the-path.bas"), MockDriveFactory::SCRIPT).check();
434
435 assert!(output.contains("You are now being dropped into"));
436 }
437
438 #[test]
439 fn test_run_repl_loop_signal_before_exec() {
440 let mut tester = Tester::default();
441 let (console, program) = (tester.get_console(), tester.get_program());
442 let signals_tx = tester.get_machine().get_signals_tx();
443
444 {
445 let mut console = console.borrow_mut();
446 console.add_input_chars("PRINT");
447 block_on(signals_tx.send(Signal::Break)).unwrap();
448 console.add_input_chars(" 123");
449 console.add_input_keys(&[Key::NewLine, Key::Eof]);
450 }
451 block_on(run_repl_loop(tester.get_machine(), console, program)).unwrap();
452 tester.run("").expect_prints([" 123", "End of input by CTRL-D"]).check();
453 }
454}