CosmOS knows about all four drives

A mounted disk is EIGHT BYTES - where its directory starts, how many
blocks it is, how big the disk is, and where you are on it. They now sit
together in the data segment, and changing drives is one copy out and one
copy in. The other three thousand lines of filesystem go on reading the
same four names they always have and never learn there is more than one
disk, which is the whole reason this was affordable.

The version is not in the record. It is checked at mount and thrown away,
because a version one disk's zero parent already reads as "in the root".

Every drive is mounted at boot: the controller says how many are plugged
in and each is tried in turn. One 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.

'drive' says which one, '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. A drive the machine has not got is refused, and refused
differently from one that is there with nothing readable in it.

Three things the assembly caught me on, all the same misunderstanding of
what survives a call:

  - OR reads A and B, and the bit came back from sbfsDriveBit in Q, which
    RET does not disturb - but RET does put A back. The mounted mask never
    got set and drive 0 was reported unmountable.
  - MVQA then RSTA throws away the copy it just made, so doubling a bit
    doubled nothing. SHL does it in one instruction, because A and B are
    one register to it.
  - There is no move from A to B. INB reads a port straight into B, which
    is what the drive count comparison wanted.

run.sh takes more than one image now, separated by a plus, since the
machine has four drives and a test that could only name one could not
check any of this.

