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::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
36/// Category description for all symbols provided by this module.
37const 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
50/// Message to print on the console when receiving a break signal.
51pub const BREAK_MSG: &str = "**** BREAK ****";
52
53/// Default extension to add to file names.
54const DEFAULT_EXTENSION: &str = "bas";
55
56/// Representation of the single program that we can keep in memory.
57#[async_trait(?Send)]
58pub trait Program {
59    /// Returns true if the program was modified since it was last saved (as indicated by a call to
60    /// `set_name`).
61    fn is_dirty(&self) -> bool;
62
63    /// Edits the program interactively via the given `console`.
64    async fn edit(&mut self, console: &mut dyn Console) -> io::Result<()>;
65
66    /// Reloads the contents of the stored program with the given `text` and tracks them as coming
67    /// from the file given in `name`.
68    fn load(&mut self, name: Option<&str>, text: &str);
69
70    /// Path of the loaded program.  Should be `None` if the program has never been saved yet.
71    fn name(&self) -> Option<&str>;
72
73    /// Resets the name of the program.  Used when saving it.
74    fn set_name(&mut self, name: &str);
75
76    /// Gets the contents of the stored program as a single string.
77    fn text(&self) -> String;
78}
79
80/// Trivial implementation of a recorded program that doesn't support editing.
81#[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
115/// If the `program` is dirty, asks if it's OK to continue on `console` and discard its changes.
116pub 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
132/// The `DISASM` command.
133pub 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    /// Creates a new `DISASM` command that dumps the disassembled version of the program.
158    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
219/// The `EDIT` command.
220pub struct EditCommand {
221    metadata: Rc<CallableMetadata>,
222    console: Rc<RefCell<dyn Console>>,
223    program: Rc<RefCell<dyn Program>>,
224}
225
226impl EditCommand {
227    /// Creates a new `EDIT` command that edits the stored `program` in the `console`.
228    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
258/// The `LIST` command.
259pub 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    /// Creates a new `LIST` command that dumps the `program` to the `console`.
268    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
305/// The `LOAD` command.
306pub 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    /// Creates a new `LOAD` command that loads a program from `storage` into `program` and that
316    /// uses `console` to communicate unsaved changes.
317    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
394/// The `NEW` command.
395pub 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    /// Creates a new `NEW` command that clears the contents of `program` and that uses `console`
404    /// to communicate unsaved changes.
405    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
453/// The `RUN` command.
454pub struct RunCommand {
455    metadata: Rc<CallableMetadata>,
456    program: Rc<RefCell<dyn Program>>,
457    actions: Rc<RefCell<Vec<MachineAction>>>,
458}
459
460impl RunCommand {
461    /// Creates a new `RUN` command that executes the `program`.
462    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
498/// The `SAVE` command.
499pub 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    /// Creates a new `SAVE` command that saves the contents of the `program` into `storage`.
508    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
579/// Adds all program editing commands against the stored `program` to the `machine`, using
580/// `console` for interactive editing and using `storage` as the on-disk storage for the programs.
581pub 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    /// Checks errors that should be handled the same way by `LOAD` and `SAVE`.
826    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        // TODO(jmmv): Versions of EndBASIC before the core rewrite would show the "after"
984        // message after an abrupt termination from the program.  core cannot do this because
985        // of how the whole line is compiled first and how RUN then replaces the full program
986        // memory.  We should fix this at some point.
987        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}