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