The buffer note is forgotten on a drive change and that is DELIBERATELY
kept although nothing can currently reach it: only the file read-ahead
consults it, a directory scan does not, and finding a file requires a
scan which overwrites the note on the way past. Two disks were built with
the same file at the same block to try to catch it and the answer was
right either way. Three instructions to hold an invariant rather than a
story about a bug - and the comment says so instead of claiming a fix.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
This commit is contained in:
Anachronaut
2026-08-31 12:30:09 -04:00
co-authored by Claude Opus 5
parent b1538e0618
commit 5644c24113
9 changed files with 435 additions and 10 deletions
+28
View File
@@ -234,6 +234,7 @@ CosmOS currently provides these built-in commands:
| `do <script>` | Run the lines in a file as though they had been typed. See Scripts. | | `do <script>` | Run the lines in a file as though they had been typed. See Scripts. |
| `echo [words]` | Say the rest of the line, or a blank line with nothing after it. | | `echo [words]` | Say the rest of the line, or a blank line with nothing after it. |
| `clear` | Empty the screen. | | `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. | | `cd [path]` | Go to a directory, or to the root with nothing after it. |
| `mkdir <path>` | Make a directory. | | `mkdir <path>` | Make a directory. |
| `rmdir <path>` | Remove one, if it is empty. | | `rmdir <path>` | Remove one, if it is empty. |
@@ -797,6 +798,33 @@ An application may also include its own libraries or access hardware ports direc
The services are an interface offered by the system, not the only way software is allowed The services are an interface offered by the system, not the only way software is allowed
to use the computer. 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.
**Paths do not name a drive yet.** `drive 1` then a path is how you reach the other disk, so
copying between two of them is not possible in one command. A prefix like `1:/notes` is the
obvious next thing and it touches the path walker, which is why it is not in the same change
as the record.
### Bank Numbers Are One Namespace: ### Bank Numbers Are One Namespace:
A program that wants a device's memory registers it as a bank, and **bank numbers belong to A program that wants a device's memory registers it as a bank, and **bank numbers belong to
+75 -1
View File
@@ -77,7 +77,7 @@ boot:
; Find out whether there is a filesystem to talk to. Doing this once at boot rather than ; Find out whether there is a filesystem to talk to. Doing this once at boot rather than
; once per command means a disk swapped underneath us is not noticed, which is honest ; once per command means a disk swapped underneath us is not noticed, which is honest
; for a machine whose disk is a file named on the command line. ; for a machine whose disk is a file named on the command line.
CALL sbfsMount CALL sbfsMountAll
SETD.0 DiskReady SETD.0 DiskReady
BNQ bootNoDisk BNQ bootNoDisk
INIA 0x01 INIA 0x01
@@ -259,6 +259,11 @@ promptWhere:
CALL textSame CALL textSame
BRQ doRename BRQ doRename
SETD.0 CommandLine
SETD.1 DriveName
CALL textSame
BRQ doDrive
SETD.0 CommandLine SETD.0 CommandLine
SETD.1 ClearName SETD.1 ClearName
CALL textSame CALL textSame
@@ -1658,6 +1663,67 @@ fileComplain:
CALL newLine CALL newLine
BRI commandFailed BRI commandFailed
; ---- drive ----
;
; Which disk the shell is standing on. With nothing after it, says which; with a number, goes
; there - and the working directory goes with it, because where you are on a disk is part of
; which disk you are on rather than something the shell keeps on the side.
doDrive:
SETD.1 TextRest
LDD.0.1
LDA.0
BRA driveSay
; One digit. Anything else is not a drive number, and the machines this is imitating never
; had ten drives either.
INIB 0d48
CCF
SUB
MVQA
INIB 0d10
CCF
SUB
BNC driveNoSuch ; Ten or more, so it was not a digit at all.
; Is there such a drive on this machine?
SETD.1 DriveWanted
STA.1
INB 0x25 ; How many drives, straight into B: there is no move from A to it.
LDA.1
CCF
SUB ; Borrows when the wanted one is inside the count.
BNC driveNoSuch
; And is there anything readable in it? A drive with no disk is a real drive and an empty
; one, so this is a different answer from "there is no such drive".
CALL sbfsDriveBit
MVQA
SETD.1 SbfsMounted
LDB.1
AND
BRQ driveNotReadable
SETD.1 DriveWanted
LDA.1
CALL sbfsUse
BRI prompt
driveSay:
INA 0x24
INIB 0d48
CCF
ADD
OUTQ 0x00
CALL newLine
BRI prompt
driveNoSuch:
SETD.0 DriveNoSuch
BRI fileComplain
driveNotReadable:
SETD.0 DriveNotReadable
BRI fileComplain
; ---- clear ---- ; ---- clear ----
; ;
; The console has done this since before there was a screen to do it on: writing 1 to the ; The console has done this since before there was a screen to do it on: writing 1 to the
@@ -3703,6 +3769,8 @@ PromptText:
; not what went wrong, and what went wrong has already been said in words. ; not what went wrong, and what went wrong has already been said in words.
LineFailed: LineFailed:
0x00 0x00
DriveWanted:
0x00
LastStatus: LastStatus:
@@ -3721,6 +3789,10 @@ ScriptNotOne:
"do: that is not a script - it wants #! on the first line" "do: that is not a script - it wants #! on the first line"
ScriptTooDeep: ScriptTooDeep:
"do: scripts are only four deep" "do: scripts are only four deep"
DriveNoSuch:
"drive: this machine has no such drive"
DriveNotReadable:
"drive: nothing this can read is in that drive"
StartupName: StartupName:
"/System/Boot/startup.sh" "/System/Boot/startup.sh"
StartupNotOne: StartupNotOne:
@@ -3916,6 +3988,8 @@ EchoName:
"echo" "echo"
ClearName: ClearName:
"clear" "clear"
DriveName:
"drive"
LoadName: LoadName:
"load" "load"
RunName: RunName:
+260 -4
View File
@@ -136,6 +136,232 @@ sbfsMountTooBig:
ADD ; Q is not zero: not a disk this will mount. ADD ; Q is not zero: not a disk this will mount.
RET RET
; ---- Every drive the machine has ----
;
; Asked for rather than assumed: the controller says how many are plugged in, and each is
; selected and mounted in turn. A drive with nothing in it, or a disk this cannot read, is
; left unmounted rather than stopping the others - a machine with a good disk in drive 0 and
; a blank in drive 1 should start.
;
; Q is zero if drive 0 mounted, because that is the one the system came off and the one the
; shell will be standing in when it gets a prompt.
sbfsMountAll:
RSTA
SETD.1 SbfsMounted
STA.1
SETD.1 SbfsDrive
STA.1
OUTA 0x24 ; Drive 0, whatever the controller was left on.
; The live record belongs to nobody yet, so every slot starts empty and a drive that fails
; to mount keeps an empty one.
SETD.0 SbfsMountTable
INIB 0d32
sbfsMountClear:
RSTA
STA.0
INCD.0
DECB
BNB sbfsMountClear
INA 0x25
SETD.1 SbfsDriveCount
STA.1
RSTA
SETD.1 SbfsDriveAt
STA.1
sbfsMountEach:
SETD.1 SbfsDriveAt
LDA.1
SETD.1 SbfsDriveCount
LDB.1
CCF
SUB
BRQ sbfsMountAllDone ; Past the last one.
; Selected directly rather than through sbfsUse: there is nothing in the live record worth
; putting back yet, and sbfsUse would copy eight bytes of nothing into a slot.
SETD.1 SbfsDriveAt
LDA.1
OUTA 0x24
SETD.1 SbfsDrive
STA.1
RSTA
SETD.1 SbfsBufferKnown
STA.1
CALL sbfsMount
BNQ sbfsMountNext ; Nothing readable in that drive, and its slot stays empty.
; Mounted: remember which, and put the live record where it belongs.
SETD.1 SbfsDriveAt
LDA.1
CALL sbfsDriveBit
MVQA ; The bit is in Q, and RET put the old A back. OR reads A and B.
SETD.1 SbfsMounted
LDB.1
OR
STQ.1
SETD.1 SbfsDriveAt
LDA.1
CALL sbfsSlotAt
PSHD.3
POPD.1
SETD.0 SbfsMountLive
CALL sbfsCopyMount
sbfsMountNext:
SETD.1 SbfsDriveAt
LDA.1
INCA
STA.1
BRI sbfsMountEach
sbfsMountAllDone:
; Back to drive 0 and its record, which is where a shell starts. A stays nought throughout,
; which is the drive, the note on the buffer, and the slot to fetch.
RSTA
OUTA 0x24
SETD.1 SbfsDrive
STA.1
SETD.1 SbfsBufferKnown
STA.1
CALL sbfsSlotAt
PSHD.3
POPD.0
SETD.1 SbfsMountLive
CALL sbfsCopyMount
; Is drive 0 one of the ones that mounted?
INIA 0x01
SETD.1 SbfsMounted
LDB.1
AND
BRQ sbfsMountAllNone
RSTA
RSTB
CCF
ADD
RET
sbfsMountAllNone:
RSTA
INIB 0d1
CCF
ADD
RET
; A = a drive number. Q is the bit that stands for it in SbfsMounted.
sbfsDriveBit:
INIB 0x01
sbfsDriveBitStep:
BRA sbfsDriveBitDone
PSHA
RSTA
SHL ; A and B are one register to SHL, so with A nought this is B
POPA ; doubled - which for four drives never reaches the top.
DECA
BRI sbfsDriveBitStep
sbfsDriveBitDone:
RSTA
CCF
ADD ; Q = B, which is the bit.
RET
; ---- Changing which disk is the disk ----
;
; A holds the drive. Q is zero if it is now the one in use.
;
; The eight live bytes go back to the drive they belong to and the wanted drive's come in.
; Everything below this line in the file goes on reading the same four names it always has and
; never learns that more than one disk exists - which is the whole of why this is affordable.
;
; The buffer is FORGOTTEN, and that is not tidiness. The controller has one buffer shared by
; every drive, so the note of which block is in it is wrong the moment the drive changes.
; Leaving it would mean the next read of that block number quietly skipping the disk and
; handing back the other drive's data.
sbfsUse:
SETD.1 SbfsDrive
LDB.1
CCF
SUB
BRQ sbfsUseAlready ; Already there, and swapping would be a long way round to nothing.
PSHA ; The drive that was asked for, kept across the copying.
; The live record back to the drive it belongs to. DP1 is still SbfsDrive, from the
; comparison above.
LDA.1
CALL sbfsSlotAt
PSHD.3
POPD.1
SETD.0 SbfsMountLive
CALL sbfsCopyMount
; And the wanted drive's record into the live eight.
POPA
PSHA
CALL sbfsSlotAt
PSHD.3
POPD.0
SETD.1 SbfsMountLive
CALL sbfsCopyMount
POPA
SETD.1 SbfsDrive
STA.1
OUTA 0x24
; ---- The note on the buffer belongs to the drive that is leaving ----
;
; One buffer serves every drive, so "block 31 is in the buffer" stops being true the moment
; the drive changes, and a read of block 31 that trusted it would hand back the other disk.
;
; IT CANNOT CURRENTLY BE REACHED, and that is worth writing down rather than leaving as an
; implied claim. Only the file read-ahead consults the note - a directory scan deliberately
; does not - and finding a file requires a scan, which overwrites the note on the way past.
; Two disks were built with the same file at the same block to try to catch it and the
; answer was right either way.
;
; Kept because it is three instructions and it holds an invariant rather than patching a
; symptom: the note describes the selected drive. The day something reads two files without
; a directory between them, this is already true instead of being a bug with a story.
RSTA
SETD.1 SbfsBufferKnown
STA.1
sbfsUseAlready:
RSTA
RSTB
CCF
ADD
RET
; DP3 = the eight bytes belonging to the drive in A. Stepped rather than multiplied, because
; this machine cannot multiply and there are at most three steps.
sbfsSlotAt:
SETD.3 SbfsMountTable
BRA sbfsSlotThere
sbfsSlotStep:
DPUP.3 0d08
DECA
BNA sbfsSlotStep
sbfsSlotThere:
RET
; Eight bytes, DP0 to DP1.
sbfsCopyMount:
INIB 0d8
sbfsCopyMountByte:
LDA.0
STA.1
INCD.0
INCD.1
DECB
BNB sbfsCopyMountByte
RET
; ---- Finding something by path ---- ; ---- Finding something by path ----
; ;
; DP0 points at a path ending in a zero byte: names with '/' between them. A path that ; DP0 points at a path ending in a zero byte: names with '/' between them. A path that
@@ -3118,10 +3344,44 @@ SbfsStateWants:
SbfsMagic: SbfsMagic:
"SBFS" "SBFS"
; ---- Which disk this is, in eight bytes ----
;
; THE ORDER AND THE ADJACENCY ARE LOAD BEARING. These four are everything that distinguishes
; one mounted disk from another, and they are together so that changing drives is one copy
; out and one copy in. Nothing else may be put between them.
;
; Everything else in this file is either a constant or scratch for the operation being done
; now, and only one operation is ever being done - which is why a filesystem of 3,300 lines
; needs an eight byte record to know more than one disk. The rest never learns there is more
; than one.
;
; The version is not here. 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.
SbfsMountLive:
SbfsDirStart: SbfsDirStart:
0x00 0x00 0x00 0x00
SbfsDirBlocks: SbfsDirBlocks:
0x00 0x00 0x00 0x00
SbfsDiskBlocks:
0x00 0x00
SbfsCwd:
0x00 0x00
; One record a drive, and the live eight above are whichever is selected. Four, because the
; controller has four.
SbfsMountTable:
#Reserve 0d32
; Which drive the live record belongs to, and which drives were found to have a disk on them
; that this can read. A bit a drive, so drive n is bit n.
SbfsDrive:
0x00
SbfsMounted:
0x00
SbfsDriveCount:
0x00
SbfsDriveAt:
0x00
SbfsFileStart: SbfsFileStart:
0x00 0x00 0x00 0x00
SbfsFileBlocks: SbfsFileBlocks:
@@ -3130,8 +3390,6 @@ SbfsFileTail:
0x00 0x00
SbfsBlock: SbfsBlock:
0x00 0x00 0x00 0x00
SbfsDiskBlocks:
0x00 0x00
SbfsWantBlocks: SbfsWantBlocks:
0x00 0x00 0x00 0x00
SbfsCandidate: SbfsCandidate:
@@ -3165,8 +3423,6 @@ SbfsLeft:
; thing that resolves a path is here. Keeping it in the shell would mean either handing it ; thing that resolves a path is here. Keeping it in the shell would mean either handing it
; down on every call or having the shell paste it onto the front of every name, and the ; down on every call or having the shell paste it onto the front of every name, and the
; second of those is how a name that is already absolute gets ruined. ; second of those is how a name that is already absolute gets ruined.
SbfsCwd:
0x00 0x00
; ---- What walking a path keeps ---- ; ---- What walking a path keeps ----
; ;
+2 -2
View File
@@ -79,7 +79,7 @@ from `make`, not from here.
### 1. Recorded output ### 1. Recorded output
`Tests/run.sh` assembles each program named in `Tests/manifest`, runs it, and compares `Tests/run.sh` assembles each program named in `Tests/manifest`, runs it, and compares
everything it printed against a file in `Tests/expected`. 179 tests, of which 117 run, 35 everything it printed against a file in `Tests/expected`. 180 tests, of which 118 run, 35
only assemble, 16 are expected to fail to assemble, and 11 boot from ROM with no image only assemble, 16 are expected to fail to assemble, and 11 boot from ROM with no image
given at all. given at all.
@@ -365,7 +365,7 @@ presenting frames. It tests the console, which is where the logic is.
## Fixture Disks: ## Fixture Disks:
`Tests/makedisks.sh` builds 26 images with SplitDisk before anything runs, into `Tests/makedisks.sh` builds 27 images with SplitDisk before anything runs, into
`Tests/build/disks`. **That is the point of them.** A SplitBit program reading one of these `Tests/build/disks`. **That is the point of them.** A SplitBit program reading one of these
is being checked against a filesystem written by different code from the same written is being checked against a filesystem written by different code from the same written
specification, rather than against itself. specification, rather than against itself.
+27
View File
@@ -0,0 +1,27 @@
CosmOS
> 0
> greet.sbx 211
hello.sbx 53
Life.sbx 1396
Snake.sbx 2164
Keys.sbx 664
Say.sbx 156
Break.sbx 149
Grid.sbx 543
notes.txt 21
hi.script 121
bad.script 45
plain.script 24
cross.script 280
nonl.script 38
outer.script 376
inner.script 44
loop.script 35
17 files
> > other.txt 28
notes <dir>
1 file, 1 directory
> /notes> > /notes> > /notes> drive: this machine has no such drive
/notes> halted
Execution halted.
[exit 0]
+11
View File
@@ -0,0 +1,11 @@
drive
dir
drive 1
dir
cd /notes
drive 0
drive 1
drive 0
drive 1
drive 3
exit
+10
View File
@@ -115,6 +115,16 @@ for i in 1 2 3 4 5 6 7 8; do "$TOOL" put "$DISKS/sbfs.img" "filler$i.txt" >/dev/
printf 'this is not a program' > notes.txt printf 'this is not a program' > notes.txt
"$TOOL" put "$DISKS/cosmos.img" notes.txt >/dev/null "$TOOL" put "$DISKS/cosmos.img" notes.txt >/dev/null
# ---- A second disk, for the second drive ----
#
# Deliberately nothing like the first: its own file and its own directory, so that a test
# looking at one cannot be looking at the other by accident. Small, because what is on it
# matters and how much does not.
"$TOOL" format "$DISKS/other.img" 512 4 >/dev/null
printf 'this lives on the other disk' > other.txt
"$TOOL" put "$DISKS/other.img" other.txt >/dev/null
"$TOOL" mkdir "$DISKS/other.img" /notes >/dev/null
# ---- Scripts, including the ones that are meant to go wrong ---- # ---- Scripts, including the ones that are meant to go wrong ----
# #
# Built here rather than committed, because two of them are about BYTE POSITIONS and a # Built here rather than committed, because two of them are about BYTE POSITIONS and a
+12
View File
@@ -808,6 +808,18 @@ cosmosGrid | CosmOS/Source/cosmos.asm | run | cosmosGri
# drive that exists with nothing in it - selectable, and failing to read, like an empty # drive that exists with nothing in it - selectable, and failing to read, like an empty
# floppy drive. # floppy drive.
driveSelectTest | testPrograms/driveSelectTest.asm | run | - | 200000 | disks/sbfs.img driveSelectTest | testPrograms/driveSelectTest.asm | run | - | 200000 | disks/sbfs.img
# ---- Two disks ----
#
# The first thing on this machine to have more than one. drive says which, and going to the
# other shows a disk with nothing in common with the first - so a listing that looked right
# could not have come from the wrong one.
#
# THE WORKING DIRECTORY GOES WITH THE DRIVE, which is the whole point of the mount record.
# Drive 1 is left standing in /notes and drive 0 at the root, and the prompt says which on
# every trip between them - three times each way, because a record that is copied one way
# and not the other would still look right once. A drive this machine has not got is
# refused.
cosmosDrives | CosmOS/Source/cosmos.asm | run | cosmosDrives.in | 60000000 | disks/cosmos.img+disks/other.img
printDecimalTest | testPrograms/printDecimalTest.asm | xfail | - | - printDecimalTest | testPrograms/printDecimalTest.asm | xfail | - | -
printDigitTest | testPrograms/printDigitTest.asm | xfail | - | - printDigitTest | testPrograms/printDigitTest.asm | xfail | - | -
printHexTest | testPrograms/printHexTest.asm | xfail | - | - printHexTest | testPrograms/printHexTest.asm | xfail | - | -
+10 -3
View File
@@ -251,10 +251,16 @@ while IFS='|' read -r name src mode stdin limit disk keys; do
# A disk starts fresh for every run, so a test cannot pass because of what a # A disk starts fresh for every run, so a test cannot pass because of what a
# previous one left lying on it. The emulator makes the image if it is # previous one left lying on it. The emulator makes the image if it is
# missing, which is what removing it first arranges for. # missing, which is what removing it first arranges for.
if [ "$disk" != "-" ]; then # ---- More than one, separated by a plus ----
#
# The machine has four drives, so a test may name up to four images and they
# become drives 0 upwards in the order written. Each keeps its own :ro and @N,
# because those are properties of a disk rather than of the machine.
for onedisk in ${disk//+/ }; do
if [ "$onedisk" != "-" ]; then
# A trailing :ro attaches the image write protected, so that a test can # A trailing :ro attaches the image write protected, so that a test can
# check the device bars writes rather than the filesystem asking nicely. # check the device bars writes rather than the filesystem asking nicely.
DISKFILE="${disk%:ro}" DISKFILE="${onedisk%:ro}"
# And a trailing @N gives the disk a latency, so that a test can check the # And a trailing @N gives the disk a latency, so that a test can check the
# filesystem waits for it. Every other test runs with the answer there # filesystem waits for it. Every other test runs with the answer there
# before the next instruction, which is the one condition under which not # before the next instruction, which is the one condition under which not
@@ -273,8 +279,9 @@ while IFS='|' read -r name src mode stdin limit disk keys; do
esac esac
EMUARGS+=(--disk "$BUILD/$DISKFILE") EMUARGS+=(--disk "$BUILD/$DISKFILE")
[ -n "$DISKWAIT" ] && EMUARGS+=(--disk-cycles "$DISKWAIT") [ -n "$DISKWAIT" ] && EMUARGS+=(--disk-cycles "$DISKWAIT")
case "$disk" in *:ro) EMUARGS+=(--write-protect) ;; esac case "$onedisk" in *:ro) EMUARGS+=(--write-protect) ;; esac
fi fi
done
# A rom test names no image. The emulator then shadows its built in stage # A rom test names no image. The emulator then shadows its built in stage
# one into Program Memory and reads the disk for everything else, which is # one into Program Memory and reads the disk for everything else, which is
# what a machine with no debugger attached does. # what a machine with no debugger attached does.