# CosmOS ## Overview: CosmOS is a small, single-tasking disk operating environment for the SplitBit 8-bit computer. It boots the machine, finds and mounts an SBFS filesystem, provides a command line and memory monitor, loads applications from disk, and takes control back when they finish. CosmOS is written entirely in SplitBit assembly. It is closer in scale and purpose to a resident monitor or an early disk operating system than to a modern multitasking OS: one program owns the machine at a time, there is no privilege boundary, and applications are assembled for fixed regions of memory. What it provides is a stable home from which those programs can be found, run, and given services without each one having to boot the machine for itself. ### Features: - Interactive Shell: Read commands from the SplitBit console and continue until `exit` or the end of input. - SBFS Filesystem: Mount, list, read, write, delete, and rename files on a SplitBit disk. - Paths: Anywhere a filename is taken, a path may be given instead - names with `/` between them, with `.` and `..`. Directories are read but not yet made; the host tool makes them. - Working Directory: `cd` moves the machine, `dir` lists where it is, and the prompt says where that is once it is not the root. A program may move too, and the shell puts the working directory back when the program stops. - Making Directories: `mkdir` and `rmdir` on the machine, and files written where their path says, so a disk can be organised without the host tool. - Loadable Applications: Validate SBEX files, copy their Program and Data segments into the addresses for which they were assembled, and start them at their declared entry point. - Invocation By Name: A word the shell has no command for is looked for on the disk as `.sbx`, and loaded and started if it is there. Built-in commands are tried first. - Resident Services: Applications can print strings and numbers, read lines, receive their command arguments, read and write files, and return to the shell through named software interrupts. - Application Vectors: Install interrupt vectors carried by a loadable program and restore whatever they replaced when the program exits. - Stack Reclamation: Save the system Stack before launching an application and take it back on exit, so an application need not unwind itself before returning. - Memory Monitor: Inspect and modify Program Memory, Data Memory, and registered device-memory banks through the SplitBit memory controller, disassemble instructions, and begin execution at an address. - Hardware Discovery: Mount the disk through the device registry rather than assuming that one is present at a particular controller bank. - Native Applications: Includes demonstrations, mathematical programs, interactive programs, a game, and a line-oriented text editor. - Reproducible Disk Image: The makefile assembles the system and every application, then constructs a fresh SBFS image containing the resulting executables. ## Building and Running: CosmOS currently lives inside the SplitBit Emulator repository and uses its assembler, emulator, and disk-image tool. From the repository root, build those tools first: ```sh make ``` That builds CosmOS, all of its applications, and a disk to boot them from - one makefile covers the machine and the system. To rebuild only part of it: ```sh make cosmos make disk ``` To boot CosmOS with that disk attached: ```sh make run-cosmos ``` Or on the Voyager, which is the same machine with a screen and a speaker instead of a terminal: ```sh make run-voyager ``` Both depend on the disk rather than merely using it, which is worth knowing: **what is on a disk is whatever was built when the disk was made.** A machine whose console has changed will start an old image quite happily and its programs will draw whatever the old way now means, which is a confusing thing to debug and an easy thing to avoid. The generated files are kept under `Programs/build/`: - `CosmOS/Source/cosmos.bin` is the bootable CosmOS image. - `CosmOS/Apps/*.sbx` are loadable application images. - `cosmos.img` is the SBFS disk containing those applications. **The disk carries every source in `Programs/`, mirrored.** Not a list kept in the makefile - a list goes stale the moment somebody adds a program and forgets to name it, and what they forgot is invisible until they go looking for it on the machine. Putting a file where the others live is the whole of putting it on the disk. That matters most for the things nobody thought worth shipping a binary of. A demo that is not interesting enough to build by default is still worth having the source of, because the machine can build it: ``` > cd /Source/Examples /Source/Examples> Asm colours.asm wrote colours.bin: program 114, data 86, labels 8 ``` Two things are left behind. `build`, because what a project builds is not what it wrote. And anything whose name is longer than a directory entry holds, which is **refused rather than skipped**: a disk quietly missing a file is exactly the failure a mirror exists to prevent, so the build stops and says which name to shorten. `/Lib` still holds the library sources separately, because that is where an `#Include` looks after looking beside the file that asked. The same files therefore appear twice - once as what a program includes, once as part of the source tree - and that is the difference between an installed library and a copy of the source. The disk is rebuilt from scratch when its applications change, so its contents describe the current source tree rather than accumulating files left by older builds. ## Lines Run Sometimes: ```text #! script set colour red if same $colour red echo it is red else echo it is not end ``` **`if` takes a command**, and what follows runs only if that command worked. That is one rule rather than two, and it is why comparing values needs no syntax of its own: `same` is an ordinary command that fails when its two words differ, so `if same $a $b` falls out of the rule instead of being an exception to it. Anything else that can fail can be asked about the same way - `if load Snake.sbx` is a perfectly good question. The shell already had the other half. `LineFailed` exists because a script stops at the first line that did not work, so every command in the system was already saying whether it had worked, for a different reason. **And two loops, which only mean anything in a script**, because a loop goes back to the line that opened it and a prompt has no line to go back to. ```text #! script for colour in red green blue echo it is $colour end set n go while same $n go echo round once set n stop end ``` The script reader keeps the position of **every** line before reading it, which is what makes that possible: by the time a line has been read the reader is past it, and a line is not a fixed size to subtract. A `while` is taken away at its `end` and its line asks the question again. A `for` is not: how many words it has used is kept in the block, and its line reads itself again and counts one more off the front - a byte in a block rather than a copy of the list in every one of them. A `for` with no words runs no times, and a loop inside a branch nobody is taking runs no times either. Blocks nest eight deep. A branch that is not being taken is **not even looked at**: the skipping happens before names are filled in, so `$whatever` inside a branch nobody is running is not a mistake, and a line nobody is running cannot fail. **Lines may be indented**, which they could not be before there was anything to indent inside. Spaces are taken off both ends of a line before anything looks at it - the leading ones so that a block can be indented, and the trailing ones because finishing a word with Tab leaves a space behind it, and a command that takes a file name would otherwise be looking for one whose name ends in a space. ## Names For Things: `set name value` writes one down, and `$name` anywhere on a later line stands for it. ```text > set apps /Apps > echo $apps/Copy.sbx /Apps/Copy.sbx ``` A name stops where a name stops - letters and digits - so it composes into a path without anything having to be quoted. Eight of them can be set at once, names up to fifteen characters and values up to forty seven. **The substitution happens on every line the shell is about to run**, typed or read from a file, so scripts and typing behave the same and no command has to know that variables exist. `set` on its own says what is written down. **A name nothing was ever set to does not run the line.** It says so, and the line counts as failed - which stops a script, the same as any other failure. Every other shell expands an unset name to nothing, and that is the wrong answer here: a mistyped name would quietly become an empty path, which is the kind of silent wrong answer the rest of this system spends its effort refusing. Somebody who genuinely wants an empty value writes `set name` with nothing after it and gets one, so the escape hatch exists and has to be asked for. A name longer than fifteen characters is an error for the same reason. Cutting it short would make two different names one variable and would put a word the person never typed into the message about it. A `$` with nothing name-like after it is just a `$`. ## Scripts: `do ` runs the lines in a file as though somebody had typed them. Every command works the same way it does at the prompt, because the only thing a script changes is where the next line comes from - the shell splits it, matches it and runs it without knowing the difference. ``` #! script ; Build the system and put it where the machine will find it. echo building CosmOS cd /Source/CosmOS Asm cosmos.asm echo done ``` `echo` is a command rather than a program on purpose. `Say.sbx` has printed words since before there were scripts and is the wrong shape for one: being a program, it has to be found on the disk and loaded and started, it prefixes what it was told with `it says:`, and the system prints `finished` after it - three lines of noise around one line of narration. **The first two bytes must be `#!`**, or the shell refuses the file and says so. That is what tells a script from anything else, and it is deliberately not the name and not a flag in the directory entry: the rule this filesystem keeps is that an entry holds only what the content cannot say about itself, and a script can say what it is. The loader already refuses anything that does not begin `SBEX`, so the two kinds of runnable file turn each other away without either of them having been told about the other. What follows the `#!` is ignored. It is where the name of an interpreter goes if there is ever a second one; today there is one and it is this shell. **`#` is a directive and `;` is a comment, exactly as in SplitBit assembly.** One rule across the machine rather than two dialects: `#` means this line is about the file, `;` means ignore this line. Comments and blank lines never reach the shell at all - they are dropped by the reader, so they are not echoed and the dispatch never sees a line it would have to know to ignore. This is not Unix's convention and is not trying to be; there `#!` genuinely is a comment that only the kernel looks at, while here the shell requires it. **Each line is echoed as it runs**, after the prompt, so that a script reads exactly like somebody typing it and a script that stops says where. **`#quiet` turns that off and `#loud` turns it back on.** The prompt and the echo go together, because together they are what makes a script look like typing - so a quiet script gets neither, and what it prints is all that appears. That is for the scripts whose own output is the point, where prompts interleaved with the message are just in the way: ``` #! script #quiet clear echo Segan Voyager echo CosmOS ready. ``` A nested script inherits quiet from the one that started it, on the grounds that a build which asked for quiet meant its helpers too, and gets its own setting back when the helper returns. A script started from the prompt always begins loud. **Anything else beginning with `#` is handed to the shell**, which does not know it, says so, and stops the script. A script that asked for something this shell cannot do should not carry on as though it had been given it. ## Starting Itself: If `/System/Boot/startup.sh` is there, it runs before anybody can type - every way of reaching the prompt for the first time goes through it, including the one where there is no disk, in which case there is simply nothing to find. **A missing one says nothing**, because a clean install has none and a machine that complained every boot about a file nobody wrote would be teaching its owner to ignore it. A file that *is* there and does not begin with `#!` is the other case entirely - somebody meant that to run - so it says so and carries on to the prompt. Between them, `startup.sh` and `#quiet` are how a machine gets a face: ``` #! script #quiet clear echo Segan Voyager echo CosmOS ready. ``` **A script stops at the first line that does not work.** A build whose first step failed and whose second step ran anyway produces something wrong and reports success, which is the whole reason the shell now remembers whether a line worked. What counts as not working is a command that failed, a name the shell does not know, or a program that exited with a status. Nothing is printed but `stopped: that line did not work` - whatever failed has already said what was wrong in words. **A script running out is not the same as typing running out.** The console ending means there is nobody there and the shell stops; a script ending means go back to whoever asked for it, so the next line comes from the console again. The interactive assembler reads its lines the same way, so a script can contain a block of assembly and end it with a `.` just as you would by hand. **A script can run another script, four deep.** What is remembered when one script starts another is a position and not a buffer - the name, which block comes next, how many are left, and where in the block it had got to. The block itself is read again on the way back, which costs one disk read per return and saves a 257-byte buffer per level. Four is deep enough for a script calling a script that calls a helper, and shallow enough that a script which runs itself says `do: scripts are only four deep` rather than filling memory. **A line that fails stops every level**, not just the innermost. A build whose helper script failed should not carry on in the script that called the helper. ## Shell Commands: CosmOS currently provides these built-in commands: | Command | Description | | -- | -- | | `dir` | List the files on the mounted disk and their sizes. | | `load ` | Read and validate an SBEX application, then place its code and data where its header requests. | | `run [words]` | Start the loaded application and make the rest of the line available to it as an argument. | | `do