1use crate::Yielder;
20use crate::console::{Console, Pager, read_line};
21use crate::storage::Storage;
22use crate::strings::parse_boolean;
23use crate::{MachineAction, MachineBuilder};
24use async_trait::async_trait;
25use endbasic_core::{
26 ArgSepSyntax, CallError, CallResult, Callable, CallableMetadata, CallableMetadataBuilder,
27 Compiler, ExprType, RequiredValueSyntax, Scope, SingularArgSyntax, SymbolKey,
28};
29use std::borrow::Cow;
30use std::cell::RefCell;
31use std::collections::HashMap;
32use std::io;
33use std::rc::Rc;
34use std::str;
35
36const CATEGORY: &str = "Stored program
38The EndBASIC interpreter has a piece of read/write memory called the \"stored program\". This \
39memory serves to maintain the code of a program you edit and manipulate right from the \
40interpreter.
41The common flow to interact with a stored program is to load a program from disk using the LOAD \
42command, modify its contents via the EDIT command, execute the program via the RUN command, and \
43finally save the new or modified program via the SAVE command.
44Be aware that the stored program's content is lost whenever you load a program, exit the \
45interpreter, or use the NEW command. These operations will ask you to save the program if you \
46have forgotten to do so, but it's better to get in the habit of saving often.
47See the \"File system\" help topic for information on where the programs can be saved and loaded \
48from.";
49
50pub const BREAK_MSG: &str = "**** BREAK ****";
52
53const DEFAULT_EXTENSION: &str = "bas";
55
56#[async_trait(?Send)]
58pub trait Program {
59 fn is_dirty(&self) -> bool;
62
63 async fn edit(&mut self, console: &mut dyn Console) -> io::Result<()>;
65
66 fn load(&mut self, name: Option<&str>, text: &str);
69
70 fn name(&self) -> Option<&str>;
72
73 fn set_name(&mut self, name: &str);
75
76 fn text(&self) -> String;
78}
79
80#[derive(Default)]
82pub(crate) struct ImmutableProgram {
83 name: Option<String>,
84 text: String,
85}
86
87#[async_trait(?Send)]
88impl Program for ImmutableProgram {
89 fn is_dirty(&self) -> bool {
90 false
91 }
92
93 async fn edit(&mut self, _console: &mut dyn Console) -> io::Result<()> {
94 Err(io::Error::other("Editing not supported"))
95 }
96
97 fn load(&mut self, name: Option<&str>, text: &str) {
98 self.name = name.map(str::to_owned);
99 text.clone_into(&mut self.text);
100 }
101
102 fn name(&self) -> Option<&str> {
103 self.name.as_deref()
104 }
105
106 fn set_name(&mut self, name: &str) {
107 self.name = Some(name.to_owned());
108 }
109
110 fn text(&self) -> String {
111 self.text.clone()
112 }
113}
114
115pub async fn continue_if_modified(
117 program: &dyn Program,
118 console: &mut dyn Console,
119) -> io::Result<bool> {
120 if !program.is_dirty() {
121 return Ok(true);
122 }
123
124 match program.name() {
125 Some(name) => console.print(&format!("Current program {} has unsaved changes!", name))?,
126 None => console.print("Current program has unsaved changes and has never been saved!")?,
127 }
128 let answer = read_line(console, "Discard and continue (y/N)? ", "", None).await?;
129 Ok(parse_boolean(&answer).unwrap_or(false))
130}
131
132pub struct DisasmCommand {
134 metadata: Rc<CallableMetadata>,
135 callables_metadata: Rc<RefCell<HashMap<SymbolKey, Rc<CallableMetadata>>>>,
136 console: Rc<RefCell<dyn Console>>,
137 program: Rc<RefCell<dyn Program>>,
138 yielder: Option<Rc<RefCell<dyn Yielder>>>,
139}
140
141struct MetadataCallable {
142 metadata: Rc<CallableMetadata>,
143}
144
145#[async_trait(?Send)]
146impl Callable for MetadataCallable {
147 fn metadata(&self) -> Rc<CallableMetadata> {
148 self.metadata.clone()
149 }
150
151 fn exec(&self, _scope: Scope<'_>) -> CallResult<()> {
152 unreachable!("MetadataCallable::exec must not be called")
153 }
154}
155
156impl DisasmCommand {
157 pub fn new(
159 callables_metadata: Rc<RefCell<HashMap<SymbolKey, Rc<CallableMetadata>>>>,
160 console: Rc<RefCell<dyn Console>>,
161 program: Rc<RefCell<dyn Program>>,
162 yielder: Option<Rc<RefCell<dyn Yielder>>>,
163 ) -> Rc<Self> {
164 Rc::from(Self {
165 metadata: CallableMetadataBuilder::new("DISASM")
166 .with_async(true)
167 .with_syntax(&[(&[], None)])
168 .with_category(CATEGORY)
169 .with_description(
170 "Disassembles the stored program.
171The assembly code printed by this command is provided as a tool to understand how high level code \
172gets translated to the machine code of a fictitious stack-based machine. Note, however, that the \
173assembly code cannot be reassembled nor modified at this point.",
174 )
175 .build(),
176 callables_metadata,
177 console,
178 program,
179 yielder,
180 })
181 }
182}
183
184#[async_trait(?Send)]
185impl Callable for DisasmCommand {
186 fn metadata(&self) -> Rc<CallableMetadata> {
187 self.metadata.clone()
188 }
189
190 async fn async_exec(&self, scope: Scope<'_>) -> CallResult<()> {
191 debug_assert_eq!(0, scope.nargs());
192
193 let image = {
194 let metadata = self.callables_metadata.borrow();
195 let mut callables = HashMap::with_capacity(metadata.len());
196 for (name, metadata) in metadata.iter() {
197 let callable = Rc::from(MetadataCallable { metadata: metadata.clone() });
198 callables.insert(name.clone(), callable as Rc<dyn Callable>);
199 }
200
201 let compiler = Compiler::new(&callables, &[])
202 .map_err(|e| CallError::Syntax(e.pos(), e.message_without_pos()))?;
203 compiler
204 .compile(&mut self.program.borrow().text().as_bytes())
205 .map_err(|e| CallError::Syntax(e.pos(), e.message_without_pos()))?
206 };
207
208 let mut console = self.console.borrow_mut();
209 let mut pager = Pager::new(&mut *console, self.yielder.clone())?;
210 for line in image.disasm() {
211 pager.print(&line).await?;
212 }
213 pager.print("").await?;
214
215 Ok(())
216 }
217}
218
219pub struct EditCommand {
221 metadata: Rc<CallableMetadata>,
222 console: Rc<RefCell<dyn Console>>,
223 program: Rc<RefCell<dyn Program>>,
224}
225
226impl EditCommand {
227 pub fn new(console: Rc<RefCell<dyn Console>>, program: Rc<RefCell<dyn Program>>) -> Rc<Self> {
229 Rc::from(Self {
230 metadata: CallableMetadataBuilder::new("EDIT")
231 .with_async(true)
232 .with_syntax(&[(&[], None)])
233 .with_category(CATEGORY)
234 .with_description("Interactively edits the stored program.")
235 .build(),
236 console,
237 program,
238 })
239 }
240}
241
242#[async_trait(?Send)]
243impl Callable for EditCommand {
244 fn metadata(&self) -> Rc<CallableMetadata> {
245 self.metadata.clone()
246 }
247
248 async fn async_exec(&self, scope: Scope<'_>) -> CallResult<()> {
249 debug_assert_eq!(0, scope.nargs());
250
251 let mut console = self.console.borrow_mut();
252 let mut program = self.program.borrow_mut();
253 program.edit(&mut *console).await?;
254 Ok(())
255 }
256}
257
258pub struct ListCommand {
260 metadata: Rc<CallableMetadata>,
261 console: Rc<RefCell<dyn Console>>,
262 program: Rc<RefCell<dyn Program>>,
263 yielder: Option<Rc<RefCell<dyn Yielder>>>,
264}
265
266impl ListCommand {
267 pub fn new(
269 console: Rc<RefCell<dyn Console>>,
270 program: Rc<RefCell<dyn Program>>,
271 yielder: Option<Rc<RefCell<dyn Yielder>>>,
272 ) -> Rc<Self> {
273 Rc::from(Self {
274 metadata: CallableMetadataBuilder::new("LIST")
275 .with_async(true)
276 .with_syntax(&[(&[], None)])
277 .with_category(CATEGORY)
278 .with_description("Prints the currently-loaded program.")
279 .build(),
280 console,
281 program,
282 yielder,
283 })
284 }
285}
286
287#[async_trait(?Send)]
288impl Callable for ListCommand {
289 fn metadata(&self) -> Rc<CallableMetadata> {
290 self.metadata.clone()
291 }
292
293 async fn async_exec(&self, scope: Scope<'_>) -> CallResult<()> {
294 debug_assert_eq!(0, scope.nargs());
295
296 let mut console = self.console.borrow_mut();
297 let mut pager = Pager::new(&mut *console, self.yielder.clone())?;
298 for line in self.program.borrow().text().lines() {
299 pager.print(line).await?;
300 }
301 Ok(())
302 }
303}
304
305pub struct LoadCommand {
307 metadata: Rc<CallableMetadata>,
308 console: Rc<RefCell<dyn Console>>,
309 storage: Rc<RefCell<Storage>>,
310 program: Rc<RefCell<dyn Program>>,
311 actions: Rc<RefCell<Vec<MachineAction>>>,
312}
313
314impl LoadCommand {
315 pub fn new(
318 console: Rc<RefCell<dyn Console>>,
319 storage: Rc<RefCell<Storage>>,
320 program: Rc<RefCell<dyn Program>>,
321 actions: Rc<RefCell<Vec<MachineAction>>>,
322 ) -> Rc<Self> {
323 Rc::from(Self {
324 metadata: CallableMetadataBuilder::new("LOAD")
325 .with_async(true)
326 .with_syntax(&[(
327 &[SingularArgSyntax::RequiredValue(
328 RequiredValueSyntax {
329 name: Cow::Borrowed("filename"),
330 vtype: ExprType::Text,
331 },
332 ArgSepSyntax::End,
333 )],
334 None,
335 )])
336 .with_category(CATEGORY)
337 .with_description(
338 "Loads the given program.
339The filename must be a string and must be a valid EndBASIC path. The .BAS extension is optional \
340but, if present, it must be .BAS.
341Any previously stored program is discarded from memory, but LOAD will pause to ask before \
342discarding any unsaved modifications.
343See the \"File system\" help topic for information on the path syntax.",
344 )
345 .build(),
346 console,
347 storage,
348 program,
349 actions,
350 })
351 }
352}
353
354#[async_trait(?Send)]
355impl Callable for LoadCommand {
356 fn metadata(&self) -> Rc<CallableMetadata> {
357 self.metadata.clone()
358 }
359
360 async fn async_exec(&self, scope: Scope<'_>) -> CallResult<()> {
361 debug_assert_eq!(1, scope.nargs());
362 let pathname = scope.get_string(0);
363
364 if continue_if_modified(&*self.program.borrow(), &mut *self.console.borrow_mut()).await? {
365 let (full_name, content) = {
366 let storage = self.storage.borrow();
367 let full_name =
368 storage.make_canonical_with_extension(pathname, DEFAULT_EXTENSION)?;
369 let content = storage.get(&full_name).await?;
370 let content = match String::from_utf8(content) {
371 Ok(text) => text,
372 Err(e) => {
373 return Err(io::Error::new(
374 io::ErrorKind::InvalidData,
375 format!("Invalid file content: {}", e),
376 )
377 .into());
378 }
379 };
380 (full_name, content)
381 };
382 self.program.borrow_mut().load(Some(&full_name), &content);
383 self.actions.borrow_mut().push(MachineAction::Clear);
384 Ok(())
385 } else {
386 self.console
387 .borrow_mut()
388 .print("LOAD aborted; use SAVE to save your current changes.")?;
389 Ok(())
390 }
391 }
392}
393
394pub struct NewCommand {
396 metadata: Rc<CallableMetadata>,
397 console: Rc<RefCell<dyn Console>>,
398 program: Rc<RefCell<dyn Program>>,
399 actions: Rc<RefCell<Vec<MachineAction>>>,
400}
401
402impl NewCommand {
403 pub fn new(
406 console: Rc<RefCell<dyn Console>>,
407 program: Rc<RefCell<dyn Program>>,
408 actions: Rc<RefCell<Vec<MachineAction>>>,
409 ) -> Rc<Self> {
410 Rc::from(Self {
411 metadata: CallableMetadataBuilder::new("NEW")
412 .with_async(true)
413 .with_syntax(&[(&[], None)])
414 .with_category(CATEGORY)
415 .with_description(
416 "Restores initial machine state and creates a new program.
417This command resets the machine to a pristine state by clearing all user-defined variables \
418and restoring the state of shared resources. These resources include: the console, whose color \
419and video syncing bit are reset; and the GPIO pins, which are set to their default state.
420The stored program is also discarded from memory, but NEW will pause to ask before discarding \
421any unsaved modifications. To reset resources but avoid clearing the stored program, use CLEAR \
422instead.",
423 )
424 .build(),
425 console,
426 program,
427 actions,
428 })
429 }
430}
431
432#[async_trait(?Send)]
433impl Callable for NewCommand {
434 fn metadata(&self) -> Rc<CallableMetadata> {
435 self.metadata.clone()
436 }
437
438 async fn async_exec(&self, scope: Scope<'_>) -> CallResult<()> {
439 debug_assert_eq!(0, scope.nargs());
440
441 if continue_if_modified(&*self.program.borrow(), &mut *self.console.borrow_mut()).await? {
442 self.program.borrow_mut().load(None, "");
443 self.actions.borrow_mut().push(MachineAction::Clear);
444 } else {
445 self.console
446 .borrow_mut()
447 .print("NEW aborted; use SAVE to save your current changes.")?;
448 }
449 Ok(())
450 }
451}
452
453pub struct RunCommand {
455 metadata: Rc<CallableMetadata>,
456 program: Rc<RefCell<dyn Program>>,
457 actions: Rc<RefCell<Vec<MachineAction>>>,
458}
459
460impl RunCommand {
461 pub fn new(
463 program: Rc<RefCell<dyn Program>>,
464 actions: Rc<RefCell<Vec<MachineAction>>>,
465 ) -> Rc<Self> {
466 Rc::from(Self {
467 metadata: CallableMetadataBuilder::new("RUN")
468 .with_async(true)
469 .with_syntax(&[(&[], None)])
470 .with_category(CATEGORY)
471 .with_description(
472 "Runs the stored program.
473This issues a CLEAR operation before starting the program to prevent previous leftover state \
474from interfering with the new execution.",
475 )
476 .build(),
477 program,
478 actions,
479 })
480 }
481}
482
483#[async_trait(?Send)]
484impl Callable for RunCommand {
485 fn metadata(&self) -> Rc<CallableMetadata> {
486 self.metadata.clone()
487 }
488
489 async fn async_exec(&self, scope: Scope<'_>) -> CallResult<()> {
490 debug_assert_eq!(0, scope.nargs());
491
492 let program = self.program.borrow().text();
493 self.actions.borrow_mut().push(MachineAction::Run(program));
494 Ok(())
495 }
496}
497
498pub struct SaveCommand {
500 metadata: Rc<CallableMetadata>,
501 console: Rc<RefCell<dyn Console>>,
502 storage: Rc<RefCell<Storage>>,
503 program: Rc<RefCell<dyn Program>>,
504}
505
506impl SaveCommand {
507 pub fn new(
509 console: Rc<RefCell<dyn Console>>,
510 storage: Rc<RefCell<Storage>>,
511 program: Rc<RefCell<dyn Program>>,
512 ) -> Rc<Self> {
513 Rc::from(Self {
514 metadata: CallableMetadataBuilder::new("SAVE")
515 .with_async(true)
516 .with_syntax(&[
517 (&[], None),
518 (
519 &[SingularArgSyntax::RequiredValue(
520 RequiredValueSyntax {
521 name: Cow::Borrowed("filename"),
522 vtype: ExprType::Text,
523 },
524 ArgSepSyntax::End,
525 )],
526 None,
527 ),
528 ])
529 .with_category(CATEGORY)
530 .with_description(
531 "Saves the current program in memory to the given filename.
532The filename must be a string and must be a valid EndBASIC path. The .BAS extension is optional \
533but, if present, it must be .BAS.
534If no filename is given, SAVE will try to use the filename of the loaded program (if any) and \
535will fail if no name has been given yet.
536See the \"File system\" help topic for information on the path syntax.",
537 )
538 .build(),
539 console,
540 storage,
541 program,
542 })
543 }
544}
545
546#[async_trait(?Send)]
547impl Callable for SaveCommand {
548 fn metadata(&self) -> Rc<CallableMetadata> {
549 self.metadata.clone()
550 }
551
552 async fn async_exec(&self, scope: Scope<'_>) -> CallResult<()> {
553 let name = if scope.nargs() == 0 {
554 match self.program.borrow().name() {
555 Some(name) => name.to_owned(),
556 None => {
557 return Err(CallError::Precondition(
558 "Unnamed program; please provide a filename".to_owned(),
559 ));
560 }
561 }
562 } else {
563 debug_assert_eq!(1, scope.nargs());
564 scope.get_string(0).to_owned()
565 };
566
567 let full_name =
568 self.storage.borrow().make_canonical_with_extension(&name, DEFAULT_EXTENSION)?;
569 let content = self.program.borrow().text();
570 self.storage.borrow_mut().put(&full_name, content.as_bytes()).await?;
571 self.program.borrow_mut().set_name(&full_name);
572
573 self.console.borrow_mut().print(&format!("Saved as {}", full_name))?;
574
575 Ok(())
576 }
577}
578
579pub fn add_all(
582 machine: &mut MachineBuilder,
583 program: Rc<RefCell<dyn Program>>,
584 console: Rc<RefCell<dyn Console>>,
585 storage: Rc<RefCell<Storage>>,
586 yielder: Option<Rc<RefCell<dyn Yielder>>>,
587) {
588 machine.add_callable(DisasmCommand::new(
589 machine.callables_metadata(),
590 console.clone(),
591 program.clone(),
592 yielder.clone(),
593 ));
594 machine.add_callable(EditCommand::new(console.clone(), program.clone()));
595 machine.add_callable(ListCommand::new(console.clone(), program.clone(), yielder));
596 machine.add_callable(LoadCommand::new(
597 console.clone(),
598 storage.clone(),
599 program.clone(),
600 machine.actions(),
601 ));
602 machine.add_callable(NewCommand::new(console.clone(), program.clone(), machine.actions()));
603 machine.add_callable(RunCommand::new(program.clone(), machine.actions()));
604 machine.add_callable(SaveCommand::new(console, storage, program));
605}
606
607#[cfg(test)]
608mod tests {
609 use super::*;
610 use crate::console::{CharsXY, Key};
611 use crate::testutils::*;
612 use crate::{MachineBuilder, Signal};
613 use futures_lite::future::{FutureExt, block_on};
614 use std::rc::Rc;
615 use std::time::Duration;
616
617 const NO_ANSWERS: &[&str] =
618 &["n\n", "N\n", "no\n", "NO\n", "false\n", "FALSE\n", "xyz\n", "\n", "1\n"];
619
620 const YES_ANSWERS: &[&str] = &["y\n", "yes\n", "Y\n", "YES\n", "true\n", "TRUE\n"];
621
622 #[test]
623 fn test_disasm_nothing() {
624 Tester::default()
625 .run("DISASM")
626 .expect_prints(["0000: EOF ; 0:0", ""])
627 .check();
628 }
629
630 #[test]
631 fn test_disasm_ok() {
632 Tester::default()
633 .set_program(None, "A = 2 + 3")
634 .run("DISASM")
635 .expect_prints([
636 "0000: LOADI R64, 2 ; 1:5",
637 "0001: LOADI R65, 3 ; 1:9",
638 "0002: ADDI R64, R64, R65 ; 1:7",
639 "0003: EOF ; 0:0",
640 "",
641 ])
642 .expect_program(None as Option<&str>, "A = 2 + 3")
643 .check();
644 }
645
646 #[test]
647 fn test_disasm_paging() {
648 let t = Tester::default();
649 t.get_console().borrow_mut().set_interactive(true);
650 t.get_console().borrow_mut().set_size_chars(CharsXY { x: 80, y: 4 });
651 t.get_console().borrow_mut().add_input_keys(&[Key::NewLine]);
652 t.set_program(None, "A = 2 + 3")
653 .run("DISASM")
654 .expect_prints([
655 "0000: LOADI R64, 2 ; 1:5",
656 "0001: LOADI R65, 3 ; 1:9",
657 "0002: ADDI R64, R64, R65 ; 1:7",
658 " << Press any key for more; ESC or Ctrl+C to stop >> ",
659 "0003: EOF ; 0:0",
660 "",
661 ])
662 .expect_program(None as Option<&str>, "A = 2 + 3")
663 .check();
664 }
665
666 #[test]
667 fn test_disasm_code_errors() {
668 Tester::default()
669 .set_program(None, "A = 3 +")
670 .run("DISASM")
671 .expect_err("1:7: Not enough values to apply operator")
672 .expect_program(None as Option<&str>, "A = 3 +")
673 .check();
674 }
675
676 #[test]
677 fn test_disasm_errors() {
678 check_stmt_compilation_err("1:1: DISASM expected no arguments", "DISASM 2");
679 }
680
681 #[test]
682 fn test_edit_ok() {
683 Tester::default()
684 .set_program(Some("foo.bas"), "previous\n")
685 .add_input_chars("new line\n")
686 .run("EDIT")
687 .expect_program(Some("foo.bas"), "previous\nnew line\n")
688 .check();
689 }
690
691 #[test]
692 fn test_edit_errors() {
693 check_stmt_compilation_err("1:1: EDIT expected no arguments", "EDIT 1");
694 }
695
696 #[test]
697 fn test_list_ok() {
698 Tester::default().run("LIST").check();
699
700 Tester::default()
701 .set_program(None, "one\n\nthree\n")
702 .run("LIST")
703 .expect_prints(["one", "", "three"])
704 .expect_program(None as Option<&str>, "one\n\nthree\n")
705 .check();
706 }
707
708 #[test]
709 fn test_list_paging() {
710 let t = Tester::default();
711 t.get_console().borrow_mut().set_interactive(true);
712 t.get_console().borrow_mut().set_size_chars(CharsXY { x: 30, y: 5 });
713 t.get_console().borrow_mut().add_input_keys(&[Key::NewLine]);
714 t.set_program(None, "one\n\nthree\nfour\nfive")
715 .run("LIST")
716 .expect_prints(["one", "", "three", "four", " << More >> ", "five"])
717 .expect_program(None as Option<&str>, "one\n\nthree\nfour\nfive")
718 .check();
719 }
720
721 #[test]
722 fn test_list_errors() {
723 check_stmt_compilation_err("1:1: LIST expected no arguments", "LIST 2");
724 }
725
726 #[test]
727 fn test_load_ok() {
728 let content = "line 1\n\n line 2\n";
729 for (explicit, canonical) in &[
730 ("foo", "MEMORY:foo.bas"),
731 ("foo.bas", "MEMORY:foo.bas"),
732 ("BAR", "MEMORY:BAR.BAS"),
733 ("BAR.BAS", "MEMORY:BAR.BAS"),
734 ("Baz", "MEMORY:Baz.bas"),
735 ] {
736 Tester::default()
737 .write_file("foo.bas", content)
738 .write_file("foo.bak", "")
739 .write_file("BAR.BAS", content)
740 .write_file("Baz.bas", content)
741 .run(format!(r#"LOAD "{}""#, explicit))
742 .expect_clear()
743 .expect_program(Some(*canonical), "line 1\n\n line 2\n")
744 .expect_file("MEMORY:/foo.bas", content)
745 .expect_file("MEMORY:/foo.bak", "")
746 .expect_file("MEMORY:/BAR.BAS", content)
747 .expect_file("MEMORY:/Baz.bas", content)
748 .check();
749 }
750 }
751
752 #[test]
753 fn test_load_dirty_no_name_abort() {
754 for answer in NO_ANSWERS {
755 Tester::default()
756 .add_input_chars("modified unnamed file\n")
757 .add_input_chars(answer)
758 .write_file("other.bas", "other file\n")
759 .run("EDIT: LOAD \"other.bas\"")
760 .expect_prints([
761 "Current program has unsaved changes and has never been saved!",
762 "LOAD aborted; use SAVE to save your current changes.",
763 ])
764 .expect_program(None as Option<&str>, "modified unnamed file\n")
765 .expect_file("MEMORY:/other.bas", "other file\n")
766 .check();
767 }
768 }
769
770 #[test]
771 fn test_load_dirty_no_name_continue() {
772 for answer in YES_ANSWERS {
773 Tester::default()
774 .add_input_chars("modified unnamed file\n")
775 .add_input_chars(answer)
776 .write_file("other.bas", "other file\n")
777 .run("EDIT: LOAD \"other.bas\"")
778 .expect_prints(["Current program has unsaved changes and has never been saved!"])
779 .expect_clear()
780 .expect_program(Some("MEMORY:other.bas"), "other file\n")
781 .expect_file("MEMORY:/other.bas", "other file\n")
782 .check();
783 }
784 }
785
786 #[test]
787 fn test_load_dirty_with_name_abort() {
788 for answer in NO_ANSWERS {
789 Tester::default()
790 .add_input_chars("modified named file\n")
791 .add_input_chars(answer)
792 .write_file("other.bas", "other file\n")
793 .set_program(Some("MEMORY:/named.bas"), "previous contents\n")
794 .run("EDIT: LOAD \"other.bas\"")
795 .expect_prints([
796 "Current program MEMORY:/named.bas has unsaved changes!",
797 "LOAD aborted; use SAVE to save your current changes.",
798 ])
799 .expect_program(
800 Some("MEMORY:/named.bas"),
801 "previous contents\nmodified named file\n",
802 )
803 .expect_file("MEMORY:/other.bas", "other file\n")
804 .check();
805 }
806 }
807
808 #[test]
809 fn test_load_dirty_with_name_continue() {
810 for answer in YES_ANSWERS {
811 Tester::default()
812 .add_input_chars("modified unnamed file\n")
813 .add_input_chars(answer)
814 .write_file("other.bas", "other file\n")
815 .set_program(Some("MEMORY:/named.bas"), "previous contents\n")
816 .run("EDIT: LOAD \"other.bas\"")
817 .expect_prints(["Current program MEMORY:/named.bas has unsaved changes!"])
818 .expect_clear()
819 .expect_program(Some("MEMORY:other.bas"), "other file\n")
820 .expect_file("MEMORY:/other.bas", "other file\n")
821 .check();
822 }
823 }
824
825 fn check_load_save_common_errors(cmd: &str) {
827 Tester::default()
828 .run(format!("{} 3", cmd))
829 .expect_compilation_err(format!(
830 "1:{}: Expected STRING but found INTEGER",
831 cmd.len() + 2,
832 ))
833 .check();
834
835 Tester::default()
836 .run(format!(r#"{} "a/b.bas""#, cmd))
837 .expect_err("1:1: Too many / separators in path 'a/b.bas'")
838 .check();
839
840 Tester::default()
841 .run(format!(r#"{} "drive:""#, cmd))
842 .expect_err("1:1: Missing file name in path 'drive:'")
843 .check();
844 }
845
846 #[test]
847 fn test_load_errors() {
848 check_load_save_common_errors("LOAD");
849
850 Tester::default()
851 .run("LOAD")
852 .expect_compilation_err("1:1: LOAD expected filename$")
853 .check();
854
855 check_stmt_err("1:1: Entry not found", r#"LOAD "missing-file""#);
856
857 Tester::default()
858 .write_file("mismatched-extension.bat", "")
859 .run(r#"LOAD "mismatched-extension""#)
860 .expect_err("1:1: Entry not found")
861 .expect_file("MEMORY:/mismatched-extension.bat", "")
862 .check();
863 }
864
865 #[test]
866 fn test_new_nothing() {
867 Tester::default().run("NEW").expect_clear().check();
868 }
869
870 #[test]
871 fn test_new_clears_program_and_variables() {
872 Tester::default()
873 .set_program(Some("previous.bas"), "some stuff")
874 .run("a = 3: NEW")
875 .expect_clear()
876 .check();
877 }
878
879 #[test]
880 fn test_new_dirty_no_name_abort() {
881 for answer in NO_ANSWERS {
882 Tester::default()
883 .add_input_chars("modified unnamed file\n")
884 .add_input_chars(answer)
885 .run("EDIT: NEW")
886 .expect_prints([
887 "Current program has unsaved changes and has never been saved!",
888 "NEW aborted; use SAVE to save your current changes.",
889 ])
890 .expect_program(None as Option<&str>, "modified unnamed file\n")
891 .check();
892 }
893 }
894
895 #[test]
896 fn test_new_dirty_no_name_continue() {
897 for answer in YES_ANSWERS {
898 Tester::default()
899 .add_input_chars("modified unnamed file\n")
900 .add_input_chars(answer)
901 .run("EDIT: NEW")
902 .expect_prints(["Current program has unsaved changes and has never been saved!"])
903 .expect_clear()
904 .check();
905 }
906 }
907
908 #[test]
909 fn test_new_dirty_with_name_abort() {
910 for answer in NO_ANSWERS {
911 Tester::default()
912 .add_input_chars("modified named file\n")
913 .add_input_chars(answer)
914 .set_program(Some("MEMORY:/named.bas"), "previous contents\n")
915 .run("EDIT: NEW")
916 .expect_prints([
917 "Current program MEMORY:/named.bas has unsaved changes!",
918 "NEW aborted; use SAVE to save your current changes.",
919 ])
920 .expect_program(
921 Some("MEMORY:/named.bas"),
922 "previous contents\nmodified named file\n",
923 )
924 .check();
925 }
926 }
927
928 #[test]
929 fn test_new_dirty_with_name_continue() {
930 for answer in YES_ANSWERS {
931 Tester::default()
932 .add_input_chars("modified named file\n")
933 .add_input_chars(answer)
934 .set_program(Some("MEMORY:/named.bas"), "previous contents\n")
935 .run("EDIT: NEW")
936 .expect_prints(["Current program MEMORY:/named.bas has unsaved changes!"])
937 .expect_clear()
938 .check();
939 }
940 }
941
942 #[test]
943 fn test_new_errors() {
944 check_stmt_compilation_err("1:1: NEW expected no arguments", "NEW 10");
945 }
946
947 #[test]
948 fn test_run_nothing() {
949 Tester::default().run("RUN").expect_clear().check();
950 }
951
952 #[test]
953 fn test_run_clears_before_execution_only() {
954 let program = "DIM a(1) AS INTEGER: a(0) = 123";
955 let mut t = Tester::default().set_program(Some("untouched.bas"), program);
956 t.run("DIM a(1) AS STRING: RUN")
957 .expect_array_simple("a", ExprType::Integer, vec![123.into()])
958 .expect_clear()
959 .expect_program(Some("untouched.bas"), program)
960 .check();
961 t.run("RUN")
962 .expect_array_simple("a", ExprType::Integer, vec![123.into()])
963 .expect_clear()
964 .expect_clear()
965 .expect_program(Some("untouched.bas"), program)
966 .check();
967 }
968
969 #[test]
970 fn test_run_something_that_exits() {
971 let program = "PRINT 5: END 1: PRINT 4";
972 Tester::default()
973 .set_program(Some("untouched.bas"), program)
974 .run(r#"RUN"#)
975 .expect_clear()
976 .expect_prints([" 5", "Program exited with code 1"])
977 .expect_program(Some("untouched.bas"), program)
978 .check();
979 }
980
981 #[test]
982 fn test_run_something_that_exits_with_trailing_code() {
983 let program = "PRINT 5: END 1: PRINT 4";
988 Tester::default()
989 .set_program(Some("untouched.bas"), program)
990 .run(r#"RUN: PRINT "after""#)
991 .expect_clear()
992 .expect_prints([" 5", "Program exited with code 1"])
993 .expect_program(Some("untouched.bas"), program)
994 .check();
995 }
996
997 #[test]
998 fn test_run_after_break_starts_from_beginning() {
999 let console = Rc::from(RefCell::from(MockConsole::default()));
1000 let program = Rc::from(RefCell::from(RecordedProgram::default()));
1001 program.borrow_mut().load(None, r#"PRINT "begin": SLEEP 0: PRINT "done""#);
1002
1003 let (signals_tx, signals_rx) = async_channel::unbounded();
1004 let signal_sent = Rc::from(RefCell::from(false));
1005 let sleep_tx = signals_tx.clone();
1006 let sleep_signal_sent = signal_sent.clone();
1007 let datetime = Rc::from(MockDateTime::default());
1008 datetime.set_sleep_fn(Box::new(
1009 move |_d: Duration| -> futures_lite::future::BoxedLocal<Result<(), String>> {
1010 let sleep_tx = sleep_tx.clone();
1011 let sleep_signal_sent = sleep_signal_sent.clone();
1012 async move {
1013 if !*sleep_signal_sent.borrow() {
1014 *sleep_signal_sent.borrow_mut() = true;
1015 sleep_tx.send(Signal::Break).await.unwrap();
1016 }
1017 Ok(())
1018 }
1019 .boxed_local()
1020 },
1021 ));
1022
1023 let storage = Rc::from(RefCell::from(Storage::default()));
1024 let mut machine = MachineBuilder::default()
1025 .with_console(console.clone())
1026 .with_signals_chan((signals_tx, signals_rx))
1027 .with_datetime(datetime)
1028 .make_interactive()
1029 .with_program(program)
1030 .with_storage(storage)
1031 .build();
1032
1033 machine.compile(&mut "RUN".as_bytes()).unwrap();
1034 match block_on(machine.exec()) {
1035 Err(crate::Error::Break) => (),
1036 r => panic!("Expected Break but got {:?}", r),
1037 }
1038
1039 machine.compile(&mut "RUN".as_bytes()).unwrap();
1040 match block_on(machine.exec()) {
1041 Ok(None) => (),
1042 r => panic!("Expected successful completion but got {:?}", r),
1043 }
1044
1045 let prints: Vec<String> = console
1046 .borrow_mut()
1047 .take_captured_out()
1048 .into_iter()
1049 .filter_map(|out| match out {
1050 CapturedOut::Print(text) => Some(text),
1051 _ => None,
1052 })
1053 .collect();
1054 assert_eq!(vec!["begin", "begin", "done"], prints);
1055 }
1056
1057 #[test]
1058 fn test_run_after_async_error_discards_old_program() {
1059 let program = r#"DIR "invalid:": PRINT "stale""#;
1060 Tester::default()
1061 .set_program(None, program)
1062 .continue_from_here()
1063 .run("RUN")
1064 .expect_clear()
1065 .expect_err("1:1: Drive 'INVALID' is not mounted")
1066 .expect_program(None as Option<String>, program)
1067 .check()
1068 .run(r#"PRINT "ok""#)
1069 .expect_clear()
1070 .expect_prints(["ok"])
1071 .expect_program(None as Option<String>, program)
1072 .check();
1073 }
1074
1075 #[test]
1076 fn test_run_does_not_leave_error_handler_active() {
1077 let program = r#"
1078 ON ERROR GOTO @handler
1079 END
1080
1081 @handler
1082 PRINT "captured: "; ERRMSG
1083 END 1
1084 "#;
1085 Tester::default()
1086 .set_program(None, program)
1087 .continue_from_here()
1088 .run("RUN")
1089 .expect_clear()
1090 .expect_program(None as Option<String>, program)
1091 .check()
1092 .run(r#"LOAD "missing-file""#)
1093 .expect_clear()
1094 .expect_err("1:1: Entry not found")
1095 .expect_program(None as Option<String>, program)
1096 .check();
1097 }
1098
1099 #[test]
1100 fn test_run_preserves_errmsg_across_commands() {
1101 let program = r#"ON ERROR RESUME NEXT: COLOR -1: END"#;
1102 Tester::default()
1103 .set_program(None, program)
1104 .continue_from_here()
1105 .run("RUN")
1106 .expect_clear()
1107 .expect_program(None as Option<String>, program)
1108 .check()
1109 .run("result = ERRMSG")
1110 .expect_clear()
1111 .expect_var("result", "1:29: Color out of range")
1112 .expect_program(None as Option<String>, program)
1113 .check();
1114 }
1115
1116 #[test]
1117 fn test_run_after_break_discards_old_program() {
1118 let program = r#"PRINT "stale""#;
1119 Tester::default()
1120 .set_program(None, program)
1121 .send_break()
1122 .continue_from_here()
1123 .run("RUN")
1124 .expect_err("Break")
1125 .expect_program(None as Option<String>, program)
1126 .check()
1127 .run(r#"SLEEP 0: PRINT "ok""#)
1128 .expect_prints(["ok"])
1129 .expect_program(None as Option<String>, program)
1130 .check();
1131 }
1132
1133 #[test]
1134 fn test_run_errors() {
1135 check_stmt_compilation_err("1:1: RUN expected no arguments", "RUN 10");
1136 }
1137
1138 #[test]
1139 fn test_save_ok_explicit_name() {
1140 let content = "\n some line \n ";
1141 for (explicit, actual, canonical) in &[
1142 ("first", "MEMORY:/first.bas", "MEMORY:first.bas"),
1143 ("second.bas", "MEMORY:/second.bas", "MEMORY:second.bas"),
1144 ("THIRD", "MEMORY:/THIRD.BAS", "MEMORY:THIRD.BAS"),
1145 ("FOURTH.BAS", "MEMORY:/FOURTH.BAS", "MEMORY:FOURTH.BAS"),
1146 ("Fifth", "MEMORY:/Fifth.bas", "MEMORY:Fifth.bas"),
1147 ] {
1148 Tester::default()
1149 .set_program(Some("before.bas"), content)
1150 .run(format!(r#"SAVE "{}""#, explicit))
1151 .expect_program(Some(*canonical), content)
1152 .expect_prints([format!("Saved as {}", canonical)])
1153 .expect_file(*actual, content)
1154 .check();
1155 }
1156 }
1157
1158 #[test]
1159 fn test_save_reuse_name() {
1160 Tester::default()
1161 .set_program(Some("loaded.bas"), "content\n")
1162 .run("SAVE")
1163 .expect_program(Some("MEMORY:loaded.bas"), "content\n")
1164 .expect_prints(["Saved as MEMORY:loaded.bas"])
1165 .expect_file("MEMORY:/loaded.bas", "content\n")
1166 .check();
1167 }
1168
1169 #[test]
1170 fn test_save_unnamed_error() {
1171 Tester::default()
1172 .add_input_chars("modified file\n")
1173 .run("EDIT: SAVE")
1174 .expect_program(None as Option<&str>, "modified file\n")
1175 .expect_err("1:7: Unnamed program; please provide a filename")
1176 .check();
1177 }
1178
1179 #[test]
1180 fn test_save_errors() {
1181 check_load_save_common_errors("SAVE");
1182
1183 Tester::default()
1184 .run("SAVE 2, 3")
1185 .expect_compilation_err("1:1: SAVE expected <> | <filename$>")
1186 .check();
1187 }
1188}