Skip to main content

endbasic_std/
program.rs

1// EndBASIC
2// Copyright 2021 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//! Stored program manipulation.
18
19use 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
35/// Category description for all symbols provided by this module.
36const 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
49/// Message to print on the console when receiving a break signal.
50pub const BREAK_MSG: &str = "**** BREAK ****";
51
52/// Default extension to add to file names.
53const DEFAULT_EXTENSION: &str = "bas";
54
55/// Representation of the single program that we can keep in memory.
56#[async_trait(?Send)]
57pub trait Program {
58    /// Returns true if the program was modified since it was last saved (as indicated by a call to
59    /// `set_name`).
60    fn is_dirty(&self) -> bool;
61
62    /// Edits the program interactively via the given `console`.
63    async fn edit(&mut self, console: &mut dyn Console) -> io::Result<()>;
64
65    /// Reloads the contents of the stored program with the given `text` and tracks them as coming
66    /// from the file given in `name`.
67    fn load(&mut self, name: Option<&str>, text: &str);
68
69    /// Path of the loaded program.  Should be `None` if the program has never been saved yet.
70    fn name(&self) -> Option<&str>;
71
72    /// Resets the name of the program.  Used when saving it.
73    fn set_name(&mut self, name: &str);
74
75    /// Gets the contents of the stored program as a single string.
76    fn text(&self) -> String;
77}
78
79/// Trivial implementation of a recorded program that doesn't support editing.
80#[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
114/// If the `program` is dirty, asks if it's OK to continue on `console` and discard its changes.
115pub 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
131/// The `DISASM` command.
132pub 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    /// Creates a new `DISASM` command that dumps the disassembled version of the program.
156    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
215/// The `EDIT` command.
216pub struct EditCommand {
217    metadata: Rc<CallableMetadata>,
218    console: Rc<RefCell<dyn Console>>,
219    program: Rc<RefCell<dyn Program>>,
220}
221
222impl EditCommand {
223    /// Creates a new `EDIT` command that edits the stored `program` in the `console`.
224    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
254/// The `LIST` command.
255pub struct ListCommand {
256    metadata: Rc<CallableMetadata>,
257    console: Rc<RefCell<dyn Console>>,
258    program: Rc<RefCell<dyn Program>>,
259}
260
261impl ListCommand {
262    /// Creates a new `LIST` command that dumps the `program` to the `console`.
263    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
295/// The `LOAD` command.
296pub 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    /// Creates a new `LOAD` command that loads a program from `storage` into `program` and that
306    /// uses `console` to communicate unsaved changes.
307    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
384/// The `NEW` command.
385pub 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    /// Creates a new `NEW` command that clears the contents of `program` and that uses `console`
394    /// to communicate unsaved changes.
395    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
443/// The `RUN` command.
444pub struct RunCommand {
445    metadata: Rc<CallableMetadata>,
446    program: Rc<RefCell<dyn Program>>,
447    actions: Rc<RefCell<Vec<MachineAction>>>,
448}
449
450impl RunCommand {
451    /// Creates a new `RUN` command that executes the `program`.
452    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
488/// The `SAVE` command.
489pub 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    /// Creates a new `SAVE` command that saves the contents of the `program` into `storage`.
498    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
569/// Adds all program editing commands against the stored `program` to the `machine`, using
570/// `console` for interactive editing and using `storage` as the on-disk storage for the programs.
571pub 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    /// Checks errors that should be handled the same way by `LOAD` and `SAVE`.
813    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        // TODO(jmmv): Versions of EndBASIC before the core rewrite would show the "after"
971        // message after an abrupt termination from the program.  core cannot do this because
972        // of how the whole line is compiled first and how RUN then replaces the full program
973        // memory.  We should fix this at some point.
974        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}