> ls Copy.sbx Say.sbx Where.sbx Walk.sbx ls.sbx Lander where.sh dir says what is there one line each, with sizes and a tally of the disk. This says the names in columns and nothing else, which is what you want ninety times in a hundred. Two programs rather than one with a switch, because they answer different questions and neither answer is a worse version of the other. AND THIS IS A PROGRAM WHERE DIR IS BUILT IN, which is the difference that matters when the disk is what you are doubting: dir is already in memory and this has to be loaded off the disk it is about to list. The daily driver and the diagnostic. Two passes over the directory and no buffer at all. Columns need the longest name before the first line can be printed, which usually means holding every name - twenty four bytes each against a format that allows 1,024 entries. Walking twice costs a read of each directory block, into a buffer that is already there. Across and not down. Real listings go down the columns so that names next to each other alphabetically are next to each other on the screen, and that reason depends on sorting - which nothing here does. With the order arbitrary, down-and-across buys nothing and costs a division. Directories in blue and wearing a separator, unfinished saves in red, everything else plain: whether a file is runnable cannot be known without opening it, and opening every file in a directory to colour a listing is a price nobody agreed to pay. The colour reaches a terminal as well as the screen, so it is one mechanism and not two. ---- Three bugs, and one of them is this machine's oldest trap ---- nameLength answered in A, and a RET puts A back the way the caller had it. So it answered with nothing, and every gap between the columns came out the same width because the padding was subtracting whatever A happened to hold. Q is the ALU's output and nothing puts it back, which is why every answer here comes home in it. The padding subtracted the other way round - the name from the cell - which borrowed on every name that was not the longest, so all of them took the "wider than its cell" path and the listing came out separated by one space. And padding after a name left trailing spaces on every line that did not fill its last column. It goes before the next name now, so spaces only ever fall between two things and a line ends on a name. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
1997 lines
135 KiB
Markdown
1997 lines
135 KiB
Markdown
# 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, and
|
|
what is found is run according to what is inside it - a program is loaded and started, a
|
|
script is read as lines. 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 <file>` 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.
|
|
|
|
**Typing a script's name runs it too**, the same way typing a program's name does; `do` is how
|
|
a file is reached by its whole name, the way `load` is for a program. See Starting An
|
|
Application By Name.
|
|
|
|
### What A Script Is Given:
|
|
|
|
Whatever followed the name reaches the script, whichever way it was started - `do build.sh
|
|
cosmos.asm` and `build.sh cosmos.asm` hand it the same thing.
|
|
|
|
| Name | What it stands for |
|
|
| -- | -- |
|
|
| `$1` to `$9` | The words it was given, counted from one. |
|
|
| `$args` | All of them, exactly as they were typed. |
|
|
|
|
```
|
|
#! script
|
|
; build.sh <source>
|
|
echo building $1
|
|
cd /Source
|
|
Asm $1
|
|
```
|
|
|
|
**Nothing is stored per parameter.** The line is kept whole and the word wanted is walked out
|
|
of it, so there is no limit on how many words a script may be handed and nothing to keep in
|
|
step. `$args` is that line; `$1` is a word copied out of it.
|
|
|
|
**A word that was not given is an error**, exactly like any other name the shell does not
|
|
know: `$2` in a script given one word says `nothing is set called 2` and stops the script.
|
|
Expanding it to nothing would let a command run with an argument missing and report success,
|
|
which is the failure the whole stop-on-error rule exists to prevent. A script that genuinely
|
|
wants to work with or without something can ask `if same $args` - and `$args` is always set
|
|
inside a script, empty if it was given nothing, so that question can always be asked.
|
|
|
|
**At the prompt they mean nothing**, and say so. There is no script there to have been given
|
|
anything, so `$1` is a name nobody set.
|
|
|
|
**A script's own words win over anything `set`.** They are looked at before the eight named
|
|
slots, so a parameter cannot be shadowed by something left lying about at the prompt.
|
|
|
|
**Each script has its own**, saved and restored with the rest of where it had got to. A script
|
|
that hands different words to a helper still has its own when the helper returns. That is also
|
|
what makes a **launcher** possible: a script whose whole body is one line handing on what it
|
|
was told, without knowing what any of it means.
|
|
|
|
```
|
|
#! script
|
|
; /Apps/Lander - the launcher
|
|
Packages/app.Lander/Lander $args
|
|
```
|
|
|
|
```
|
|
#! 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 <path>` | 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 <script> [words]` | Run the lines in a file as though they had been typed, and hand it those words. See Scripts. |
|
|
| `echo [words]` | Say the rest of the line, or a blank line with nothing after it. |
|
|
| `clear` | Empty the screen. |
|
|
| `drive [n]` | Say which disk the shell is on, or go to another. See Several Disks. |
|
|
| `cd [path]` | Go to a directory, or to the root with nothing after it. |
|
|
| `mkdir <path>` | Make a directory. |
|
|
| `rmdir <path>` | Remove one, if it is empty. |
|
|
| `<name> [words]` | Any word the shell does not recognise is looked for on the disk, and started if it is found: loaded if it is a program, read as lines if it is a script. |
|
|
| `delete <file>` | Remove a file from the filesystem and release its blocks. |
|
|
| `rename <file> <to>` | Give a file a different name without moving its contents. |
|
|
| `set [name [value]]` | Give a name a value, or say what the names are. See Names For Things. |
|
|
| `if <command>` | Run the lines after it only if that command worked. See Lines Run Sometimes. |
|
|
| `else` | Run them only if it did not. |
|
|
| `end` | Close the block. |
|
|
| `while <command>` | Run the lines after it for as long as that command keeps working. |
|
|
| `for <name> in <words>` | Run them once for each word, with the name set to it. |
|
|
| `same <a> <b>` | Fails when the two are different, which is how `if` asks about a value. |
|
|
| `monitor` | Enter monitor mode, in which the prompt becomes `*` and the commands below are also available. |
|
|
| `help` | Show the built-in command summary. |
|
|
| `exit` | Leave monitor mode if in it, and otherwise halt the machine. |
|
|
|
|
Monitor mode adds the following. It is a mode rather than a separate program because a
|
|
loaded application occupies the one region a loaded application is given, so a monitor
|
|
which was itself an application could never examine another one. The mode persists: an
|
|
application started with `g` which returns through `osExit` arrives back at the monitor
|
|
prompt rather than at the shell.
|
|
|
|
| Command | Description |
|
|
| -- | -- |
|
|
| `x [address]` | Display 64 bytes as hexadecimal and as characters. |
|
|
| `d [address]` | Disassemble eight instructions. |
|
|
| `a <address>` | Assemble instructions into memory until a line containing only a dot. |
|
|
| `s <address> <byte>...` | Write bytes into the bank being examined, including Program Memory. |
|
|
| `b <program\|data\|bank>` | Select a memory space or a registered bank number. |
|
|
| `g <address>` | Begin execution at an address. |
|
|
|
|
`x` and `d` share a position, and each leaves it after what it displayed, so either may be
|
|
given without an address to continue from where the last one stopped.
|
|
|
|
**Everything the shell does still works in the monitor**, since it is a mode of the shell
|
|
rather than a different program: a program can be started by name, `dir` still lists, and a
|
|
word that is nothing at all is still told so.
|
|
|
|
For example:
|
|
|
|
```text
|
|
> dir
|
|
> load Snake.sbx
|
|
> run
|
|
```
|
|
|
|
Or, equivalently:
|
|
|
|
```text
|
|
> Snake
|
|
```
|
|
|
|
Loading and running remain separate operations, and both of them remain. A loaded program
|
|
may be run again without being read from disk again, which is useful both as a monitor
|
|
facility and as a test that CosmOS correctly restores its Stack and vector table after
|
|
every run; and `load` is how the monitor puts an arbitrary file in front of itself, which
|
|
is a thing typing a name deliberately cannot do.
|
|
|
|
### Typing A Line:
|
|
|
|
The shell reads what you type a key at a time and edits the line itself, which is why the
|
|
line can be moved about in at all.
|
|
|
|
| Key | What it does |
|
|
| -- | -- |
|
|
| Left, Right | Move a character. |
|
|
| Home, End | Go to the start of the line or the end of it. |
|
|
| Backspace | Take out the character before the cursor. |
|
|
| Delete | Take out the one under it. |
|
|
| Up, Down | Walk back through the last eight lines, and forward again. |
|
|
| Tab | Finish the word being typed, if there is only one thing it could be. |
|
|
| Return | Finish the line, wherever the cursor happens to be sitting. |
|
|
|
|
Anything typed goes in where the cursor is, so a word left out of the middle of a line is
|
|
put back by moving there and typing it, and the rest of the line moves along.
|
|
|
|
**This used to be three different things depending on where the machine was running.** On a
|
|
terminal the host held the line and did the echoing and the backspacing; behind a window the
|
|
console's own gatherer did it; from a file nothing did it at all. One job, three
|
|
implementations, and none of them here - which is why there was no way to move about in a
|
|
line, and nowhere for a history to live. Now the console delivers keys and says nothing
|
|
about what they mean, the same way it reports what a drive is and says nothing about what
|
|
should be on it, and the shell decides.
|
|
|
|
### Finishing A Word:
|
|
|
|
Tab finishes the word being typed. One match goes in with a space after it, because a word
|
|
that can only be one thing is finished. Several are folded into their longest common prefix
|
|
and that goes in, which is the most that can be said without guessing which was meant - and
|
|
if that adds nothing, because what is typed is already as far as they all agree, the matches
|
|
are listed and the line put back underneath.
|
|
|
|
**What it can be depends on where in the line it is.** The first word is a command or a
|
|
program; anything after it is a file.
|
|
|
|
| Where | What is offered |
|
|
| -- | -- |
|
|
| The first word | the fifteen commands, and every file, under the name it actually has |
|
|
| After it | anything on the disk, under its real name |
|
|
| Either, with a `/` in it | whatever is in the directory the word names |
|
|
|
|
A directory answers with a separator on the end instead of a space, which says what it is and
|
|
lets the next part be typed straight away.
|
|
|
|
**Files are looked for in the three places the shell would look to run one**: where you are,
|
|
`/Apps` on the disk you are on, and `/Apps` on drive 0. Offering something the shell would not
|
|
find would be finishing a word into a thing that then does not work. A file that is in two of
|
|
those places is offered twice and so is never the only match, which costs it the space after
|
|
it - the shell is comparing names, not deciding which file it would have run.
|
|
|
|
A first word used to be offered with `.sbx` taken off, and anything without that ending was
|
|
not offered at all. That was right while the extension was what made a file reachable; now
|
|
that the shell decides by reading, hiding the files without one would hide scripts - which is
|
|
much of what a first word is.
|
|
|
|
Nothing typed and nothing matching both do nothing, quietly.
|
|
|
|
**This is the same wall the history was behind.** A shell that never sees a keystroke has no
|
|
moment at which somebody has typed half a word - the terminal hands over finished lines - so
|
|
there is nothing to press Tab at. What made it possible was moving the line editing into the
|
|
system, and everything since has been downstream of that one decision.
|
|
|
|
The commands are walkable because they are **packed**: fifteen names, each ending in the zero
|
|
that says where the next begins. The dispatch is a chain of comparisons and cannot be walked,
|
|
so the names have to be data as well as code, and `make test` checks that the two agree.
|
|
|
|
### The History:
|
|
|
|
The last eight lines are kept, and Up walks back through them. It is a ring: a ninth line
|
|
pushes the oldest out by moving where the ring starts rather than by moving any of the
|
|
lines.
|
|
|
|
An empty line is not kept - that is somebody pressing Return - and neither is a line the
|
|
same as the one already at the top, so running a command twice does not put it in twice.
|
|
|
|
**What you were typing is kept too.** Pressing Up when you were half way through a line puts
|
|
that line somewhere and Down brings it back, so looking at what you did before does not cost
|
|
you what you were doing.
|
|
|
|
The history belongs to the shell rather than to the console, and that is the whole reason it
|
|
can exist: until the keys reached the system there was nothing to press Up at. It is shared
|
|
with the monitor, which reads its lines the same way - and since the monitor reads into a
|
|
buffer of forty characters where the shell reads into one of 127, a longer line recalled
|
|
there is cut short rather than written past the end of it.
|
|
|
|
The line holds 127 characters. It held 63 until the shell could edit one, which is when the
|
|
limit started to be felt: a copy between two disks with a directory on each is most of the
|
|
way there before anything has been said.
|
|
|
|
**An application reading a line through `osReadLine` gets the same editing**, which is the
|
|
point of the service being there: the editor is a program, and a word typed with two letters
|
|
the wrong way round can now be put right without starting the line again.
|
|
|
|
What a program does *not* get is the history. Up and Down do nothing while a program is
|
|
reading, and nothing it reads is kept. That is not meanness: the editor would otherwise fill
|
|
the history with the text of somebody's document, and pressing Up in the middle of writing
|
|
one would put `dir` into it. The history belongs to the thing whose lines are commands.
|
|
|
|
### Paths:
|
|
|
|
Everywhere CosmOS takes a filename it will take a path: names with `/` between them,
|
|
walked from the root, with `.` meaning where you are and `..` meaning the directory above.
|
|
`..` from the root is the root. A bare name is a path of one name, so nothing written
|
|
before directories existed had to change.
|
|
|
|
```text
|
|
> load /Apps/Snake.sbx
|
|
> Type /Notes/today.txt
|
|
```
|
|
|
|
**Programs did not have to be taught any of this.** Path resolution lives inside
|
|
`sbfsFind`, below the services, so `osFileInfo`, `osFileBlock`, `osFileSave`,
|
|
`osFileDelete` and `osFileRename` all still take a pointer to a name - and a path is
|
|
simply a longer name. `Type`, `More`, `Edit` and the assembler gained subdirectories
|
|
without a line being changed in any of them.
|
|
|
|
Each *name* along a path is still the 22 characters a directory entry holds, and a longer
|
|
one is refused rather than cut short, because a name cut to 22 characters is a different
|
|
name that might well be some other file's.
|
|
|
|
### What It Costs:
|
|
|
|
**The assembler is superlinear in what it reads**, and that is worth knowing before reaching
|
|
for it on something large. Cycles per byte of source climb with the size of the source:
|
|
|
|
| Source | Bytes | Cycles a byte |
|
|
| --- | --- | --- |
|
|
| `colours.asm` | 4,299 | 1,383 |
|
|
| `Edit.asm` | 16,778 | about 3,000 |
|
|
| `cosmos.asm` | about 104,000 | 6,290 |
|
|
|
|
So assembling the operating system is 654 million cycles, which is eleven minutes at a
|
|
megahertz. **It is not the disk**: the same build costs 654 million on a disk carrying the
|
|
whole source tree and 653 million on a flat one with a sixth as many files. The suspected
|
|
cause is looking a label up by walking the whole table, of which there are about nine
|
|
hundred, once for every reference - suspected rather than measured.
|
|
|
|
Nothing is being done about it, deliberately. Development happens with the host assembler,
|
|
which is where the tooling is, and the machine assembling itself is a demonstration that it
|
|
can rather than the way anybody works. But faster hardware buys a constant factor and does
|
|
not change the shape of the curve, so the program that eventually forces this is not CosmOS
|
|
- it is the first one twice its size.
|
|
|
|
### The Working Directory:
|
|
|
|
`cd` moves the machine. A path beginning with `/` is measured from the root and anything
|
|
else from where you are, so a bare name means a file in the current directory - which is
|
|
the whole of what a working directory is, and no program had to be told.
|
|
|
|
```text
|
|
> cd /Apps
|
|
/Apps> dir
|
|
/Apps> cd Deep
|
|
/Apps/Deep> cd ..
|
|
/Apps> cd
|
|
>
|
|
```
|
|
|
|
`cd` with nothing after it goes to the root, which is the only place always there.
|
|
|
|
**The prompt says where you are, but only when that is not the root**, so a machine nobody
|
|
has moved about on looks exactly as it always did. Nothing stores the path: the working
|
|
directory is an entry index and two bytes, and the text on the prompt is worked out again
|
|
each time by walking the chain of parents upward.
|
|
|
|
That walk goes from where you are up to the root, so the names arrive deepest first and
|
|
are written into the buffer **backwards, from its end**. When they do not all fit, what is
|
|
already down is the deep end of the path, which is the end worth keeping - so the prompt
|
|
is cut at the front and says so:
|
|
|
|
```
|
|
...opqrst03/abcdefghijklmnopqrst04/.../abcdefghijklmnopqrst08>
|
|
```
|
|
|
|
**Three limits, and the smallest is not the one you would guess.** A path handed to any
|
|
one operation is capped at 95 characters up to the last separator plus a name of 22; the
|
|
host tool carries 512, which only means it can build a tree CosmOS cannot name in one
|
|
piece. Neither of those binds anything: the longest path on a full install is 21
|
|
characters. What binds is the prompt's 127 bytes - and **nothing caps how deep the
|
|
directories go**, because `mkdir a` and `cd a` are each well inside every limit and can be
|
|
typed all day.
|
|
|
|
Before the walk was bounded it wrote past the front of that buffer and into whatever the
|
|
assembler had put below it, which was the shell's own command names. Six directories of 22
|
|
characters was enough. The first five bytes to go were the word `exit`, so the shell
|
|
stopped recognising the command for leaving - a fault with no plausible connection to the
|
|
directory you happened to be standing in.
|
|
|
|
`dir` lists the directory you are in rather than the whole disk.
|
|
|
|
A program can move too, with `osChangeDir`, and **the shell puts the working directory
|
|
back when the program stops** - the same discipline it already applies to the Stack and to
|
|
the vector table, and for the same reason. A program is entitled to move about; the shell
|
|
is entitled to find itself where it left off.
|
|
|
|
Whenever what a relative path means changes - a `cd`, a program calling `osChangeDir`, a
|
|
program exiting - CosmOS forgets the file it was remembering. That cache is keyed on the
|
|
path as it was typed, so `notes.txt` is the same key in two directories and nothing about
|
|
the entry it holds would look wrong. It is the kind of stale that gets believed rather
|
|
than noticed.
|
|
|
|
### Making And Removing Directories:
|
|
|
|
`mkdir` and `rmdir` are the machine's own, so a disk can be organised without the host
|
|
tool. A file a program writes goes where its path says, and a bare name means the
|
|
directory you are in.
|
|
|
|
**A directory costs one entry and no blocks at all.** Its start, block count and tail are
|
|
all zero, which is what keeps the flat array of entries the whole allocation map - with
|
|
files laid down contiguously, every block is inside some entry's range or it is not, and
|
|
an entry with no range is in nobody's way.
|
|
|
|
Making the first directory on a disk is what raises it from version one to version two,
|
|
because it is the only thing that makes the difference between them real. A disk stays
|
|
readable by anything that has never heard of a directory right up until it actually has
|
|
one.
|
|
|
|
**A disk may have at most 8,191 directory blocks**, which is 65,528 entries, and that
|
|
number comes from the parent field rather than from anything about size. A parent is an
|
|
index *plus one* in two bytes, so entry 65,535 has no parent number at all: adding one
|
|
wraps to zero, and zero means the root.
|
|
|
|
The failure is worth describing, because it is the shape of failure this format has to
|
|
watch for. Such an entry does not refuse what is put inside it. It writes a parent of zero
|
|
and the thing lands in **the root**, while whatever asked is told it went where it asked
|
|
for. Looking in that directory afterwards finds nothing, because the search is for a
|
|
parent the entry does not carry - so the same create succeeds again, and again, filling
|
|
the root with entries of one name. Two entries of one name in one directory is precisely
|
|
what `rename` refuses on the grounds that a search answers with whichever it meets first
|
|
and the rest can never be reached again; this made them by the handful, one per attempt.
|
|
|
|
Both implementations refuse to format past the bound, and refuse to read a disk that
|
|
claims it - because a disk claiming it was made by something that never checked.
|
|
|
|
Four things are refused, and each refusal is the reason a separate command exists:
|
|
|
|
**`rmdir` will not take a file and `delete` will not take a directory.** Neither can be
|
|
the one that removed more than was asked for.
|
|
|
|
**A directory with anything in it is refused.** This is not politeness. A parent is an
|
|
entry *index*, and a freed index is handed to the next thing created - so the children of
|
|
a directory removed from under them would turn up inside whatever took its place. Nothing
|
|
points downward, so there would be no way to find them afterwards and no way to notice.
|
|
|
|
**A name already used in that directory is refused.** Two entries with one name in one
|
|
place is a directory that cannot be searched sensibly: a search answers with whichever it
|
|
meets first, and the other becomes unreachable without ever having been deleted. The same
|
|
name in a *different* directory is fine, and is the point of the exercise.
|
|
|
|
**`rename` will not move anything.** Only the twenty two bytes of the name change and the
|
|
parent is not among them, so `rename a/x b/y` would be a lie the disk went along with.
|
|
|
|
`Tests/agree.sh` builds the same disk twice, once with SplitDisk and once with CosmOS, and
|
|
compares the images byte for byte. Every field one writes and the other only reads is
|
|
checked there and nowhere else: which entry a thing lands in, which block, what a
|
|
directory's unused fields hold, the version, the free count.
|
|
|
|
### Two Listings:
|
|
|
|
`dir` says what is there one line each, with sizes and a tally of the disk. `ls` says the
|
|
names in columns and nothing else, which is what you want ninety times in a hundred.
|
|
|
|
```text
|
|
> ls
|
|
Copy.sbx Say.sbx Where.sbx Walk.sbx ls.sbx Lander where.sh
|
|
```
|
|
|
|
They are two programs rather than one with a switch, because they answer different questions
|
|
and neither answer is a worse version of the other.
|
|
|
|
**And `ls` is a program where `dir` is built into the shell.** That is the difference that
|
|
matters when the disk is the thing being doubted: `dir` is already in memory, and `ls` has to
|
|
be loaded off the disk it is about to list. The daily driver and the diagnostic, and it is
|
|
worth having both.
|
|
|
|
**Two passes over the directory, and no buffer.** Columns need the longest name before the
|
|
first line can be printed, which usually means holding every name in memory - twenty four
|
|
bytes each, against a format that allows 1,024 entries on a disk. So the directory is walked
|
|
twice instead: once to measure, once to print. That is also the first thing `osDirFirst` made
|
|
possible which `dir` could not already do.
|
|
|
|
**Across, and not down.** Real listings go down the columns so that names next to each other
|
|
alphabetically are next to each other on the screen - and that is a reason which depends on
|
|
sorting. Nothing here sorts; entries come back in the order the directory holds them. With the
|
|
order arbitrary, down-and-across buys nothing and costs a division on a machine that cannot
|
|
divide. If sorting ever arrives, that is the moment to change it.
|
|
|
|
**The width comes from the screen**, port `0x32`, which is 80 while CosmOS is running because
|
|
CosmOS asks for the wide mode as it starts.
|
|
|
|
**Padding goes before a name rather than after it**, so that a line ends on the last character
|
|
of a name. Trailing spaces on a line are the kind of thing nobody sees until something else
|
|
reads the output.
|
|
|
|
### What A Listing Says About The Disk:
|
|
|
|
`dir` ends with what is there and what is left:
|
|
|
|
```text
|
|
43 files, 3 directories
|
|
46 of 64 entries, 1893 blocks free
|
|
```
|
|
|
|
**Entries come first because they are the ceiling nobody notices until they hit it.** A disk
|
|
of small files runs out of directory slots long before it runs out of blocks, and saying both
|
|
means never having to work out which one is about to bite.
|
|
|
|
**The free figure is counted, not asked for.** The superblock keeps a free count and
|
|
`sbfs.asm` calls it *"a note rather than the truth"* in three separate places; `sbfsSpace`
|
|
reads the whole directory table instead, which costs a read per directory block and is the
|
|
answer rather than a guess. SplitDisk reads the note as well and says so when the two
|
|
disagree, which is the right place for that check: the host tool is what you audit a disk
|
|
with, and this is what you work on one with.
|
|
|
|
**And it is a fact about the disk, not about where you are standing.** The first version of
|
|
this added the blocks up as the listing walked past them, which cost nothing extra and was
|
|
wrong - that walk stops only on entries in the working directory, so the same disk came out as
|
|
1,996 blocks free from the root and 2,025 from `/Apps`. The two implementations of this format
|
|
disagreeing is what said so.
|
|
|
|
A third line appears when it needs to:
|
|
|
|
```text
|
|
4 of 16 entries, 37 blocks free
|
|
the longest run is 25
|
|
```
|
|
|
|
**Files are laid down contiguously**, so the free total is not what decides whether a file
|
|
will fit - the longest unbroken run is. A disk with a thousand blocks free in ten pieces
|
|
refuses a file of two hundred, and nothing else in the listing would hint at it.
|
|
|
|
**It is said only when it differs from the free total**, which on a healthy disk it does not.
|
|
Deleting is what fragments a contiguous store, and a disk that has only been appended to has
|
|
one gap, at the end. A line printed under every listing saying the same number twice is noise;
|
|
one that appears only when something is wrong is a line somebody reads.
|
|
|
|
Finding it costs a pass of the directory table per gap. There is no sort on this machine and
|
|
the entries are in no order, so a candidate walks the disk instead: each pass looks for the
|
|
used extent nearest at or after it, and anything the candidate is standing inside pushes it to
|
|
the far end and starts the pass again - the same trick allocating uses, for the same reason.
|
|
So the measurement costs almost nothing on a disk with one gap and more the more fragmented
|
|
the disk is, which is the right way round: it is slow exactly when it has something to say.
|
|
|
|
### Starting An Application By Name:
|
|
|
|
A word the shell has no command for is not immediately an error. Before saying so, the
|
|
shell looks for a file of that name and, if one is there, decides what to do with it by
|
|
what is inside it. Whatever followed the word reaches the program through `osArgument`, the
|
|
same way and by the same route as whatever follows `run`.
|
|
|
|
**The shell runs what it reads.** A file beginning `SBEX` is loaded and started exactly as
|
|
`load` and `run` would; a file beginning `#!` is read as lines exactly as `do` would. The
|
|
loader already refuses anything that is not `SBEX` and the script reader already refuses
|
|
anything without the shebang, so between them the two kinds of runnable thing turn each
|
|
other away and neither has to know the other exists. **The name says nothing about which
|
|
happened**, which is what makes `.sbx` a convention rather than a rule.
|
|
|
|
Four properties of this are deliberate:
|
|
|
|
**Built-in commands are tried first and always win.** The search happens only after the
|
|
whole dispatch chain has failed to match, so a file named `dir.sbx` cannot become `dir`.
|
|
The commands that are worth trusting when the disk is the thing being doubted stay
|
|
trustworthy.
|
|
|
|
**The word as typed is tried first, and only then the word with `.sbx` on the end.** The
|
|
first is what makes a file reachable under the name it actually has - a script, a program
|
|
somebody dropped the extension from, or a launcher. The second is what keeps every program
|
|
already on a disk startable by the short name people have always typed for it. The suffix
|
|
can only ever be a second guess, so a file that is really there always wins over one that
|
|
would have to be invented, and typing `notes.txt` no longer sends the shell after a
|
|
`notes.txt.sbx` that was never going to exist.
|
|
|
|
It costs a second walk of each directory when a word is not found in it, which is the price
|
|
of the exact name being asked for first.
|
|
|
|
A path works here too, so `/Apps/Say hello` starts `/Apps/Say.sbx` and gives it `hello`.
|
|
|
|
**Two places are tried, in order: where you are, and then `/Apps`.** The first is what makes
|
|
a program you are working on the one that runs; the second is what lets `Snake` work from
|
|
anywhere without a copy of it in every directory. A word that already begins with `/` has
|
|
said where to look, so only that place is tried. Neither is stored anywhere, so there is
|
|
nothing to configure and nothing to go stale - a search path somebody could set would need
|
|
somewhere to live between one boot and the next, and there is no such place yet.
|
|
|
|
**A file that is found but is neither says so.** If `notes.txt` exists and begins with
|
|
neither `SBEX` nor `#!`, typing `notes.txt` reports `that is not a program or a script`
|
|
rather than `I do not know: notes.txt`. Reporting an unknown command about a file that is
|
|
sitting on the disk would send somebody looking in the wrong place.
|
|
|
|
Names are matched exactly, including case, because every other name on the filesystem is.
|
|
What limits the typed word is the buffer it is built in rather than the format: each name
|
|
along a path is still twenty two characters, and the path walker refuses a longer one
|
|
rather than cutting it down. A word that will not fit is reported as unknown, which is the
|
|
truth, since nothing the shell can reach is called that.
|
|
|
|
CosmOS also boots without a disk. It reports that no filesystem was found, leaves the
|
|
shell and memory monitor available, and refuses commands that require a mounted disk
|
|
without stopping the machine.
|
|
|
|
## What Is On The Disk:
|
|
|
|
`make disk` builds the disk this system is meant to be met on, and it is
|
|
laid out in three directories:
|
|
|
|
| Where | What |
|
|
| -- | -- |
|
|
| `/Apps` | What you run. The second place the shell looks for a word it does not recognise, so anything here starts by name from anywhere on the disk - programs, and scripts too. |
|
|
| `/Source` | The things you name to the assembler: CosmOS itself, the assembler itself, and small programs to read. |
|
|
| `/Packages` | What applications own. One directory per packaged application, named `app.<name>`, holding the program and its assets so they can be found together and removed together. |
|
|
| `/Lib` | The things those include. Everything here is named by an `#Include` somewhere and by nothing else, which is what makes it a library rather than a source. `path.asm` is the newest of them: joining a name to a place, which is arithmetic on two strings and so wants no help from the system. |
|
|
|
|
The split is by role rather than by which directory the host keeps a file in, and it only
|
|
works because **an include is looked for where you are and then in `/Lib`** - the same rule
|
|
the shell uses for programs, applied to the assembler. Without that search every source
|
|
that calls a service would have to sit beside `services.asm`, and there would be nothing to
|
|
organise.
|
|
|
|
So the machine rebuilds itself from its own disk:
|
|
|
|
```text
|
|
> cd /Source
|
|
/Source> Asm cosmos.asm
|
|
wrote cosmos.bin: program 9778, data 3346, labels 648
|
|
/Source> Asm Asm.asm
|
|
wrote Asm.sbx: program 7570, data 4114, labels 562
|
|
```
|
|
|
|
Both come out byte for byte what the host assembler makes from the same source.
|
|
|
|
## Included Applications:
|
|
|
|
`Programs/CosmOS/Apps` holds what the shell can load, and the application disk is built
|
|
from every assembly file in it. Several are old programs written for the bare machine that needed five edits each to become loadable ones - the Fibonacci and sieve programs, `greet`, and `hello`. The rest were written for the system as it is now, and each of those exists to show one thing working:
|
|
|
|
| Program | What it is for |
|
|
| --- | --- |
|
|
| Life | Conway's Game of Life, which had to be taught to stop, since a program that never ends takes the shell with it. Polls the console between generations. |
|
|
| Snake | A game. Draws a whole screen with cursor addressing and steers with single keys, asking the console once a frame and never waiting. |
|
|
| Keys | The console interrupting rather than being asked. The only one that brings a vector of its own, which is what the version two format exists for. |
|
|
| Play | Four voices on one clock, which is what music is and one channel cannot be. The timer keeps a tick and every voice keeps its own place in its own track and its own count of how much longer the note it is holding lasts, so the parts move at four different rates and share nothing but the beat. A track is pairs of bytes, what to play and how many ticks it lasts: 1 to 127 is a MIDI note, zero is a rest, and 255 ends it - MIDI stops at 127, so neither of those had to be invented. A note's duration is its whole life and the gate goes down when the count runs out, which means a gap between two notes is written as a rest rather than invented by the player out of some fraction it decided on. Each voice loads an instrument of its own before a note is played - an oboe for the melody, strings under it, a square wave for the bass and a kalimba for the arpeggio - out of the format SoundPatch writes, which the player reads as a count and that many parameter and value pairs and understands nothing else about. Four patches can be up at once because a patch belongs to its channel; Kalimba has an LFO switched off and the other three have one on, and under a device where the LFOs belonged to the whole machine the last patch loaded would have imposed its setting on every part. A voice does not play one long track: it walks an ORDER LIST of its own, a table of sequence addresses, and takes the next one when a sequence runs out. That is where repetition comes from and it costs no notation - the bass plays the same sequence in the first bar and the last, written once. The four columns are how it reads and how a tracker would show it; per voice is how it is stored, because a voice's order cursor is then a pointer it advances by itself. Sequence and patch names are INDICES through two tables, which are the only places an address lives - so a tune read from a file will need its base added to two arrays and nothing else, rather than a loader that walks every sequence looking for addresses to correct. A sequence can also carry commands, which take no time at all: 0x80 plays the rest of that voice on a different patch, which is how the melody's last bar becomes a swell rather than a reed. Nothing keeps the voices together except that their sequences add up to the same length, which is the first thing a compiler should check. The patch each voice starts on is declared rather than assumed, because a voice given no instrument would play on whatever the device woke up with. `Play <file>` reads a tune and plays that; `Play` on its own plays the one built into it. A tune file is "SBTU", a version, the tick in cycles, and offsets to a patch table, a sequence table and four order lists - everything in it an OFFSET from wherever it was put, so loading one is adding the base to two tables and pointing four voices at their order lists. No sequence is walked and nothing inside one is an address, which is what makes a malformed tune something that plays wrongly rather than something that takes the loader with it; the magic is checked first, because the loader follows what the offsets name. The patch each voice starts on is in the header, because a starting instrument is state. The engine that plays it is Libraries/player.asm rather than this program. It spends over ninety nine per cent of its time asleep, because a beat is something to be woken by rather than counted up to. |
|
|
| Say | Prints whatever it was told, which is the shortest thing that shows osArgument working. |
|
|
| ls | The listing you read, where dir is the listing you audit: the names in columns, sized to the longest of them, with directories in blue and wearing a separator. Takes a directory to list, and says nothing at all about an empty one. |
|
|
| Walk | Says what is in the working directory, one line each: what kind of thing it is, its name, and how many blocks. The shortest thing that shows osDirFirst and osDirNext working, and the first program that could see a directory at all. |
|
|
| Where | Says the path it was loaded from, and with a name after it, the path of that name beside it. The shortest thing that shows osWhereAmI and path.asm working, and the answer follows the program rather than whoever ran it. |
|
|
| Reboot | Starts the machine again, in 45 bytes. Writes a port rather than asking the system, because a reset has to work when the system does not. |
|
|
| Once | Asks the loader to start something else on the next start, and only that one, in 569 bytes. |
|
|
| Status | Says what the last program made of what it was asked to do, in 222 bytes. The shell keeps the number and does not print it; this is how a person looks. |
|
|
| Settle | Says how the last start went and tells the machine to stop falling back, in 353 bytes. A program rather than a shell word, because the shell is for what cannot be done without it. |
|
|
| Files | Writes a file, reads it back, renames it and deletes it, in 675 bytes, including nothing but the service names. It is what says a program does not need a filesystem inside it. |
|
|
| Break | Stops itself twice with SWI osBreak, so that the registers can be seen changing between one stop and the next. |
|
|
| Grid | The first program to use the screen as a screen. Redefines a tile above the font, fills all 128 map rows, and scrolls it diagonally a pixel at a time. |
|
|
| Sprite | Moves a ball across the shell's own text, writing not one byte of the map to do it. It leaves the sprite in the table on the way out, because clearing them is the system's job - see below. |
|
|
| Pad | Says what the controllers are doing, printing a line whenever one changes. It tells apart the three things that look identical from inside a game that is not responding: a pad nobody noticed, a pad mapped to nothing, and a mapping that is wrong. |
|
|
| Lander | Lunar Porter, rung one: a lander over a moon that wraps. Flown with a controller if there is one - a held thruster burns every tick it is held for - and with the arrow keys if there is not, where one press is one burn and that is the most the console can say. A bar at the bottom is the sideways drift, drawn as a sprite stretched to the speed - a moon has no air, so a drift never stops by itself and stopping one means cancelling it exactly, which is hard to do blind. It opens on a splash: the studio, and whatever splash.tune holds, played over it before the world is built - found beside the game by asking the system where the game came from, rather than at a fixed place on the disk, since the game lives in /Packages/app.Lander now and what you type is a launcher in /Apps. Lander includes Libraries/player.asm and owns the timer and all four channels while it plays, because nothing else is happening yet - and it POLLS the timer rather than being interrupted by it, the same way it waits for the screen, so it brings no handler that would have to be taken away before the game starts. Any key or A or Start skips it, and one already held when it begins means no splash at all rather than a picture nobody wanted; a missing tune means the logo and silence, the way a missing /lander.state means the defaults stand. A flight begins where a flight begins: run does not reload, so the numbers the assembler wrote are load-time values and an init routine puts back the ones a new flight needs - including the terrain seed, which is a written number, so the moon is the same moon every time rather than whatever the last run's random walk left behind. What is not on that list keeps what the last run left it, which is a decision rather than an oversight. It lands or crashes on arrival, and what decides is the speed at the moment it touches: gentler than three quarters of a pixel a frame downwards and half of one sideways, or it is a lander on its side. A fuel gauge sits in the window layer, where the moon turning underneath cannot scroll it away, and every thruster costs a unit of fuel every tick it fires. It turns from green to red at a quarter of a tank, and a warning sounds ONCE on the way down through it - once, because a lander is at its most careful in the last seconds before it touches and something repeating in its ear through that is a distraction rather than a warning. Filling up at a base allows it again. The sound is the moment it happened and the colour is how things stand, and both come off the same number so they cannot disagree. An empty tank is not an ending: it is a lander still flying that can no longer do anything about where. Four landing pads are carved into the moon after it is generated, levelled to whatever height their first column happened to have, and marked in cyan by an attribute rather than a tile of their own. The lander starts above one, because that is where a porter's day begins. Each pad is a base, told apart by the colour it is drawn in, and landing at one either loads cargo for the base across the moon or delivers what is aboard and pays eighty units of fuel. A landing is not an ending: the lander rests where it is until the throttle opens again, and A or Start says "read it" as readily as a key does. What a base says goes in the window rather than out of the console: the console draws into the map, so a message printed while flying is one the lander then flies over, and printing scrolls the whole world up a row. Opening the throttle wipes the line. Two bars read the speeds and are green while a landing would survive and red while it would not, so "can I put down" is a glance rather than a sum. A third runs up the left edge and is how much sky is under the lander, half a pixel of bar to a pixel of it; it reads nought the moment the lander is down, and it exists because the orbit takes the lander off the top of the screen and a panel that only works while the ground is in sight is no use above it. And gravity here is the pull MINUS the swing outwards, which is what makes an orbit rather than a one way trip: under four pixels a frame sideways the pull wins and the lander falls, over it the swing wins and the lander climbs, and at it they cancel and it circles. Falling buys sideways speed and climbing spends it - at a rate set by the product of the two, so the trade stops by itself as the sideways speed runs out - and that is the cycle: a fall carries the lander past orbital speed and becomes a climb, the climb pays it back and becomes a fall, periapse and apoapse, round and round. Orbital speed is marked on the drift bar, sixty four pixels either side of the middle, because a number nothing points at is folklore. Gravity never falls off and the moon wraps, so a circular orbit is the same length at any height and one orbital speed serves everywhere. There is a ceiling 192 pixels above the world's origin, which is the top of the wide view - it was sixty four while sixty four was all the sky a forty column screen had over the origin, which was a fact about the view rather than about the world. It pins rather than ends: leaving upward is recoverable, so the lander is held there and told so, and one sideways is spent every tick it tries to climb - without that the pin wiped the climb, the trade saw neither fall nor climb, and the speed pushing it up never changed. What kills you out here is running dry a long way from the ground. B, or z on the keyboard, swaps the screen between forty columns and eighty, which is the same map, the same cells and the same engine at half the size: nearly two thirds of the moon at once for the orbit, and twice as big for the landing. The twenty extra rows that buys go to the SKY and not to the moon - the wide view starts twenty four rows above the world's origin rather than at it, so the ground sits near the bottom and the whole flyable band is on the screen. Drawn down from the origin instead it was 71 per cent rock, which is a zoom that shows you more of the thing you cannot fly through. A lander at the ceiling is off the top of a forty column screen, which is where the orbit lives and why the altitude bar had to exist; zoomed out it is in the picture. And there is a station up there, twelve rows above the world's origin - about two thirds of the way from the ground to the ceiling, clear of work near the surface - going round at orbital speed - which needs no physics of its own, because a body at 64 sixteenths is exactly what a circular orbit IS under these rules, at any height, since gravity never falls off and the moon wraps. It is off the top of a forty column screen too, so it is somewhere to go that the zoom is needed to see. Docking it is landing on something that is moving: close enough, and slow enough RELATIVE TO THE STATION, or it is a wreck. Its speed is orbital speed, which is the number the marks on the drift bar point at, so the instrument for it was on the screen before there was anything to dock with. A dock pays eighty units of fuel and holds the lander a tile under the station until a thruster lets go, sliding it into line at half a pixel a frame rather than snapping - a dock is allowed eight pixels out either way, so putting the lander exactly in place the instant it took hold moved it a whole tile in one frame, right at the moment the player was being told they had been careful. It settles squarely under the port, which it did not used to: the lander is drawn from half a screen LESS HALF A TILE, because that is what centres it, and the station was drawn from half a screen exactly, so a perfectly flown dock still looked four pixels out. Getting there is a two burn manoeuvre and not a straight line: climbing SPENDS sideways speed, so a lander cannot rise while matched - it arrives slower than orbital every time, and has to raise the far side of its orbit and then circularise at the top. Which is also why it reads /lander.state if the disk has one: sixteen bytes of position, velocity, station, fuel and screen, laid straight over the numbers it would otherwise start with, so a rendezvous can be examined without flying one first. A disk without the file is the game as it always was. The buffer is a whole block wide because osFileRead lands a file in blocks of 256, and sixteen bytes of room for it wrote over everything that followed. A landing kicks up dust, sideways and up off the lander's feet, because that is where kicked dust goes and there is ground in the way of the rest of it. Letting go of the station vents gas instead, evenly in every direction, since nothing is in the way of a docking port - and that one does NOT stop the world the way the others do, because it goes off on the frame a thruster is pressed and freezing then would be felt as the controls sticking. There is none on docking: a dock is a catch and not a touchdown, and there is nothing under it to kick. A lander also has to get two tiles clear of the station before it can take hold of it again, because a docked one sits exactly one tile under it - so letting go upwards docked it straight back on, took the fuel again, and sat waiting to be told the message had been read. A thruster bangs when it lights and rumbles while it burns, and EVERY thruster that catches bangs - a pilot already burning upwards who then adds a sideways one has lit an engine, so what is watched is which of them are lit rather than whether any is. The rumble is struck once and runs until every one is out, since restriking it when a second joins would start its attack over, which is a stutter rather than an engine, and the rumble is a HELD note: its gate goes down when one lights and does not come up until every one is out - or until anything stops to wait, because a held note outlives the loop that was holding it and landing on the thruster used to leave it roaring under the verdict. Arriving somewhere latches two notes quickly, middle C then the C above for taking hold of the station and the same pair reversed for letting go, two octaves lower for the ground. The second note is pending rather than played, since at an undocking the pilot is mid-burn and stopping the world for an eighth of a second would be felt as the controls sticking. Four channels for five sounds: the bang and the latch share one, because a lander arriving either arrives or does not and a crash ends the run outright. No instrument is set up once at startup: each sound loads its patch immediately before its note, because the bang and the latch share a channel and a channel used by two sounds has to be told which of them it is about to be. That rule arrived as a workaround for something worse - an LFO used to belong to the DEVICE and not to a channel, so whichever patch was loaded last owned both of them for every voice at once, which is what made the warning's trill vanish after the first landing of a run when the landing's patch carried an LFO switched off. An intermittent fault of the worst kind, right until something unrelated plays and right again next time the machine starts. Loading at the note fixed each sound as it STARTED and could not fix two of them overlapping, so the device was changed instead and an LFO now belongs to the channel it was written to. A crash also makes a noise, which was the first sound this game had, and it is a TRIGGERED voice - struck once and then playing its own length, so nothing has to come back and end it. That the program says so rather than the patch is deliberate: the patch decides what the bang sounds like and the program decides that it is a bang. It is noise through a low pass that the modulation envelope shuts as the level falls, so the bright part is only at the front and it is a boom rather than a hiss. The instrument is built at startup the way the tiles are, because a patch is twenty odd writes and a note is two - which is what the selector and value registers are for. It plays on channel THREE: effects count down from the top so that music, if it ever arrives, can take nought and count up and the two never have to negotiate. A crash takes the lander apart: it goes, and six pieces of it leave in a rough hexagon at its own colour for about a second before the verdict is said. It used to be a line of text and a lander still sitting there in one piece, so a watcher had to read the words to know what had happened. The world is not running while that plays - it is a loop of its own, so nothing else has to know how to be half destroyed. A plume hangs off whichever side the engine is pushing from - under the lander to lift, over it to retro, and on the far side from the way it is being pushed sideways, since that is where the gas leaves. Every other reading here is a number drawn as a bar and all of them say what is happening TO the lander, so without this a watcher has to read gauges to work out that a thruster is even lit. It is drawn while the BUTTON IS HELD rather than while the engine fires: the engine fires one frame in ten, because that is the tick gravity is applied on, and a flame that honest is a fault lamp rather than a rocket. An empty tank draws none, and neither does the retro thruster on the ground, because in both cases the button really is doing nothing. Down is a retro thruster at half the strength of up - one sixteenth a tick against two, which is exactly gravity's own step, so it can stop a climb and hurry a descent and can never turn a landing approach into a crash faster than letting go would. Arresting a rise otherwise meant a sideways burn and a wait for the orbit to come round, which is how a rendezvous really is flown and a lot to ask of somebody who has not flown one. It does nothing at all to a lander on the ground, which is not politeness: touchdown has already had its say and will not speak again, so without that guard a landed lander holding it goes straight through the moon. |
|
|
| Depth | Four pillars at four distances and a ball walking past them, behind the near ones and in front of the far ones. The ball is sprite nought and every pillar is numbered after it, so table order puts it in front of all four - what actually decides is the depth buffer, asked a column at a time. |
|
|
| Flip | Draws a whole screen into the bank that is not being shown, waits, and then shows it in one byte out of one port. It writes nothing else at all - not a tile, not a colour - so it does not ask for the screen to be saved, and the line it printed is still there when it comes back. It deliberately does not put the displayed screen back either, because that is the system's to restore: a program that faulted while flipped could not have. |
|
|
| Edit | A line editor. |
|
|
| Stream | Reads an 84,000 byte file through a buffer of 256, which is what says a file bigger than Data Memory can be read at all. |
|
|
| Type | Prints a named text file a block at a time, including one too large to fit in Data Memory. |
|
|
| Pour | Writes a file a block at a time, never holding more than one block of it. Each block is filled with a byte naming itself, so a block written to the wrong place shows up as content rather than as a length. |
|
|
| Copy | Copies one path to another a block at a time, including an empty file or one larger than Data Memory. |
|
|
| Compare | Compares two files a block at a time, stopping at their real tails rather than comparing unused bytes in the final disk blocks. |
|
|
| Wander | Goes to the directory it is given and reads a file there by a bare name. The only thing that moves the machine from inside a program, and so the only thing that can check the shell puts the working directory back afterwards. |
|
|
| More | A forward-only pager. Space advances a screen, Return one line, and q stops. A screen is as many lines as the screen has, asked of the rows register rather than assumed - 22 on the forty column mode and 47 on the eighty. |
|
|
| Press | Says what the console handed it, in hexadecimal and by name. It reads a line and then keys, because the keys that are not characters are dropped in line mode and delivered in key mode, and both halves of that rule want showing. |
|
|
| Crash | Breaks on purpose, in whichever of the four ways the system now catches, so that a fault screen can be looked at without having written a bug first. |
|
|
| Mode | Forty columns or eighty, whichever the screen is not in. Ten instructions and no data at all, which is the point of it: it is the smallest shape a loadable program can take, and the loader used to stop the machine dead on one. |
|
|
|
|
### When Something Goes Wrong:
|
|
|
|
A fault used to stop the machine and print a line to whatever was behind it. On a terminal
|
|
that is a diagnosis; behind a window it is a frozen picture and no reason at all, because the
|
|
message went to a standard error nobody was looking at. **The machine looked hung and was
|
|
not** - it had stopped, and said so somewhere invisible.
|
|
|
|
CosmOS catches all five faults the machine can raise and says what happened on the screen:
|
|
|
|
```
|
|
> Crash opcode
|
|
that byte is not an instruction, at 404E
|
|
A 00 B 0F Q 00
|
|
the program was stopped
|
|
>
|
|
```
|
|
|
|
**A fault ends the program, not the machine.** That is not a compromise. A bare `RETI` from
|
|
most faults meets the very instruction that failed and fails again, so carrying on is not on
|
|
offer - but the machine is almost never what is broken. Everything the shell puts back when a
|
|
program exits, which is the Stack, any vectors it installed, the drive, the working directory,
|
|
the console, the screen and the sound, is exactly what wants putting back after one dies. So you are
|
|
returned to the prompt, and the program is recorded as having stopped rather than finished.
|
|
|
|
A fault *below* where programs load is the system's own code, and there is nothing to go back
|
|
to. That one says so and stops.
|
|
|
|
**The glyphs and the colours come back too.** A program is as free to redefine a letter as
|
|
any other tile, so one that did has left the shell unable to spell, and one that wrote its
|
|
own palette used to hand back green text on blue. Both were permanent until the video device
|
|
grew a character generator to ask. The shell asks for both whenever a program exits, before
|
|
restoring a saved screen, so a program that saved one still gets back what was actually on
|
|
it and a program that saved nothing at least leaves something readable.
|
|
|
|
**The screen is put back into a mode text can be seen in first**, and that is the part that
|
|
matters most rather than the part that is prettiest. A program that faulted in bitmap mode
|
|
left the console with no text rows at all, so it draws nothing - the message about what went
|
|
wrong would be perfectly correct and completely invisible. Two palette entries are rewritten
|
|
for the same reason, since a program that wrote its own colours can leave every ink the same
|
|
as every paper. Only the two the message needs are touched; the rest of what the program
|
|
chose is left alone.
|
|
|
|
The address is where it happened, and it is exact. For a missing service or a device with
|
|
nobody listening it is the address *after* the instruction, because those two are the faults
|
|
where the instruction did dispatch and it was the entry that was empty.
|
|
|
|
### The Monitor:
|
|
|
|
The monitor is **part of the shell**, not a program the shell loads, and that is the whole reason it works. A loaded program occupies the one place a loaded program goes, so a monitor that was an application could never look at any other application: loading the thing you wanted to inspect would replace the thing doing the inspecting.
|
|
|
|
`monitor` turns it on and the prompt changes from `>` to `*`. It is **a mode, not a detour** - the shell's own commands still work, and the mode persists until you say otherwise:
|
|
|
|
```
|
|
> load Snake.sbx
|
|
> monitor
|
|
* d 2000
|
|
2000 47 00 11 00 SETD.0 1100
|
|
* b data
|
|
bank 01
|
|
* x 1000
|
|
* exit
|
|
>
|
|
```
|
|
|
|
**A program giving the machine back lands at the prompt it was started from**, so `g` into something, letting it run, and having it exit puts you back at `*` rather than at the shell. That falls out of the mode being a variable the prompt reads rather than a second loop: every way back to the prompt goes through one place, including `osExit`. Looking at a program and running it therefore do not interrupt each other, which is the thing a monitor is for.
|
|
|
|
`exit` leaves whatever you are in - the monitor if you are in it, the machine if you are not.
|
|
|
|
| | |
|
|
| --- | --- |
|
|
| `x [addr]` | Sixty-four bytes, as hex and as characters |
|
|
| `d [addr]` | Eight instructions, disassembled |
|
|
| `a addr` | Assemble instructions, until a line that is just a dot |
|
|
| `s addr b b ...` | Put those bytes there |
|
|
| `b program\|data\|n` | Which bank to look at |
|
|
| `g addr` | Go there |
|
|
|
|
`a` writes the assembler's own syntax: a selector rides on the mnemonic as `LDA.0` or `LDD.0.1`, and leaving one off means Data Pointer 0 exactly as it does in a source file, so nothing learned at the monitor has to be unlearned when writing a program. Case does not matter, and the whole line is refused before anything is written, so a mistyped instruction leaves no half of itself behind.
|
|
|
|
```
|
|
* b data
|
|
* s 8100 68 65 6C 6C 6F 2C 20 74 79 70 65 64 0A 00
|
|
* b program
|
|
* a 8200
|
|
8200: SETD.0 8100
|
|
8204: SWI 10
|
|
8206: SWI 12
|
|
8208: .
|
|
* g 8200
|
|
hello, typed
|
|
```
|
|
|
|
A program and its data, both entered by hand, calling a system service and returning to the prompt they were written at. Note the two banks: instructions go into Program Memory and the string into Data Memory, because that is what a Harvard machine means and the monitor will not guess for you.
|
|
|
|
**Numbers here are hexadecimal and bare.** A source file writes `0x2000` or `0d16` because it has both and must say which; the monitor has one and says so once.
|
|
|
|
**What cannot be written is a label**, and that is the whole difference between this and the assembler proper. A label is a promise to fill an address in later, and later is what a line at a time does not have. It is also why the same instruction table serves both directions here: what `a` writes, `d` reads back, and neither can drift from the other or from the assembler they were generated from.
|
|
|
|
`x` and `d` share one cursor and each leaves it past what it showed, so without an address either carries on - reading through memory is one letter at a time, and you can switch between bytes and instructions without retyping where you are. `s` deliberately does not move it.
|
|
|
|
Everything else here does something; the monitor looks at what the others did. It shows memory as hex and as characters, disassembles it, writes bytes into it, and jumps to an address - all through the memory controller, which is the only thing that can reach Program Memory.
|
|
|
|
That is why a monitor is worth more on this machine than on most. Data Memory a program can already read for itself with a Data Pointer. The half it cannot see is Program Memory, and that is the half its bugs are in.
|
|
|
|
**Its instruction table is generated from the assembler's**, by `Tests/instructiontable.py`, and checked against it by `Tests/docs.sh` - along with a second check that the lengths that table implies are the ones the manual's own Bytes column prints. Both matter for the same reason: a disassembler that disagreed about how long an instruction is would not print one line wrong, it would lose its place and print everything after it wrong. Which is what a disassembler does anyway when it starts in the middle of an instruction, and is worth seeing once so it is recognised later.
|
|
|
|
**Where to put something you typed in yourself** is a question the monitor answers, because the answer moves every time the monitor is rebuilt. `m` says where its own two segments end, and those are the first free addresses:
|
|
|
|
```
|
|
> m
|
|
code from 2000, free from 2607
|
|
data from 1000, free from 1367 up to the stack
|
|
```
|
|
|
|
Which is what makes the monitor's real trick possible - a program that no assembler ever saw:
|
|
|
|
```
|
|
> s 8000 26 48 D1 00 26 49 D1 00 26 0A D1 00 18 12
|
|
> d 8000
|
|
8000 26 48 INIA 48
|
|
8002 D1 00 OUTA 00
|
|
8004 26 49 INIA 49
|
|
...
|
|
800C 18 12 SWI 12
|
|
> g 8000
|
|
HI
|
|
```
|
|
|
|
Typed in as bytes, checked by disassembling it back, and run. It ends with `SWI osExit`, which is how it gives the machine to the shell rather than to nothing.
|
|
|
|
`g` does not come back: what it runs has to give the machine to the shell itself, which `SWI osExit` is how a program does. Breakpoints are not the monitor's - they are `SWI osBreak`, written into the program rather than poked over it, and described under Stopping To Look.
|
|
|
|
### The Editor:
|
|
|
|
`Edit` is the first program on this machine that makes a file a person typed - every byte on every disk before it was put there by the host tool. It is line oriented in the manner of `ed`: `l` lists, `a` adds at the end, `i` and `c` and `d` take a line number, `w` writes and `q` stops.
|
|
|
|
It includes nothing but `services.asm` and `text.asm`: the filesystem and the console are the system's, asked for rather than carried. That is what brought `Edit` down from 4,941 bytes to 2,243 bytes without a line of its own logic changing - and the way that was checked is worth knowing, because the recorded output of the `cosmosEdit` test did not move by a single byte across the rewrite.
|
|
|
|
**A line it reads in is at most 128 characters**, the same length a line is everywhere else
|
|
on this machine, and a file with a longer one is refused rather than opened. Refused rather
|
|
than shortened, because this is an editor: a line cut on the way in would be written back
|
|
cut, and the file damaged by having been looked at.
|
|
|
|
That limit was not there at all until a source file found it. The buffer is followed in
|
|
memory by the head of the document and the pointer the line allocator hands out, so a 94
|
|
character line wrote characters over both - a 31 line file opened as 3, and opening it a
|
|
second time walked a list that led back into itself for ever, with the emulator still
|
|
running and the machine never answering again. Typing a long line was always safe, because
|
|
`osReadLine` is told how much room there is; only the file being read went unchecked, which
|
|
is why a new document behaved and a source file did not.
|
|
|
|
It keeps the document as a **linked list of lines** rather than one buffer with newlines in it. Each line says where the next one is, how long it is, and then its bytes. Inserting is two pointers changed and nothing moved; with a flat buffer it would mean shifting every byte after the edit, on a machine whose only block move is a device asked politely. The price is that deleted lines are not reused, so a heavy session uses more room than the document needs and writing it out is what tidies up.
|
|
|
|
Saving goes through `sbfsSaveFile`, so a document that has grown is written somewhere else and the original is only let go of once the new one is safely down. That is the whole reason the editor was written: not because the machine needed an editor, but because every tool that produces a file needs the same four operations, and building them for one imaginary tool is how they end up wrong.
|
|
|
|
These are ordinary SBEX files on SBFS. None of them is built into the operating system, and
|
|
a disk can be filled from either side: the host tool puts files on, and so does the machine,
|
|
which assembles its own now.
|
|
|
|
### A Clean Install:
|
|
|
|
`make disk` lays down a disk the machine can start itself from, and
|
|
`make run-cosmos` starts it - with no boot image named, so the emulator shadows its ROM and
|
|
reads the disk for everything else.
|
|
|
|
```
|
|
/ Apps Packages Source Lib System
|
|
/Apps what you run, and the second place the shell looks for a word
|
|
/Packages what applications own, one app.<name> directory each
|
|
/Source what you assemble, including stage1.asm and stage2.asm
|
|
/Lib what those include
|
|
/System/Boot cosmos.bin, and the slots the loader lives in
|
|
```
|
|
|
|
**The loader's own source is on the disk**, which means the machine can rebuild what starts
|
|
it: `Asm stage2.asm` produces the bytes that go in a boot slot, and everything stage two
|
|
includes is already in `/Lib`. Stage one is the exception and always will be - it is the
|
|
ROM, and the one part of this a disk cannot replace.
|
|
|
|
No `boot.cfg` is written. Stage two falls back to `/System/Boot/cosmos.bin` when there is
|
|
none, and a clean install having nothing to configure is the right default.
|
|
|
|
`make run-cosmos-direct` hands the system over the old way instead, memory placed from
|
|
outside with nothing on the disk consulted. That is what a debugger does, and it is what to
|
|
use when the thing being debugged is the boot chain, since it skips the boot chain.
|
|
|
|
### Grid, and what a tile engine costs:
|
|
|
|
Everything else drawn on this machine has been text or a bitmap. `Grid` is the first program
|
|
to use the tile engine as an engine, and it is worth reading for the size of the numbers.
|
|
|
|
It scrolls **a pixel a frame, diagonally**, and the whole of that is four port writes and two
|
|
carries. The map is 128 rows and 128 columns against a screen of 50 and 80, so the cells
|
|
around the edge are already drawn - scrolling moves the origin rather than 2,000 bytes of
|
|
screen, and what leaves the top has not gone anywhere.
|
|
|
|
It fills **all 128 columns of every map row**, not the eighty the screen shows. That is the
|
|
distinction a scrolling program has to make: asking the screen how wide it is - which there is
|
|
a register for - gives you the window, and a program that scrolls wants everything the window
|
|
can be moved over. Filling only the window leaves 48 empty columns, and scrolling sideways
|
|
walks into them.
|
|
|
|
The coarse registers move a whole cell and the fine ones move the remainder, and **they do not
|
|
carry into each other**, so the program does:
|
|
|
|
```asm
|
|
SETD.0 FineDown
|
|
LDA.0
|
|
INCA
|
|
INIB 0x07
|
|
AND
|
|
STQ.0
|
|
OUTQ 0x38
|
|
BNQ stepAcross ; Still inside the cell.
|
|
; ... and here, one step of the row origin.
|
|
```
|
|
|
|
The `AND` is both the wrap and the test: `Q` coming out as nought is exactly the moment the
|
|
cell boundary was crossed. It moved eight pixels every fourth frame before the fine registers
|
|
existed, which reads as the picture jumping rather than travelling.
|
|
|
|
It puts its tile at **200**, because the machine wakes with the font in tile memory - glyph n
|
|
at tile n, for 135 of the 256 - so a program that starts at zero paints over the alphabet and
|
|
the shell it is about to hand the machine back to. Above 135 is empty and nobody else's.
|
|
|
|
Its sixteen colour schemes are **one tile**, not sixteen. A cell's attribute nibble is added
|
|
to every palette index in it, sixteen at a time, so the same 64 bytes come out in sixteen
|
|
colourings and the map bands down the screen as it scrolls.
|
|
|
|
**What it cannot give back is the palette.** The console's colours are sixteen banks at
|
|
exactly the entries the attribute nibble lands on, so any program using the nibble overwrites
|
|
them and there is nowhere else for it to write. `Grid` restores bank 0 - grey on black, what
|
|
plain text has always been - and leaves the other fifteen as it made them. The proper answer
|
|
is a command to the screen meaning "give me back what you woke up with", the way the console
|
|
has one for clearing. There is not one yet, and this is the first program that ever wanted it.
|
|
|
|
## The Application Model:
|
|
|
|
CosmOS divides the two SplitBit address spaces by convention:
|
|
|
|
| Memory | CosmOS | Loaded application |
|
|
| -- | -- | -- |
|
|
| Program Memory | `0x0000` through `0x4FFF` | `0x5000` and above |
|
|
| Data Memory | `0x0000` through `0x2FFF` | `0x3000` and above |
|
|
|
|
CosmOS's halves have been enlarged twice: doubled once when it outgrew the first ones, and
|
|
given a page each when the shell learned to edit and remember a line. **The division is a
|
|
convention and nothing enforced it**, so CosmOS quietly grew past `0x1FFF` and the next
|
|
program loaded landed on top of its own code - which does not fail where it happens, it
|
|
fails later, in whatever part of the shell the program happened to cover. `make test` now
|
|
measures both segments against the numbers in this table, so the table is checked rather
|
|
than merely written down.
|
|
|
|
**A page is a cheap thing to give it and an expensive thing to run out of.** An application
|
|
still has 44K of Program Memory before the vector table, and the largest one here uses 7.5K,
|
|
so the space taken from applications is space nothing has ever asked for - while the space
|
|
given to the system is the difference between building the next thing and counting bytes
|
|
while building it. Moving the wall costs a `#Base` line in each application and a rebuild;
|
|
that is the whole of it, because nothing in the machine knows where the wall is.
|
|
|
|
**The table is checked against itself as well.** The first version of that check read only
|
|
the CosmOS column, and so it passed a table whose Data row gave the system `0x3FFF` and an
|
|
application `0x2000` - two columns that cannot both be true, sitting next to each other.
|
|
Measuring one number against the code and never against the number beside it is how a
|
|
specification contradicts itself in public.
|
|
|
|
Applications state their actual Program and Data addresses with `#Base`. The SplitBit
|
|
assembler then writes an SBEX loadable image containing those addresses, the entry point,
|
|
the segment lengths, and any vectors the application needs. CosmOS does not relocate
|
|
code: the addresses in the file must be the addresses for which it was assembled.
|
|
|
|
This division is an ABI convention rather than protection. An application owns the
|
|
machine while it runs and may address hardware or CosmOS memory directly. The convention
|
|
keeps independently assembled software out of the system's way; it is not a security
|
|
boundary.
|
|
|
|
### System Services:
|
|
|
|
Applications include `Source/services.asm` to obtain stable names and vector numbers for
|
|
the services CosmOS provides. Neither side ever types a number: the file both of them
|
|
include is the only place any of them is written down. What each service is and what it
|
|
answers in is set out under "What A Program May Ask The System For".
|
|
|
|
The largest application CosmOS has is the assembler in `Programs/CosmOS/Assembler/`. It
|
|
travels with CosmOS rather than with the emulator, for the same reason the C assembler
|
|
travels with the emulator: it is part of the system it was written for.
|
|
|
|
A minimal CosmOS application therefore looks like this:
|
|
|
|
```asm
|
|
#Include services.asm
|
|
|
|
#Program
|
|
#Base 0x5000
|
|
|
|
start:
|
|
SETD.0 Message
|
|
SWI osPrintString
|
|
SWI osExit
|
|
|
|
#Data
|
|
#Base 0x3000
|
|
|
|
Message:
|
|
"Hello from CosmOS."
|
|
```
|
|
|
|
An application may also include its own libraries or access hardware ports directly.
|
|
The services are an interface offered by the system, not the only way software is allowed
|
|
to use the computer.
|
|
|
|
### Several Disks:
|
|
|
|
The machine has four drives behind one controller, and `drive` says which one the shell is
|
|
standing on. `drive 1` goes to another, and **the working directory goes with it** - where you
|
|
are on a disk is part of which disk you are on, not something the shell keeps on the side. Go
|
|
back and you are where you were.
|
|
|
|
Every drive is mounted at boot: the controller says how many are plugged in and each is tried
|
|
in turn. A drive with nothing in it, or a disk this cannot read, is left unmounted rather than
|
|
stopping the others, so a machine with a good disk in drive 0 and a blank in drive 1 starts
|
|
normally. `drive 1` then says there is nothing readable there, which is a different answer
|
|
from there being no such drive.
|
|
|
|
**What a mounted disk is, is eight bytes**: where its directory starts, how many blocks it is,
|
|
how big the disk is, and where you are on it. They sit together in the data segment on purpose,
|
|
because changing drives is one copy out and one copy in - and the other three thousand lines
|
|
of filesystem go on reading the same four names they always have and never learn that more
|
|
than one disk exists. That is the whole reason this was affordable.
|
|
|
|
The version is not among them. It is checked at mount and thrown away, because a version one
|
|
disk's zero parent already reads as "in the root", which is where all of its files are.
|
|
|
|
**A path may name a drive**, as a digit and a colon on the front: `1:/notes`, or `1:` on its
|
|
own for wherever that drive already was. It is handled where every path in the system arrives,
|
|
so it works for anything that takes one rather than for whichever commands somebody remembered.
|
|
|
|
**Naming a drive goes there and stays there.** Switching for the length of one command and
|
|
switching back reads better in a listing and cannot work: what a path resolves to is a start
|
|
block and a length, and those mean nothing without the drive they were read from. A `load`
|
|
that resolved on drive 1 and then read its blocks on drive 0 would read the right blocks of
|
|
the wrong disk.
|
|
|
|
**A name that begins with a digit is still a name**, because the colon is the whole of what
|
|
tells the two apart. `2things` is a directory; `2:` is a drive.
|
|
|
|
A drive the machine cannot read makes the whole path unfindable, and says so as `no such
|
|
file` - which it is, since there is nowhere for the rest of it to be.
|
|
|
|
**Copying between two disks is one command**: `Copy 1:/notes.txt 0:/keep.txt`. Every
|
|
`osFileBlock` names its path again and so goes back to the source drive; the write stream
|
|
remembers the drive it was opened on and returns there for each block. Between them the copy
|
|
walks back and forth without `Copy` itself knowing there is more than one disk.
|
|
|
|
### A Disk Of Your Own:
|
|
|
|
`make run-voyager` puts a second disk in **drive 1**, at `Disks/personal.img`, and a scratch
|
|
drive made of memory in **drive 2**. The scratch drive is what `osTakeScreen` writes to; drive
|
|
1 is yours and comes after nothing, so adding the scratch drive later did not renumber it. It is made the
|
|
first time it is needed and then left alone: never rebuilt, never cleaned, never committed.
|
|
|
|
It is **copied** before every start, though, and kept three starts back. One copy taken at
|
|
every start would be worse than none: a disk is lost when something goes wrong, and the next
|
|
thing anybody does is start the machine again to look, which is when a single backup gets
|
|
overwritten by the wreckage. Nor is the copy guarded by any check that the disk still looks
|
|
right, because no such check can be written - the disk that went missing here was a perfectly
|
|
valid and perfectly empty filesystem, because the format had succeeded.
|
|
|
|
That last part is the point. Everything else in this repository is made from source and can be
|
|
thrown away without losing anything - but a disk is where something *made on the machine*
|
|
lives, and a disk that `make clean` deletes is not a disk of your own. It sits outside
|
|
`build/` for exactly that reason, and `Disks/` is in `.gitignore`.
|
|
|
|
Delete it by hand if you ever want a fresh one.
|
|
|
|
### Disks Made Of Memory:
|
|
|
|
A drive the machine calls **volatile** loses everything when the machine stops. CosmOS formats
|
|
one it cannot read, because a drive whose contents do not survive never had anything to lose,
|
|
and mounts it like any other - so `--ram-disk 2048` gives you a working disk with nothing on
|
|
it, brought up before you reach a prompt.
|
|
|
|
**It leaves every other unreadable drive alone.** An unformatted floppy is not an invitation.
|
|
That distinction is the machine's to state and the system's to act on: the hardware says what
|
|
a drive *is*, and says nothing about filesystems, which is what leaves room for a system that
|
|
would rather have its own.
|
|
|
|
The size to format comes from the drive, not from a superblock - a superblock states a size
|
|
too, and that is no use on a disk which has not got one. The directory is sixteen blocks, 128
|
|
names, chosen rather than worked out: a scratch disk runs out of names long before it runs out
|
|
of room, and this machine cannot divide.
|
|
|
|
### Giving The Screen Back:
|
|
|
|
A program that takes the whole screen leaves the shell a blank one, and whatever was on it -
|
|
the listing you were reading, the error you were about to act on - is gone. There is nowhere
|
|
to put 48K of video memory on a machine with 64K of Data Memory that CosmOS is already living
|
|
in.
|
|
|
|
**A drive made of memory is somewhere.** `SWI osTakeScreen` says *"I am about to use the whole
|
|
screen, and would like what is on it now put back when I exit."* The system writes video
|
|
memory to a file on the scratch drive and restores it from `handleExit`, alongside the vectors
|
|
and console mode it already puts back. Q is zero if that was arranged; a machine with no
|
|
volatile drive says no, and **a program told no should carry on regardless**, because it was
|
|
going to before this existed.
|
|
|
|
**A program told no must cope.** There is no volatile drive on every machine, and a refusal is
|
|
not a fault - it means doing what the program would have done before there was anywhere to
|
|
save a screen. `Grid` clears up after itself when refused, which is the difference between a
|
|
clean prompt and a prompt printed into somebody's grid.
|
|
|
|
**The system always leaves the screen usable, refusal or not.** The fine scroll registers go
|
|
back to zero at every program exit, because the console draws in whole cells and a view three
|
|
pixels into one puts every character three pixels out for ever. That is true whether or not
|
|
the picture could be saved, so it is not part of the saving.
|
|
|
|
**It is not automatic, and that is the point.** Saving on every program start would be cheap
|
|
enough, but restoring on every exit would be wrong: `dir`, `Files` and `Say` print and stop,
|
|
and their output is the reason you ran them. A program that says nothing behaves exactly as
|
|
every program did before this existed.
|
|
|
|
**Tiles, map and palette all go** - 196 pages, and a block on the front holding the cursor,
|
|
the four scroll registers and the mode. The map's off-screen rows are the console's
|
|
scrollback; the tiles are the font, which a program that redefines one has overwritten; and
|
|
the palette is where the console's own colours live. `Grid` used to give back the map and not
|
|
the colours, and handed the shell green text on a blue ground.
|
|
|
|
It also replaced thirty lines of `Grid`: four scroll registers put back by hand, the map
|
|
filled with spaces, the cursor sent home, and palette bank 0 written out - all of which was
|
|
*still* wrong, because the other fifteen banks kept `Grid`'s colours and there was nowhere to
|
|
have kept the real ones.
|
|
|
|
### Where A Program Is Looked For:
|
|
|
|
Three places, tried in order:
|
|
|
|
1. Where you are.
|
|
2. `/Apps` on the disk you are on.
|
|
3. `/Apps` on drive 0.
|
|
|
|
The first makes a program you are working on the one that runs. The second makes `Snake` work
|
|
from any directory. **The third makes the system's programs work from a disk of your own** -
|
|
one with your files on it and no system - which is most of the point of having a second disk.
|
|
|
|
**Fetching a program does not move you, and neither does running one.** The drive is put back
|
|
after the load, because by then the program is in memory and the block numbers it came from
|
|
mean nothing; and put back again when the program exits, because a program that copies between
|
|
two disks moves the drive as its own paths need it to and being left wherever it finished is
|
|
not what anybody asked for. So `Copy 1:/a 0:/b` leaves you exactly where you were.
|
|
|
|
### Bank Numbers Are One Namespace:
|
|
|
|
A program that wants a device's memory registers it as a bank, and **bank numbers belong to
|
|
the whole machine**. Nothing hands them out and nothing refuses a number that is already
|
|
spoken for - registering one that is taken does not fail, it succeeds, and whatever held it
|
|
before quietly answers to nothing.
|
|
|
|
| Bank | Whose |
|
|
| --- | --- |
|
|
| 0 | Program Memory. The machine's. |
|
|
| 1 | Data Memory. The machine's. |
|
|
| 2 | The bank table. The machine's. |
|
|
| 3 | The disk's buffer, given at mount by `sbfsMount`. CosmOS needs it for as long as it is running. |
|
|
| 4 | The screen's atlas - its tiles and palette - while `osTakeScreen` is saving or restoring. |
|
|
| 5 | The screen's map, for the same. |
|
|
| 6 and up | Free for a program to use. |
|
|
|
|
**Tile pages 1 to 3 are not saved either**, and need not be: the shell's own text is drawn
|
|
from page 0, which is, and a tile left in another page is invisible unless a map cell names
|
|
that page - and the map is given back or cleared. What a program puts in page 1 is its own
|
|
and nobody is looking at it afterwards.
|
|
|
|
**The depth buffer is not touched at all.** It only matters to a sprite that has a depth, and
|
|
the table is cleared, so there are none - a program that wants one writes it, and a program
|
|
that leaves one behind has left something nobody is asking about.
|
|
|
|
**A window left up is taken down at exit**, for the same reasons as the sprites and by the
|
|
same rule. A window is a layer at a screen position that does not scroll, which is exactly
|
|
what makes one left behind unpleasant: it sits over the top rows of whatever comes next and
|
|
cannot be scrolled off, cleared away or typed past. Lunar Porter left its fuel gauge up once
|
|
and the shell came back with FUEL across the top and the cursor underneath it.
|
|
|
|
**The sprite table is cleared at exit, not saved and restored.** It lives in the atlas at
|
|
0xC000, and the pages the screen save walks run to the end of the map and pick up again at the
|
|
palette, with the sprites in the gap between. That is deliberate: nothing the shell draws is a
|
|
sprite, so there is nothing to give back. What is needed is to take away, or a program that
|
|
put something on the screen and left would have left it sitting over the prompt, in front of
|
|
everything, with nothing able to type it away. A program that faulted could not have cleared
|
|
its own, which is why the system does it for every program rather than trusting each one.
|
|
|
|
4 and 5 are only registered while a screen is being saved or given back, so a program is free
|
|
to point them somewhere else in between - but a program that takes the screen will find them
|
|
pointing at the screen again afterwards, so there is nothing to be gained by it. `Grid` uses
|
|
them for exactly what CosmOS uses them for. `Flip` wants a third bank, the screen nobody is
|
|
looking at, and takes 6.
|
|
|
|
`Grid` learned this the hard way and is the reason the table is here. It asked for 3, took the
|
|
disk's buffer, and every read the filesystem made afterwards came out of video memory - so the
|
|
shell found an empty disk and could not start anything by name, several commands after the
|
|
program that did it had exited. Nothing said a word, because from the controller's point of
|
|
view nothing went wrong.
|
|
|
|
**A program returns a bank by giving it back**, which today means knowing what was there
|
|
before. There is no service that hands out a free number, and if this becomes a common thing
|
|
for programs to want then that is what should exist rather than a longer table.
|
|
|
|
## What A Program May Ask The System For:
|
|
|
|
A loaded program is on its own hardware and can do anything the machine can do - it is a fence, not a wall. But the things it usually wants are things the system is already doing, and asking is both shorter and the only way to reach code that was assembled separately. `CALL` needs a label, and a label has to be in the same assembly; `SWI` needs only a number both sides agree on.
|
|
|
|
Those numbers are written down once, in `Programs/CosmOS/Source/services.asm`, which both the system and the program include. Neither side ever types a number.
|
|
|
|
| Service | Does |
|
|
| --- | --- |
|
|
| osPrintString | DP0 names a string ending in a zero byte. Prints it. |
|
|
| osReadLine | DP0 names somewhere to put a line, B says how much room there is. Reads one from the console, with the shell's own editing - arrows, Home, End, Delete - but not its history. Q comes back holding how long it was. The console is left in whatever mode it was found in. |
|
|
| osExit | Gives the machine back. Does not return. The system puts back what the program was borrowing - the Stack, its vectors, the drive, the working directory, the screen - and lets every voice of the sound device go, because a gate is a register only a program can drop and a program that has stopped cannot drop one. |
|
|
| osArgument | DP0 names somewhere to put whatever followed the run command, B says how much room there is. |
|
|
| osFileRead | DP0 names a file, DP1 says where to put it. Q is zero if it read, and DP3 comes back holding how many bytes there were. It writes WHOLE BLOCKS: a sixteen byte file still puts 256 bytes where it is told, so the room given has to be the length rounded up to the next 256. |
|
|
| osFileSave | DP0 names a file, DP1 is the bytes, A and B together are how many. Q is zero if it saved, whether or not it was there before. |
|
|
| osFileDelete | DP0 names a file. Q is zero if it went. |
|
|
| osFileRename | DP0 is the name a file has, DP1 the name it should have. Q is zero if it moved. |
|
|
| osFileInfo | DP0 names a file. Q is zero if it is there, and DP3 comes back holding how many blocks it occupies. |
|
|
| osFileBlock | DP0 names a file, DP1 says where to put a block of it, A and B together are which block counting from zero. Q is zero if it read, and DP3 comes back holding how many of the block's bytes belong to the file. |
|
|
| osChangeDir | DP0 names a directory. Q is zero if the machine is now in it. What a program changes here, the shell puts back when the program stops. |
|
|
| osFileStart | DP0 names a file, DP3 is how many whole blocks and A is the bytes left over in the last one. Q is zero if a write is now open. Nothing already on the disk is touched. |
|
|
| osFileWrite | DP1 is a block, A and B together are which block of the file it is, counting from zero. Q is zero if it was written. An index past the end of the file is refused. |
|
|
| osFileDone | DP3 is how many whole blocks it came to and A the bytes left over. The old file goes and what was written takes its name, at that size. Q is zero if it was committed. |
|
|
| osFileFetch | DP1 is where a block should go, A and B together are which block. Reads back a block of the file being written. |
|
|
| osPrintNumber | A and B together are a number. Prints it in decimal, without leading zeroes. |
|
|
| osBreak | Stops the program, shows every register as it had them, waits for a key, and carries on. |
|
|
| osLastStatus | Q answers what the last program exited with: 0 it did what it was asked, 1 it did not, 2 it was asked wrongly. A program may give its own meanings if it says so. |
|
|
| osTakeScreen | Says this program is about to use the whole screen and would like what is on it put back when it exits. Q is zero if that was arranged; anything else means it was not, which is the ordinary answer on a machine with no volatile drive. See Giving The Screen Back. |
|
|
| osWhereAmI | DP0 names somewhere to put the path the program was loaded from, B says how much room there is. See Where A Program Came From. |
|
|
| osDirFirst | DP0 names somewhere to put an entry's name, B says how much room there is. Q says what it is: 0 a file, 1 a directory, 2 an unfinished save, 0xFF nothing more. Starts a walk of the working directory. See Seeing What Is On The Disk. |
|
|
| osDirNext | The same, for the entry after it. |
|
|
| osBootState | Q answers how the last start went: 0 settled, 1 trying, 2 fell back. A machine with no disk answers settled, because there is nothing there to be unsettled about. |
|
|
| osBootSettle | Puts it back to settled, which is how a machine that fell back is told the situation has changed. Q is zero if the disk took it. **Settling is the only write a program gets** - marking a start as trying or fallen back is the loader's business, and a service that let a program claim either would let it lie about something the loader cannot check. |
|
|
|
|
```
|
|
#Include services.asm
|
|
...
|
|
SETD.0 Message
|
|
SWI osPrintString
|
|
```
|
|
|
|
### Stopping To Look:
|
|
|
|
`SWI osBreak` is a breakpoint. It shows every register as the program had them, waits for a key, and returns as though nothing happened.
|
|
|
|
```
|
|
break at 200E
|
|
A 11 B 22 Q 00 status 00
|
|
DP0 1030 DP1 05EF DP2 039A DP3 2000 SP FFFF
|
|
press a key
|
|
```
|
|
|
|
Every value comes out of the interrupt frame rather than out of the registers, because by the time the handler runs the registers belong to the handler. The frame is what the program had and what RETI is about to give back, so what is shown is what will be resumed with. The address is two before where it resumes: the `SWI` and the vector it names.
|
|
|
|
**The Stack Pointer is the exception, because it is not in the frame** - the frame is *where* the Stack Pointer is. What the program had is fourteen bytes above the frame, that being what entering an interrupt puts down, so it is worked out rather than read. Breaking inside a subroutine shows it ten lower than breaking outside one, which is the size of a CALL frame and a quick way to see how deep you are.
|
|
|
|
The status byte is shown as a number and then as the bits that are up - `carry`, `fault`, `interrupts` - because a dump that makes you look the number up is only half a dump.
|
|
|
|
**Nothing is overwritten, and that is the whole of why it is simple.** A breakpoint poked into a running program has to replace an instruction, and putting that instruction back in order to continue is the same act as disarming the breakpoint. Firing a second time would mean stepping over the restored instruction and putting the breakpoint back behind it, and this machine has no way to step a single instruction. Two bytes of `SWI` cost a little space and fire for ever, because there was never anything to restore.
|
|
|
|
The price is that a breakpoint is part of the program. A build with breakpoints in it has different addresses from a build without - the same bargain every machine makes that has a break instruction.
|
|
|
|
### The Disk Without A Filesystem:
|
|
|
|
A program that wants a file does not need to know what a filesystem is. Before these existed it had to include the whole of `sbfs.asm` - two and a half kilobytes of a private copy of code the system already had running - and then mount a disk that was already mounted.
|
|
|
|
There is no service to mount one, and that is not an omission. The system mounts the disk before it reads its first prompt, and there is one disk with one buffer registered as one bank; a program mounting it again was only ever an artefact of owning a second copy of the library. That call disappears rather than moving.
|
|
|
|
Sizes fit the registers exactly, in both directions. A file that can be read into Data Memory is under 64K by definition, so its length is sixteen bits: coming back it is DP3, and going out it is A and B together. Neither direction needs a record in memory whose shape both sides have to agree on.
|
|
|
|
A file of 256 blocks or more is refused by `osFileRead` rather than partly read, because 64K will not fit in Data Memory and its length will not fit in the pointer that reports it. A length that lies would be worse than a file that will not open.
|
|
|
|
### Reading A File That Will Not Fit:
|
|
|
|
`osFileRead` writes in whole blocks, because a block is what a disk is read in. A sixteen
|
|
byte file still puts 256 bytes wherever it is pointed, and a caller that reserves exactly the
|
|
file's length writes over whatever follows it - which is a quiet corruption rather than a
|
|
refusal, and looks like a bug somewhere else entirely. `DP3` still reports the file's own
|
|
length; the bytes past it are the rest of the block. Reserve the length rounded up to the next
|
|
256, read into that, and copy the parts wanted where they are wanted.
|
|
|
|
`osFileRead` hands over a whole file, which settles the question for anything under 64K and settles nothing above it. CosmOS's own source is above it: the sources together are a hundred kilobytes and Data Memory is sixty four. A machine that assembles itself has to be able to read a file bigger than its memory, and this is what that stands on.
|
|
|
|
So there is a second way to ask. `osFileInfo` says how big something is and `osFileBlock` hands over one block of it, and between them a program reads a file of any size through a buffer of 256 bytes.
|
|
|
|
```
|
|
SETD.0 Name
|
|
SWI osFileInfo ; DP3 is how many blocks, Q is zero if it is there.
|
|
BNQ noSuchFile
|
|
|
|
readLoop:
|
|
SETD.0 Name
|
|
SETD.1 Block
|
|
SETD.2 Index
|
|
LDA.2
|
|
INCD.2
|
|
LDB.2 ; Which block, most significant first.
|
|
SWI osFileBlock
|
|
BNQ readDone ; Three when there are no more.
|
|
... ; DP3 is how many of its bytes are the file's.
|
|
```
|
|
|
|
**There is no open and no close.** Every call names the file and says which block it wants, so nothing is held between them: a program that stops halfway leaves nothing behind, and there is no handle to run out of. The system does remember where the last file it was asked about lives, so reading four hundred blocks searches the directory once rather than four hundred times - but that is a speed and not a promise, and a caller never has to know about it.
|
|
|
|
`osFileInfo` answers in **blocks rather than bytes**, and that is forced rather than chosen. A file on a sixteen megabyte disk can be twenty four bits long and a Data Pointer holds sixteen. Blocks fit; the bytes in the last one come back from `osFileBlock` when the reader gets there.
|
|
|
|
`osFileBlock` answers a count in DP3 rather than in a register for the same kind of reason: every block but a short last one holds a whole **256** bytes, and 256 does not fit in a byte. A count that reported a full block as zero would make every reader treat the end of a file as a special case.
|
|
|
|
**These two say why when the answer is no**, which the other services do not. Everywhere else the only useful thing to do about a failure is to give up, so one value is enough. These exist to be asked questions with, and the difference between the answers is the answer:
|
|
|
|
| Q | Means |
|
|
| --- | --- |
|
|
| 0 | it worked |
|
|
| 1 | there is no disk |
|
|
| 2 | there is no file of that name |
|
|
| 3 | that block is past the end of the file |
|
|
| 4 | the disk would not read it |
|
|
|
|
Running off the end is how a reader finds out it has finished, so it gets an answer of its own rather than being reported as a disk that failed.
|
|
|
|
`Programs/CosmOS/Apps/Stream.asm` reads an 84,000 byte file through a 256 byte buffer, then reads a small file both ways - whole with `osFileRead` and streamed - and checks that the two agree.
|
|
|
|
`Programs/CosmOS/Apps/Files.asm` does the whole round trip - write, read, report, rename, delete - in 675 bytes, and includes nothing but the service names.
|
|
|
|
`osArgument` is how a program is told what it is for. Everything written before it did the same thing however it was started, which is fine for a program that greets you and no use to one that edits a named document. What arrives is the whole rest of the line, spaces and all, rather than a list of words: what counts as an argument is the program's business, and handing over what was typed is the system's.
|
|
|
|
A handler is entered with the caller's registers exactly as they were, because an interrupt frame is pushed rather than cleared. That is why a service can be given a pointer in DP0 and a count in B without any of it being copied anywhere first.
|
|
|
|
## Where A Program Came From:
|
|
|
|
`SWI osWhereAmI` hands back the path the program was loaded from, on the same terms as
|
|
`osArgument`: DP0 says where to put it and B how much room there is.
|
|
|
|
```text
|
|
/work> Where
|
|
loaded from /Apps/Where.sbx
|
|
```
|
|
|
|
**The answer follows the program and not the person.** It is worked out once, before the
|
|
program is given the machine, and a program's own place cannot move under it afterwards.
|
|
Standing in `/work` and running the copy in `/Apps` is told `/Apps`, which is the whole point.
|
|
|
|
**Made absolute, because the path the search settled on may be a bare name.** A program found
|
|
where somebody was standing is named by the word that was typed, and a bare name means the
|
|
working directory - which a program is entitled to move out of. So a program asked later
|
|
where it lives would be told "beside wherever you are now", which is the one answer that is
|
|
certainly wrong.
|
|
|
|
**Why this and not a service that opens a file beside the program.** That service would need a
|
|
twin for every file operation there is: read, save, info, block, start, write, done, delete,
|
|
rename. One service handing back a path composes with all of them, and joining a name to a
|
|
place becomes a library instead - `/Lib/path.asm`, whose `pathBeside` takes a path and a name
|
|
and answers with the name in the same place.
|
|
|
|
```
|
|
SETD.0 MyPath
|
|
SETD.1 Given
|
|
SETD.2 Beside
|
|
INIB 0d192
|
|
CALL pathBeside
|
|
```
|
|
|
|
**And why not simply put the program in its own directory.** Because a program's *assets* are
|
|
relative to the program and its *arguments* are relative to the person, and the working
|
|
directory can only be one of them. Setting it to the program's own would mean `Play
|
|
mytune.tune`, typed by somebody in their own directory, looked in `Play`'s. The working
|
|
directory stays the person's; this is how a program finds its own things.
|
|
|
|
`Programs/CosmOS/Apps/Where.asm` is the whole of it in one program, and exists to be run
|
|
rather than read.
|
|
|
|
## Seeing What Is On The Disk:
|
|
|
|
Every other file service takes a name a program already knows - read it, save it, rename it,
|
|
delete it, ask how big it is. **None of them could find out what names there are.** `dir`
|
|
could list only because it lives in the shell and calls the filesystem directly, so nothing
|
|
loaded could list anything at all: a file manager, a backup and a package manager were each
|
|
unwritable for want of two services.
|
|
|
|
```
|
|
SETD.0 Name
|
|
INIB 0d24
|
|
SWI osDirFirst
|
|
...
|
|
SWI osDirNext
|
|
```
|
|
|
|
**Q answers the kind rather than a yes or no**, so one value says both whether there is an
|
|
entry and what it is: 0 a file, 1 a directory, 2 a save that stopped before it committed,
|
|
0xFF nothing more. A caller that only wants names tests for 0xFF and ignores the rest.
|
|
|
|
**The size is not in it.** A walk hands back a name, and a program that wants the size asks
|
|
`osFileInfo` about that name. The alternative is a record in memory whose shape both sides
|
|
have to agree on, which is exactly what these services went out of their way to avoid for
|
|
file sizes - and most callers want names and nothing else.
|
|
|
|
**One walk at a time, and the system holds where it has got to**, the same bargain
|
|
`osFileStart` makes for writing. A program that starts a second walk before finishing the
|
|
first gets the second; there is one position, not a handle per caller. The state is small, so
|
|
a program that stops in the middle leaves nothing behind to clean up.
|
|
|
|
`Programs/CosmOS/Apps/Walk.asm` is the whole of it in one program. It asks `osFileInfo` about
|
|
each entry **between two steps of the walk**, which is not decoration: where a walk has got to
|
|
and where the last file asked about lives are both the system's, and two things sharing one
|
|
position would show as a listing that stopped early or said a name twice.
|
|
|
|
## An Application With Things Of Its Own:
|
|
|
|
A simple program is one file and lives in `/Apps`. One with **assets** gets a directory of
|
|
its own and a **launcher** in `/Apps` under the bare name somebody types.
|
|
|
|
```text
|
|
/Apps/Say.sbx a simple program, unchanged
|
|
/Apps/Lander a launcher
|
|
/Packages/app.Lander/Lander.sbx the program
|
|
/Packages/app.Lander/splash.tune its tune
|
|
```
|
|
|
|
The launcher is a script, and starting it needs no new rule at all: the shell runs what it
|
|
reads, so `Say.sbx` begins `SBEX` and gets loaded while `/Apps/Lander` begins `#!` and gets
|
|
read as lines, and neither the shell nor the person has to know which kind of thing they
|
|
started. It says `#quiet`, because a launcher is machinery rather than narration.
|
|
|
|
**The indirection is the point.** Where the game physically lives stops being part of its
|
|
name, so an installer may put it on another disk and change one line. That is also why the
|
|
launcher names the drive: found in the system's place on drive 0 by somebody standing on a
|
|
disk of their own, its lines run on *their* disk, and a plain `/Packages/...` would look
|
|
there. Naming `0:` makes the game and its things be looked for where the game is.
|
|
|
|
**Not in `/Apps`, for two reasons.** A launcher and a directory of the same name cannot both
|
|
be there - SBFS refuses the second outright. And `/Apps` is the directory the shell walks for
|
|
every word it does not know and Tab walks for every first word, so doubling what is in it is
|
|
a cost on the path that runs most.
|
|
|
|
**The `app.` goes in front rather than behind**, and that is not decoration. Tab matches the
|
|
*start* of a name, so a prefix is a namespace and a suffix is a collision: with `Copy.app`
|
|
beside `Copy.sbx`, typing `Copy` and pressing Tab completes to the shared `Copy.` and hands
|
|
you a broken word. In front, `C` and Tab does not list them at all and `app.` and Tab is how
|
|
you ask for them.
|
|
|
|
**Assets and documents are told apart by where they are looked for.** Lander's tune lives
|
|
beside Lander and is found by asking where Lander is; `/lander.state`, a saved position,
|
|
stays at the root and is found where the *person* is standing. One belongs to the program and
|
|
the other to whoever ran it, which is the whole distinction and the reason the working
|
|
directory could not serve both.
|
|
|
|
## The Filesystem Library:
|
|
|
|
The disk knows blocks and nothing else, so a filesystem is software. Programs/CosmOS/Source/sbfs.asm is one.
|
|
|
|
| Routine | Does |
|
|
| --- | --- |
|
|
| sbfsMount | Registers the disk's buffer as bank 3, reads the superblock, and checks the disk is one of ours. Q is zero if it is. |
|
|
| sbfsFind | DP0 names a file, ending in a zero byte. Q is zero if it was found, and then SbfsFileStart, SbfsFileBlocks and SbfsFileTail describe it. |
|
|
| sbfsRead | Reads the file that was found into Data Memory at DP1. Q is zero if it worked. |
|
|
| sbfsFirst | Starts a walk through the directory. Q is zero if there is an entry, and then SbfsName holds its name and the SbfsFile fields describe it. |
|
|
| sbfsNext | Steps the walk to the next entry in use. Q is zero if there was one. |
|
|
| sbfsCreate | Makes a file. DP0 names it, and SbfsFileBlocks with SbfsFileTail say how big it is. Q is zero if it was made, and then SbfsFileStart says where it went. |
|
|
| sbfsWriteFile | Writes the file that was made, from Data Memory at DP1. |
|
|
| sbfsDelete | DP0 names a file. Frees its entry and its blocks. Q is zero if it went. |
|
|
| sbfsRename | DP0 is the name a file has, DP1 the name it should have. Q is zero if it was renamed. Refused if something already answers to the new name. |
|
|
| sbfsSaveFile | DP0 names the file, DP1 is the data, and SbfsFileBlocks with SbfsFileTail say how big it now is. Writes it whether or not it was there before, and whatever size it used to be. |
|
|
|
|
Finding a file and listing what is there are different jobs. sbfsFind searches for one name; sbfsFirst and sbfsNext walk the whole directory, stopping on each entry that is in use and stepping over the free ones. A walk keeps a directory block in SbfsBuffer between calls, so anything else that goes to the disk in the middle of one ends it: take what is wanted out of an entry before asking the disk for anything else.
|
|
|
|
A file's size is settled when it is made, because nothing can grow one afterwards. Files are laid down contiguously, so the block after a file usually belongs to somebody else. A program that does not know how much it will write has to guess high and accept the slack, or build its output elsewhere and make the file once the size is known.
|
|
|
|
### Writing A File Too Big To Hold:
|
|
|
|
`osFileSave` is handed a whole document at once, which is what a text editor has. A program
|
|
that produces its output a piece at a time - an assembler, say - would have to hold all of
|
|
it first, and the largest thing on this machine would then be limited by memory rather than
|
|
by the disk.
|
|
|
|
So there is the other half of the streaming pair. `osFileInfo` and `osFileBlock` read a
|
|
file a block at a time; `osFileStart`, `osFileWrite` and `osFileDone` write one.
|
|
|
|
```text
|
|
SETD.0 Name
|
|
SETD.3 0x00 0x06 ; six whole blocks
|
|
INIA 0d40 ; and forty bytes after them
|
|
SWI osFileStart
|
|
|
|
...for each block: DP1 the bytes, A and B which block...
|
|
SWI osFileWrite
|
|
|
|
SETD.3 0x00 0x06 ; and what it came to, which need not be
|
|
INIA 0d40 ; what was asked for
|
|
SWI osFileDone
|
|
```
|
|
|
|
**One write is open at a time, and the system holds it rather than the program.** Reading
|
|
needs no state - a name and an index are the whole question - but writing safely does,
|
|
because the new file has to exist before the old one is thrown away and something has to
|
|
remember which temporary belongs to which name. Keeping that here means the careful order
|
|
is written once instead of in every program that streams.
|
|
|
|
**Nothing already on the disk is touched until `osFileDone`.** The room for the whole file
|
|
is taken at the start, so a disk that cannot hold it says so while the old one is still
|
|
there. That is stronger than `osFileSave` can manage, where the size is only known once the
|
|
caller already has every byte in hand.
|
|
|
|
**The size asked for need not be the size it comes to.** Some sizes are not knowable until
|
|
the last byte is out - the assembler cannot say how many vectors a program installs until
|
|
it has resolved them, and by then the file it is writing into has to exist. So the room is
|
|
taken generously at the start, where running out costs nothing, and `osFileDone` is told
|
|
the truth. The blocks that were asked for and not used go back.
|
|
|
|
`osFileFetch` reads a block of the file back, which is what lets a program keep only one
|
|
block of it in hand. Anything producing two parts of a file at once - source that says
|
|
`#Program` and `#Data` in whatever order it likes - has to be able to put a block down, go
|
|
and write somewhere else, and pick it up again where it left off.
|
|
|
|
Two limits differ between the two. `osFileSave` is handed a byte count in two registers and
|
|
so cannot write more than 65,535 bytes; `osFileStart` is told blocks and a tail, the way an
|
|
entry holds a size, and reaches the whole disk. And `osFileWrite` refuses an index past the
|
|
end of the file - files are contiguous, so block nine of a three block file is a real block
|
|
belonging to something else, and writing it would put one file's bytes inside another with
|
|
nothing anywhere saying so.
|
|
|
|
### Reading Ahead:
|
|
|
|
A file is read front to back, so when a program asks for a block, the one after it is
|
|
almost certainly wanted next. `sbfsReadOne` asks the disk for it straight away and hands
|
|
the caller the block it wanted - so the transfer happens while the program is busy with
|
|
what it already has, and the wait is mostly gone by the time it comes back.
|
|
|
|
Nothing is done differently and nothing is done out of order. The machine simply stops
|
|
standing still.
|
|
|
|
**It is not done for directory searches, and that is not an oversight.** A scan stops the
|
|
moment it matches, so the next block is one nobody will ever look at: it costs a transfer to
|
|
fetch and another wait to throw away. Tried there, it was nineteen per cent *slower*. Read
|
|
ahead is a bet that the next block is wanted, and a search is exactly the case that hopes it
|
|
is not.
|
|
|
|
What it is worth, printing a fourteen kilobyte file:
|
|
|
|
| Cycles a block | Without | With |
|
|
| -- | -- | -- |
|
|
| 0 | 936,626 | 962,959 |
|
|
| 2,000 | 1,064,498 | 976,882 |
|
|
| 10,000 | 1,576,562 | 1,032,889 |
|
|
|
|
The second column barely moves. From an instant disk to a slow one the cost rises seven per
|
|
cent, where without it the same change costs sixty eight - which is the point: **a machine
|
|
that reads ahead stops caring very much how fast its disk is.** The three per cent it costs
|
|
at zero is the bookkeeping, paid when there is nothing to hide behind it.
|
|
|
|
### And Waiting For What Is Left:
|
|
|
|
Read ahead hides most of the wait and cannot hide all of it. What remained was a loop
|
|
asking the disk's status port over and over, which is work the machine is doing and memory
|
|
it is touching to find out that nothing has happened yet.
|
|
|
|
`sbfsWaitDisk` uses `WAIT` now. It tests the status port, and only if the disk is still
|
|
busy does it stop - the CPU is put down until a device raises a line, and the disk raises
|
|
one when it finishes. **Asking first is what makes it safe:** if the disk finished in the
|
|
gap between the test and the `WAIT`, its line is already standing and the `WAIT` does
|
|
nothing rather than sleeping through the answer.
|
|
|
|
There is no handler and no vector. The shell keeps the Interrupt Flag down, and a `WAIT`
|
|
wakes on a line whether or not anybody intends to answer it, taking it down on the way
|
|
past. Printing the same fourteen kilobyte file:
|
|
|
|
| Cycles a block | Total | Of that, the bus | Waiting |
|
|
| -- | -- | -- | -- |
|
|
| 0 | 922,570 | 922,570 | 0 |
|
|
| 2,000 | 946,474 | 922,702 | 23,772 |
|
|
| 10,000 | 1,042,474 | 922,702 | 119,772 |
|
|
|
|
**The middle column stops moving.** What the program costs in memory is now the same
|
|
whatever the disk does, and the difference is time spent with the bus quiet. On this
|
|
emulator that changes nothing anybody can see; on hardware it is the difference between a
|
|
CPU contending for memory with everything else and a CPU standing out of the way.
|
|
|
|
### Saving Something Twice:
|
|
|
|
Which is why saving a document is not the same as writing a file, and why sbfsSaveFile exists rather than each tool doing it. A file that has grown will usually not fit where it was, so saving it means putting it somewhere else and letting go of where it was - and **the obvious order is a trap**:
|
|
|
|
```
|
|
delete the old one
|
|
make a new one <- refused, and the old one is already gone
|
|
write it
|
|
```
|
|
|
|
A create can be refused for want of a run long enough even on a disk with plenty of free blocks, because free blocks are only useful to a contiguous file when they are next to each other. Done in that order, the first fragmented disk somebody meets eats their work. sbfsSaveFile does it the other way round:
|
|
|
|
```
|
|
make a temporary nothing is lost if there is nowhere to put it
|
|
write it
|
|
delete the original only now, once the new one is safely down
|
|
rename the temporary
|
|
```
|
|
|
|
**That is what renaming is for.** It looks like a convenience and it is the safety mechanism: it is the only one of the three operations that moves no data - a name lives in the directory entry, so renaming writes twenty two bytes into one block - which makes it the only one that can be left until last and relied on not to fail.
|
|
|
|
#### Exactly what that promises:
|
|
|
|
The ordering protects the original against **every way a save can fail while it is running**, and it is worth naming those, because they are the ones that actually happen: there is no run of free blocks long enough, or none at all; the disk refuses a block write; the name turns out to belong to a directory; the writer gives up part way through. In all of them the file that was already there is untouched, and what is lost is the temporary, which nothing had come to depend on yet.
|
|
|
|
It is **not** power-loss atomic, and nothing about SBFS claims it is. The commit is two block writes - delete the old entry, then give the temporary its name - and a machine that stops between them leaves the old file gone and the new one under the temporary's name. Both writes are to the directory, so `sbfs.part` or `sbfs.out` is sitting there holding every byte of the work; the data survives and the name does not, and putting it right is one `rename` typed by hand. `dir` marks it `<unfinished>` so that it can be found, which is the whole of the recovery this format offers.
|
|
|
|
Closing that window means a journal or a second copy of the directory, and both are a great deal of machinery to buy back a two-write gap on a machine with no power failures to speak of. The honest description is the one to write down: **safe against the failures of ordinary operation, not against the machine stopping.**
|
|
|
|
#### What tells a temporary from a file:
|
|
|
|
While the save runs the temporary is an ordinary entry in every way that matters - it holds real blocks and answers to a name - and the only thing that makes it different is that nobody has committed it yet. That is **not a property of its contents.** The same bytes become the finished file the instant the rename lands, so there is nothing to put inside it that would be true. It belongs in the entry, which is the thing the commit changes, and it is **flag bit `0x04`**.
|
|
|
|
It used to be told apart by being *called* `sbfs.part` or `sbfs.out`, and those are names anybody is entitled to give a file of their own. Starting a save deleted whatever answered to one, as stale scratch - so saving anything at all in a directory destroyed your file of that name there, silently, and the first you would know of it is going to look for it. A file that does not carry the flag now belongs to somebody, and the save is **refused** rather than helping itself to the name.
|
|
|
|
The same bit is what makes an interrupted save recoverable. Both listings show an unfinished write rather than sizing it, because the size in the entry is the room it asked for and not what was written into it:
|
|
|
|
```
|
|
> dir
|
|
stranded.txt <unfinished>
|
|
```
|
|
|
|
Rename it to keep the data, delete it to give the blocks back. Nothing reclaims it on its own; a boot-time consistency check could, and this is the field it would read.
|
|
|
|
**A committed file never carries the bit**, so a disk this writes is byte for byte the disk the older code wrote - the agreement tests compare whole images and say so. Only the wreckage of a save that stopped looks different, and code that has never heard of the flag reads that as an ordinary file, which is exactly what it did before.
|
|
|
|
Finding room is a walk through the directory rather than a lookup, because there is no allocation table. With files laid down contiguously the directory already says which blocks are spoken for, and a second copy of that would be a second thing to keep right. The free count in the superblock is kept up to date but it is a note rather than the truth: it can be worked out again from the directory, and the directory is the one to believe.
|
|
|
|
A file's length is its block count times 256 plus its tail, which is the same as putting the block count in the high byte and the tail in the low one. Nothing pads a file out, so the bytes after the end of one are whatever else happened to be in that block, and it is the reading program's business to stop where the tail says.
|
|
|
|
The other implementation of this format is SplitDisk, on the host. Nothing is shared between the two but the specification, so a change to either has to be a change to both.
|
|
|
|
### What A Program Made Of It:
|
|
|
|
`SWI osExit` takes a status **in A**: zero if the program did what it was asked, one if it
|
|
did not, two if it was asked wrongly. A program may give its own meanings if it says so, and
|
|
`Compare` does - one there means the files differ, which is a result rather than a failure.
|
|
|
|
**In A rather than Q**, which is not a departure from the rule that a service answers in Q.
|
|
This one *takes an argument*, the way `osPrintNumber` takes A and B, and it never returns to
|
|
answer anything. A is free precisely because a return would have put it back - and Q is the
|
|
ALU's output, so setting it to a small number costs four instructions where A costs one.
|
|
|
|
**The shell keeps the number and does not print it.** A program that failed has already said
|
|
so in words, and a number beside that would be noise. `osLastStatus` hands it back and
|
|
`Status` is the program that shows it. The indirection is the point: this number is for the
|
|
thing that cannot read words - whatever comes to run programs in sequence and has to decide
|
|
whether to run the next one.
|
|
|
|
Marking all fifty eight exits found a defect on its first run. `Type` and `More` printed why
|
|
they had failed and then **fell through into the success exit**, reporting that all was
|
|
well. Nobody had noticed, because while the only reader was a person, the person could see
|
|
both.
|
|
|
|
### How A Service Answers:
|
|
|
|
A handler arrives with the caller's registers pushed rather than cleared, and **`RETI`
|
|
restores every one of them** - which is what makes an interrupt safe to arrive at an
|
|
arbitrary moment, since the interrupted code cannot tell it happened. A service is not
|
|
arbitrary. It was asked for, and it has something to say.
|
|
|
|
It says it with `SRET`, which is `RET` adapted to an interrupt frame: **A, B and Data
|
|
Pointers 0 through 2 come back, the saved Q and Data Pointer 3 are dropped, and the
|
|
Interrupt Flag is put back from the frame.** So a service answers in exactly the registers
|
|
a subroutine answers in, and there is one rule on this machine rather than two.
|
|
|
|
Before it existed, a handler with an answer wrote into its own frame:
|
|
|
|
```asm
|
|
MVSD.2
|
|
DPUP.2 0d02 ; the saved Q, by an offset it had to know
|
|
STA.2
|
|
RETI
|
|
```
|
|
|
|
Thirty places did that, each knowing the frame's layout by heart, and all thirty would have
|
|
gone quietly wrong the day the frame gained a field. None of them knows it now.
|
|
|
|
`RETI` is still right for a **hardware** handler, which has nothing to say and must leave
|
|
no trace. The two returns are not a choice of style: one says *I was never here* and the
|
|
other says *here is your answer*.
|
|
|
|
### What A Subroutine Can And Cannot Hand Back:
|
|
|
|
This is the thing that catches people, including whoever wrote the last three pieces of system code, so it is worth stating once and plainly.
|
|
|
|
CALL saves **A, B, and Data Pointers 0, 1 and 2**, and RET puts all five back. So a subroutine cannot return anything in any of them: whatever it puts there is undone by its own return, silently, and the caller carries on with its old values as though the subroutine had never run.
|
|
|
|
What comes back is **Q**, which is one byte, and **Data Pointer 3**, which is two. That is the whole of it, and it is why DP3 is not preserved.
|
|
|
|
The same rule catches a loop that steps a pointer inside a subroutine. The step is thrown away every time round, so the loop reads the same byte forever and the fault is a wrong answer rather than a crash.
|
|
|
|
If two bytes have to come back and DP3 is spoken for, the honest answers are to write them into Data Memory, or to do the work in the caller rather than in a routine. A short sequence written out twice is better than a subroutine that quietly does nothing.
|
|
|
|
The same rule cuts the other way, which is easier to miss. Because DP3 is not put back, **a routine you call may leave something of its own in it**. It is where a routine hands a pointer out, so it is not a safe place to leave one of your own across a call to anything that might use it. The Stack is: push it before the call and pop it after, and it will be exactly as it was.
|
|
|
|
A label may only be defined once across a program and everything it includes, so a routine in one library cannot use a name that another has already taken.
|
|
|
|
## The Console Library:
|
|
|
|
Programs/CosmOS/Source/console.asm is the console library. It replaces print.asm, which was written for a machine with one Data Pointer and no vector table, and which is still there because the programs that include it still work.
|
|
|
|
| Routine | Does |
|
|
| --- | --- |
|
|
| newLine | Prints a line feed. |
|
|
| printString | DP0 names a string ending in a zero byte. Prints it. |
|
|
| printSpaces | A holds how many spaces to print. None is a fair answer, and prints nothing. |
|
|
| printByteHex | A holds a byte. Prints it as two hexadecimal digits. |
|
|
| printWordHex | DP0 names two bytes, most significant first. Prints them as four hexadecimal digits. |
|
|
| printHexDigit | A holds a nybble. Prints the one character that stands for it. |
|
|
| printDecimalDigit | A holds a digit from zero to nine. Prints it. |
|
|
| printByteDecimal | A holds a byte. Prints it in decimal, without leading zeroes. |
|
|
| printWordDecimal | DP0 names two bytes, most significant first. Prints them in decimal, without leading zeroes. |
|
|
| readLine | DP0 names a buffer and B says how many characters it holds. Reads a line into it. Q is how long the line turned out to be. |
|
|
|
|
Two things about it are different from the old library, and both are deliberate.
|
|
|
|
There is no branch at the top. print.asm begins with a BRI to a label called start, so that a program including it arrives at its own entry point rather than falling into the library. That was the only way to do it before the Vector Table existed, and it is why print.asm cannot be assembled on its own: the label it branches to is one only the including program defines. A program including console.asm says where it begins in its own Vector Segment instead, with a Boot line, and the library assembles by itself.
|
|
|
|
Every routine names the Data Pointer it works through rather than assuming there is only one. A pointer handed in is DP0, and nothing in the library disturbs DP3.
|
|
|
|
readLine cuts a line short if it is longer than the buffer, and then reads the rest of it and throws it away, so that what is left over does not turn up as the next line. ConsoleEndOfInput is set if the console ran out instead of ending a line, and it is cleared at the start of every call, so it always describes the last line read. That is a different thing from an empty line, and a program reading until there is no more has to be able to tell the two apart.
|
|
|
|
## Source Layout:
|
|
|
|
- `Source/cosmos.asm`: Boot process, shell, loader, monitor, system services, and
|
|
application lifecycle.
|
|
- `Source/console.asm`: Console input, strings, hexadecimal and decimal output, and line
|
|
handling.
|
|
- `Source/text.asm`: String comparison, splitting, and hexadecimal text conversion used
|
|
by the shell.
|
|
- `Source/sbfs.asm`: Target-side implementation of the SplitBit filesystem.
|
|
- `Source/services.asm`: The shared names and stable vector numbers used by CosmOS and
|
|
separately assembled applications.
|
|
- `Apps/`: Loadable programs packaged onto the CosmOS disk image.
|
|
|
|
## Tests:
|
|
|
|
CosmOS is exercised as part of the SplitBit repository's normal test suite:
|
|
|
|
```sh
|
|
make test
|
|
```
|
|
|
|
The tests boot the system with and without a disk and drive the shell through recorded
|
|
console input. They cover directory traversal, every loader refusal, repeated application
|
|
runs, memory inspection, vector installation and restoration, command arguments,
|
|
filesystem deletion and renaming, interactive applications, and editing a file followed
|
|
by reading the saved result back in a second editor session.
|
|
|
|
Individual CosmOS tests can be run from the repository root, for example:
|
|
|
|
```sh
|
|
./Tests/run.sh cosmos cosmosRun cosmosEdit
|
|
```
|
|
|
|
Test disks are constructed with the host-side `SplitDisk` tool. CosmOS is therefore
|
|
reading filesystems written by an independent implementation of the same format rather
|
|
than merely checking its filesystem code against itself.
|
|
|
|
## Current Scope:
|
|
|
|
CosmOS is early software for an experimental computer. It runs one application at a
|
|
time, has no privilege levels or process isolation, does not relocate applications, and
|
|
has no linker. Its purpose is to make SplitBit usable from inside the machine: inspect
|
|
it, manage persistent files, load programs, provide common services, and return reliably
|
|
to a command prompt.
|
|
|
|
**Self-hosting is done.** `Assembler/` reads source off a SplitBit disk and writes a boot
|
|
image or a loadable program back to it, byte for byte what the host assembler builds from
|
|
the same source. It assembles CosmOS, and it assembles itself, and the CosmOS it built
|
|
assembles CosmOS again to the same bytes. What is left of that milestone is a linker, and
|
|
editing source under CosmOS comfortably enough to want to: `Edit` is line oriented and
|
|
knows nothing about assembly.
|
|
|
|
## Additional Information:
|
|
|
|
The SplitBit Programming Manual describes the machine underneath: the CPU, the vector
|
|
table and interrupt model, devices, the memory controller, the console, and storage as a
|
|
block device. The SplitBit Assembler Manual documents the assembly language, the segment
|
|
bases and vector declarations, and the SBEX loadable program format.
|
|
|
|
What a program may ask CosmOS for is documented here rather than in either of those,
|
|
because the services are this system's and not the machine's.
|
|
|
|
## License:
|
|
|
|
CosmOS is part of the SplitBit Emulator project and is licensed under the Apache License,
|
|
Version 2.0. See the repository's top-level `LICENSE` file for the full license text.
|