Tell the person where it hurts

A fault stopped the machine and printed a line to standard error. On a terminal
that is a diagnosis. Behind a window it is a frozen picture and no reason at
all, because the message went somewhere nobody was looking - the machine looked
hung and was not. It had stopped, and said so invisibly.

CosmOS catches all five faults now and says what happened on the screen, with
the address, in red.

A FAULT ENDS THE PROGRAM, NOT THE MACHINE. That is the answer to "carry on or
start again", and it is not a compromise: a bare RETI from most of these meets
the instruction that failed and fails again, so carrying on was never on offer.
But the machine is almost never what is broken. Everything the shell puts back
when a program exits - the Stack, its vectors, the drive, the working directory,
the console, the screen - is exactly what wants putting back after one dies, so
the handler sets a status and joins handleExit. You are back at the prompt, and
the program is recorded as having STOPPED rather than finished, because saying
"finished" under a red fault message would be the shell contradicting itself.

A fault below where programs load is the system's own, and there is nothing to
go back to. That one says so and stops.

THE SCREEN GOES BACK TO A MODE TEXT CAN BE SEEN IN, and that is the part that
matters rather than the part that is prettiest. A program that faulted in bitmap
mode left the console with no text rows, so it draws nothing at all: the message
would be perfectly correct and completely invisible, which is the one thing it
must never be. Two palette entries go back 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, so the rest of what the program chose is left alone.

Both halves are checked by looking at the PICTURE, because the serial line was
never where the problem was. Crash blind ruins the palette and drops into bitmap
mode before it faults; without the mode the screen comes back 320 by 200 with
nothing on it, and without the palette it is the right size with the message
present and unreadable. Each break loses the red on its own.

Crash is also a program worth having: it breaks in whichever of the five ways
you name, so a fault screen can be looked at without having written a bug first.

Two things found on the way:

The native assembler keeps its OWN copy of the reserved vector names, so it did
not know NoHandler or NoDevice and built a cosmos.bin that differed from the
host assembler's. Caught by native.sh, which is exactly the drift that test
exists for.

And cosmosMonitor had dead input. It assembles code into 0x8000 and runs it, and
that code faults - which used to kill the machine, so everything after it in the
file had never run. It runs now, and the recording grew by sixty lines of
monitor session that had been unreachable since the day the fault was put there.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
This commit is contained in:
Anachronaut
2026-09-01 15:08:20 -04:00
co-authored by Claude Opus 5
parent 000a6d39cb
commit fd9c4c75f8
19 changed files with 931 additions and 10 deletions
+145
View File
@@ -0,0 +1,145 @@
; Breaks on purpose, in whichever of the four ways it is asked for.
;
; Every one of these used to stop the machine and print a line to a standard error that
; nobody behind a window is looking at, so the machine appeared to hang. The system catches
; all of them now and says what happened and where, and this is what says so - and what a
; person can run when they want to see the fault screen without having written a bug first.
;
; Crash opcode a byte in the middle of the code that does not decode
; Crash service a SWI naming a service the system does not implement
; Crash bank a transfer out of a bank that has nothing registered in it
; Crash device asking the console to interrupt, with no handler installed
; Crash blind the same bad byte, but from a screen with nowhere to print at all
;
; The last one is the odd one: it is not something this program does wrong, it is something
; it fails to have done. The interrupt arrives from outside once the console has anything to
; say, and with no input it is the END of input that arrives.
;
; Written by Anachronaut
#Include services.asm
#Program
#Base 0x4000
start:
SETD.0 Argument
INIB 0d15
SWI osArgument
SETD.0 Argument
SETD.1 WordOpcode
CALL textSame
BRQ crashOpcode
SETD.0 Argument
SETD.1 WordService
CALL textSame
BRQ crashService
SETD.0 Argument
SETD.1 WordBank
CALL textSame
BRQ crashBank
SETD.0 Argument
SETD.1 WordDevice
CALL textSame
BRQ crashDevice
SETD.0 Argument
SETD.1 WordBlind
CALL textSame
BRQ crashBlind
SETD.0 Usage
SWI osPrintString
INIA 0x0A
OUTA 0x00
INIA 0x01
SWI osExit
; ---- The case the fault screen exists for ----
;
; Bitmap mode has no text rows, so the console draws NOTHING there: a program that faults
; here leaves the system with a message to print and nowhere to print it. Putting the screen
; back into a mode that has characters in it is the difference between a diagnosis and a
; machine that appears to have hung.
crashBlind:
; ---- And with the colours ruined as well ----
;
; A known mode is only half a known screen. This makes the ink of attribute one the same as
; its paper, which is what a program that wrote its own palette can easily leave behind -
; and a message printed into that is perfectly present and completely invisible.
INIA 0d4
OUTA 0xE3
INIA 0x30
OUTA 0xE2
INIA 0x03
OUTA 0xE8 ; Video memory as bank four.
INIA 0d4
OUTA 0xE3
INIA 0xFC
OUTA 0xE4
INIA 0x40
OUTA 0xE5 ; 0xFC40, the two entries attribute one draws from.
RSTA
INIB 0d8
crashBlindWipe:
OUTA 0xE9
DECB
BNB crashBlindWipe ; Both of them black, ink and paper alike.
INIA 0x02
OUTA 0x31
crashOpcode:
0x00 ; Not an instruction, and never will be.
crashService:
SWI 0d40 ; Forty is nobody's.
crashBank:
INIA 0d9 ; Nothing is registered there.
OUTA 0xE0
RSTA
OUTA 0xE1
OUTA 0xE2
INIA 0d1
OUTA 0xE3 ; Into Data Memory.
RSTA
OUTA 0xE4
OUTA 0xE5
OUTA 0xE6
INIA 0d16
OUTA 0xE7
INIA 0x01
OUTA 0xE8 ; Blit, from a bank that is not there.
crashDevice:
INIA 0x02 ; Interrupt me when the console has something to say.
OUTA 0x02
SIF
crashWait:
; Never touches the console, so whatever happens next came from outside.
BRI crashWait
#Data
#Base 0x2000
WordOpcode:
"opcode"
WordService:
"service"
WordBank:
"bank"
WordDevice:
"device"
WordBlind:
"blind"
Usage:
"Crash opcode | service | bank | device | blind"
Argument:
#Reserve 0d16
#Include text.asm
+7 -1
View File
@@ -2258,7 +2258,7 @@ ReservedLeft:
ReservedWalk:
0x00 0x00
ReservedCount:
0d5
0d7
VecHandlerName:
#Reserve 0d23
@@ -2285,6 +2285,12 @@ ReservedNames:
"BankFault"
#Reserve 0d5
0d4
"NoHandler"
#Reserve 0d5
0d5
"NoDevice"
#Reserve 0d6
0d6
ProgPut:
0x00 0x00
+40
View File
@@ -599,8 +599,48 @@ from every assembly file in it. Several are old programs written for the bare ma
| 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. |
| 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 and the screen, 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 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.
+257
View File
@@ -1260,6 +1260,204 @@ historyAddDone:
STA.1
RET
; ---- When something goes wrong that nothing can carry on past ----
;
; 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 goes to a standard error nobody is looking at. The machine looked hung and was
; not - it had stopped, and said so somewhere invisible.
;
; So the system catches all five and says it on the screen instead.
;
; ---- A fault ends the PROGRAM, not the machine ----
;
; That is the answer to "carry on or start again", and it is not a compromise: a bare RETI
; from most of these 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 - the Stack, the vectors it installed, the drive, the working
; directory, the console, the screen - is exactly what wants putting back after one dies, so
; a fault in a loaded program joins handleExit and you are back at the prompt.
;
; A fault BELOW where programs load is the system's own, and there is nothing to go back to.
; That one says so and stops.
;
; ---- What a handler must not do ----
;
; Fault. There is no double fault rule on this machine: a handler that commits the fault it
; was called about is called again, forever, and each time costs another frame of Stack. So
; nothing below asks for a service, touches a disk, or reaches anything that can refuse.
faultBadOpcode:
SETD.0 FaultOpcode
BRI faultPlain
faultGuard:
SETD.0 FaultGuard
BRI faultPlain
faultBank:
SETD.0 FaultBank
faultPlain:
; The Stack Pointer names the frame, and it has to be taken before anything pushes. Nothing
; above here does: a SETD and a branch move no Stack.
MVSD.3
RSTA
SETD.1 FaultNumbered
STA.1
BRI faultSay
faultNoHandler:
SETD.0 FaultNoHandler
BRI faultNumbered
faultNoDevice:
SETD.0 FaultNoDevice
faultNumbered:
MVSD.3
; These two are the only faults that say WHICH one, and the machine hands that over in Q -
; the one thing a handler here is given in a register. Kept at once, before printing can
; disturb it.
MVQA
SETD.1 FaultNumber
STA.1
INIA 0x01
SETD.1 FaultNumbered
STA.1
faultSay:
; The screen before the words. There is no use saying any of this somewhere it cannot be
; read, and a program that faulted may have left the screen with nowhere to put a letter.
CALL faultScreen
CALL printString
SETD.1 FaultNumbered
LDA.1
BRA faultWhere
SETD.1 FaultNumber
LDA.1
CALL printByteHex
faultWhere:
SETD.0 FaultAt
CALL printString
CALL faultAddress
CALL newLine
; The registers as the frame kept them, which is what they were when it happened.
SETD.0 FaultRegisters
CALL printString
PSHD.3
POPD.0
DPUP.0 0d3
LDA.0
CALL printByteHex
SETD.0 FaultB
CALL printString
PSHD.3
POPD.0
DPUP.0 0d4
LDA.0
CALL printByteHex
SETD.0 FaultQ
CALL printString
PSHD.3
POPD.0
DPUP.0 0d2
LDA.0
CALL printByteHex
CALL newLine
; ---- Whose fault it was ----
;
; Where it happened says which, and one byte of the address decides it: a loaded program
; begins at 0x4000 and everything below that is the system.
PSHD.3
POPD.0
DPUP.0 0d13
LDA.0
INIB 0x40
CCF
SUB
BRC faultInSystem
; A program that will not be carrying on. handleExit does all of the putting back, and
; takes the status the program is deemed to have stopped with in A.
INIA 0x01
SETD.1 FaultStopped
STA.1
INIA 0xFF
BRI handleExit
faultInSystem:
; Nothing to go back to: the shell IS what faulted, and its Stack, its variables and its
; place in its own code are all suspect. Saying so and stopping is the only honest answer,
; and it is a great deal better than the frozen picture this used to be.
SETD.0 FaultSystem
CALL printString
CALL newLine
HALT
; The two bytes of the address in the frame, printed high half first.
faultAddress:
PSHD.3
POPD.0
DPUP.0 0d13
LDA.0
CALL printByteHex
INCD.0
LDA.0
CALL printByteHex
RET
; ---- A screen this can be read on ----
;
; "Put the screen in a known mode" is not tidiness. A program that left the screen in bitmap
; mode left nowhere to draw a character at all - the console draws nothing when there are no
; text rows - so without this, the message about what went wrong is invisible, which is the
; one thing it must never be.
faultScreen:
INIA 0x01
OUTA 0x31 ; Eighty columns of text, whatever was being used.
; And the whole view back to the corner. A scrolled origin or a fraction of a cell puts
; every character somewhere other than where it says it is.
RSTA
OUTA 0x34
OUTA 0x36
OUTA 0x37
OUTA 0x38
; ---- Colours it can be read in ----
;
; A known mode is only half of a known screen: a program that wrote its own palette may
; have left every ink the same as every paper. Attribute one draws in palette entries 16
; and 17, so those two are written and the rest of the program's colours are left alone -
; there is nothing to be gained here by taking away more than is needed.
CALL screenBank
INIA 0d4
OUTA 0xE3
INIA 0xFC
OUTA 0xE4
INIA 0x40
OUTA 0xE5 ; 0xFC40, which is entry sixteen.
RSTA
OUTA 0xE9
OUTA 0xE9
OUTA 0xE9
OUTA 0xE9 ; Black paper.
INIA 0xD0
OUTA 0xE9
INIA 0x40
OUTA 0xE9
INIA 0x38
OUTA 0xE9
RSTA
OUTA 0xE9 ; Red ink, the same red the machine wakes up with.
INIA 0x01
OUTA 0x06 ; And draw in it.
; A console that can be printed to at all: line mode, a cursor, nothing interrupting.
INIA 0x04
OUTA 0x02
RET
; ---- A command that did not work ----
;
; The one place a failure is recorded, so that the thing reading lines out of a file can
@@ -4010,7 +4208,26 @@ exitWorked:
INIA 0x04
OUTA 0x02
; The ink too, and for the same reason as the cursor: a program that chose a colour is not
; around to put it back, and neither is one the fault screen printed for. The shell owns
; the prompt, so the shell is what makes sure it is readable.
RSTA
OUTA 0x06
; ---- Finished, or stopped ----
;
; A program that faulted did not finish, and saying so would be the shell's own word
; against what the fault screen just said in red immediately above it.
SETD.1 FaultStopped
LDA.1
BRA exitFinished
RSTA
STA.1 ; Cleared, so the next program is not blamed for this one.
SETD.0 Stopped
BRI exitSay
exitFinished:
SETD.0 Finished
exitSay:
CALL printString
CALL newLine
BRI prompt
@@ -5259,6 +5476,28 @@ LoadedText:
"loaded, starting at "
NothingLoaded:
"nothing is loaded"
Stopped:
"the program was stopped"
FaultOpcode:
"that byte is not an instruction"
FaultGuard:
"a write into a fenced off part of a bank"
FaultBank:
"a bank that is not there, or an address past its end"
FaultNoHandler:
"nothing is installed at service "
FaultNoDevice:
"nothing is installed for the device on port "
FaultAt:
", at "
FaultRegisters:
" A "
FaultB:
" B "
FaultQ:
" Q "
FaultSystem:
"that was the system itself, so there is nowhere to carry on from. Start the machine again."
Finished:
"finished"
@@ -5682,6 +5921,19 @@ EditWalkBack:
EditWasControl:
0x00
; ---- What the fault screen is saying ----
;
; Whether this cause names an entry as well as itself, and which one. Only the two faults
; about a missing handler do, and for those the machine puts the number in Q.
FaultNumbered:
0x00
FaultNumber:
0x00
; Set when a program is being ended by a fault rather than by asking. handleExit reads it to
; choose its last word, and clears it.
FaultStopped:
0x00
; Whether the line being read is one to remember. The shell's are; a program's are not.
EditKeepHistory:
0x00
@@ -5726,6 +5978,11 @@ CommandLine:
#Vectors
Boot boot
BadOpcode faultBadOpcode
GuardViolation faultGuard
BankFault faultBank
NoHandler faultNoHandler
NoDevice faultNoDevice
osPrintString handlePrintString
osReadLine handleReadLine
osExit handleExit
+1 -1
View File
@@ -79,7 +79,7 @@ from `make`, not from here.
### 1. Recorded output
`Tests/run.sh` assembles each program named in `Tests/manifest`, runs it, and compares
everything it printed against a file in `Tests/expected`. 194 tests, of which 132 run, 35
everything it printed against a file in `Tests/expected`. 196 tests, of which 134 run, 35
only assemble, 16 are expected to fail to assemble, and 11 boot from ROM with no image
given at all.
Executable
+277
View File
@@ -0,0 +1,277 @@
#!/usr/bin/env bash
# Checks SplitDisk against the SBFS format.
#
# The tool and the SplitBit side are two implementations of one written specification,
# and nothing but that document keeps them the same. This checks the host half on its
# own: that a file put onto a disk comes back off it byte for byte, that the sizes which
# exercise the block and tail arithmetic all survive, and that the things the format
# says cannot happen are refused rather than half done.
#
# Written by Anachronaut
set -u
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
TOOL="$ROOT/SplitDisk"
WORK="$ROOT/Tests/build/disk"
PASS=0
FAIL=0
FAILED_NAMES=()
GREEN=$'\033[32m'; RED=$'\033[31m'; RESET=$'\033[0m'
[ -t 1 ] || { GREEN=""; RED=""; RESET=""; }
check() {
local name="$1"; shift
if "$@" >/dev/null 2>&1; then
PASS=$((PASS + 1)); printf " [%sok %s] %s\n" "$GREEN" "$RESET" "$name"
else
FAIL=$((FAIL + 1)); FAILED_NAMES+=("$name")
printf " [%sFAIL%s] %s\n" "$RED" "$RESET" "$name"
fi
}
# The opposite: the command is supposed to fail, and passing would be the bug.
refuses() {
local name="$1"; shift
if "$@" >/dev/null 2>&1; then
FAIL=$((FAIL + 1)); FAILED_NAMES+=("$name")
printf " [%sFAIL%s] %s (it was allowed)\n" "$RED" "$RESET" "$name"
else
PASS=$((PASS + 1)); printf " [%sok %s] %s\n" "$GREEN" "$RESET" "$name"
fi
}
if [ ! -x "$TOOL" ]; then
echo "SplitDisk is not built."
exit 1
fi
rm -rf "$WORK"; mkdir -p "$WORK"
cd "$WORK" || exit 1
echo "Checking SplitDisk against the SBFS format."
check "format a disk" "$TOOL" format work.img 64 2
refuses "refuse a disk with no room" "$TOOL" format tiny.img 2 4
refuses "refuse an unformatted disk" "$TOOL" list /dev/null
# The sizes that exercise every corner of blocks-plus-tail: nothing at all, less than a
# block, exactly a block, a part block, and an exact multiple.
: > empty.bin
printf 'x' > one.bin
head -c 256 /dev/urandom > exact.bin
head -c 700 /dev/urandom > part.bin
head -c 768 /dev/urandom > whole.bin
for f in empty.bin one.bin exact.bin part.bin whole.bin; do
check "put $f" "$TOOL" put work.img "$f"
done
roundTrip() {
"$TOOL" get work.img "$1" "got_$1" >/dev/null 2>&1 || return 1
cmp -s "$1" "got_$1"
}
for f in empty.bin one.bin exact.bin part.bin whole.bin; do
check "$f comes back byte for byte" roundTrip "$f"
done
refuses "refuse a name of 29 characters" "$TOOL" put work.img part.bin twentyNineCharactersLong.asm
refuses "refuse a duplicate name" "$TOOL" put work.img one.bin
refuses "refuse a file that is not there" "$TOOL" get work.img nosuch.bin out.bin
check "delete" "$TOOL" delete work.img one.bin
refuses "the deleted file is gone" "$TOOL" get work.img one.bin out.bin
check "the name can be used again" "$TOOL" put work.img one.bin
# Contiguous files mean a disk can have room without having room in one piece. That is a
# consequence of the format rather than a bug, so it is checked rather than worked around.
"$TOOL" format frag.img 16 1 >/dev/null 2>&1
head -c 1024 /dev/urandom > a.bin; cp a.bin b.bin; cp a.bin c.bin
"$TOOL" put frag.img a.bin >/dev/null 2>&1
"$TOOL" put frag.img b.bin >/dev/null 2>&1
"$TOOL" put frag.img c.bin >/dev/null 2>&1
"$TOOL" delete frag.img a.bin >/dev/null 2>&1
"$TOOL" delete frag.img c.bin >/dev/null 2>&1
head -c 2048 /dev/urandom > big.bin
refuses "refuse a file with no run long enough" "$TOOL" put frag.img big.bin
head -c 512 /dev/urandom > fits.bin
check "but one that fits the gap goes on" "$TOOL" put frag.img fits.bin
# ---- Directories ----
#
# Version two, which adds a parent to each entry and a flag bit saying an entry is a
# directory. Both come out of bytes the entry had already set aside, so nothing moved and
# a version one disk needs no converting: zero in those bytes means the root, which is
# exactly where every file on a flat disk is.
#
# The version is therefore a statement about what is ON a disk rather than about what made
# it, and these check that it is only raised when it becomes true.
"$TOOL" format tree.img 64 2 >/dev/null 2>&1
printf 'a file in the root' > root.txt
check "a fresh disk is flat" "$TOOL" put tree.img root.txt
version() { "$TOOL" list "$1" 2>/dev/null | head -1 | grep -q "version $2"; }
check "and says it is version 1" version tree.img 1
check "make a directory" "$TOOL" mkdir tree.img /Apps
check "which raises it to version 2" version tree.img 2
check "make one inside it" "$TOOL" mkdir tree.img /Apps/Source
check "put a file down a path" "$TOOL" put tree.img root.txt /Apps/Source/deep.txt
# The point of the whole exercise: a name means something different in each place, so the
# same one can be used twice without either being in the other's way.
check "the same name in two places" "$TOOL" put tree.img root.txt /Apps/root.txt
roundTripAt() {
"$TOOL" get tree.img "$1" got_deep.txt >/dev/null 2>&1 || return 1
cmp -s root.txt got_deep.txt
}
check "it comes back byte for byte" roundTripAt /Apps/Source/deep.txt
check ". and .. walk the path" roundTripAt /Apps/./Source/../root.txt
check ".. from the root is the root" roundTripAt /Apps/../../root.txt
# Each of these is a way the tree could be made to contradict itself, and each is refused
# rather than half done.
refuses "no file where a directory goes" "$TOOL" put tree.img root.txt /root.txt/x.txt
refuses "no putting into thin air" "$TOOL" put tree.img root.txt /Nowhere/x.txt
refuses "no duplicate in one directory" "$TOOL" mkdir tree.img /Apps
refuses "no getting a directory" "$TOOL" get tree.img /Apps out.bin
refuses "delete will not take a directory" "$TOOL" delete tree.img /Apps
refuses "rmdir will not take a file" "$TOOL" rmdir tree.img /root.txt
refuses "nor the root" "$TOOL" rmdir tree.img /
# THE REFUSAL THAT MATTERS MOST. Parents are entry indices and a freed index is handed out
# again, so removing a directory with things still in it would let the next file created
# adopt them. Emptying it first is the only safe order.
refuses "no removing an occupied one" "$TOOL" rmdir tree.img /Apps/Source
check "empty it first" "$TOOL" delete tree.img /Apps/Source/deep.txt
check "then it goes" "$TOOL" rmdir tree.img /Apps/Source
# A path is names with separators between them, and a name is still twenty two characters.
refuses "refuse a 23 character component" "$TOOL" mkdir tree.img /Apps/abcdefghijklmnopqrstuvw
refuses "refuse a path naming nothing" "$TOOL" mkdir tree.img /Apps/
# A directory costs an entry and no blocks at all, which is what keeps the flat array of
# entries the whole allocation map. If a directory ever took a block, this would drop.
blocksFree() { "$TOOL" list "$1" 2>/dev/null | tail -1 | sed 's/.*used, //; s/ blocks free.*//'; }
before=$(blocksFree tree.img)
"$TOOL" mkdir tree.img /Empty >/dev/null 2>&1
check "a directory costs no blocks" [ "$before" = "$(blocksFree tree.img)" ]
# ---- A directory no bigger than the parent field can name ----
#
# Eight entries to a block and the parent is an index plus one in two bytes, so entry
# 65535 has no parent number: adding one wraps to zero, and zero is the root. Such an
# entry does not refuse what is put inside it. It writes the thing into the ROOT while
# reporting the path that was asked for, and then cannot find it again - so the same
# create succeeds over and over, piling up entries of one name in one directory, which is
# the exact corruption rename exists to refuse.
refuses "no directory past the wrap" "$TOOL" format huge.img 65535 8192
check "the largest that fits" "$TOOL" format huge.img 65535 8191
# And a disk claiming one, which is what something that never checked would have written.
# The claim is in the superblock, so it does not need a disk that size to be made.
"$TOOL" format lying.img 64 2 >/dev/null
printf '\x20\x00' | dd of=lying.img bs=1 seek=10 conv=notrunc status=none
refuses "nor reading one that claims it" "$TOOL" list lying.img
# ---- A boot area, and the two halves of the superblock that describe it ----
#
# bootBlocks and directoryStart say the same thing from two sides, so a disk where they
# disagree is one where there is no way to tell which is wrong. Both are refused.
check "format with a boot area" "$TOOL" format boot.img 512 4 32
check "and it reads back" "$TOOL" list boot.img
refuses "no boot area bigger than a disk" "$TOOL" format small.img 32 2 64
check "and none at all is still fine" "$TOOL" format plain.img 64 2
bootField() { python3 -c "
import sys
f = open(sys.argv[1], 'r+b'); f.seek(int(sys.argv[2])); f.write(bytes.fromhex(sys.argv[3]))
" "$@"; }
cp boot.img lying.boot.img
bootField lying.boot.img 14 0010 # Claims 16 blocks a slot, directory says 32.
refuses "nor a boot area that disagrees" "$TOOL" list lying.boot.img
cp boot.img badslot.img
bootField badslot.img 16 07 # Names slot 7, and there are two.
refuses "nor a slot that does not exist" "$TOOL" list badslot.img
# ---- Writing a boot slot, and choosing between them ----
#
# Two commands rather than one, deliberately: writing a slot and starting from it are
# different decisions, and joining them would make every write a commitment.
printf 'not really a bootloader' > stage.bin
check "write a boot slot" "$TOOL" boot boot.img stage.bin 0
check "and the other one" "$TOOL" boot boot.img stage.bin 1
check "choose which one starts" "$TOOL" bootslot boot.img 1
refuses "no third slot to write" "$TOOL" boot boot.img stage.bin 2
refuses "nor a third to choose" "$TOOL" bootslot boot.img 2
refuses "no boot slot without an area" "$TOOL" boot plain.img stage.bin 0
# A slot holds what it holds. Something too big for one is refused rather than cut off,
# because half a bootloader is the failure with no way back.
head -c 9000 /dev/zero > toobig.bin # A slot on boot.img is 32 blocks, so 8192.
refuses "nor more than a slot holds" "$TOOL" boot boot.img toobig.bin 0
# THE WHOLE SLOT IS WRITTEN, not just the part the file fills. A slot still holding the
# tail of whatever was there before is one whose contents depend on its history.
printf 'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa' > long.bin
"$TOOL" boot boot.img long.bin 0 >/dev/null
"$TOOL" boot boot.img stage.bin 0 >/dev/null
check "and it is written whole" python3 -c "
import sys
d = open('boot.img','rb').read()
slot = d[256:256 + 32 * 256]
sys.exit(1 if b'aaaa' in slot else 0)"
# ---- How the last start went ----
check "the boot state reads" "$TOOL" bootstate boot.img
check "and can be set" "$TOOL" bootstate boot.img 2
refuses "but only to a state there is" "$TOOL" bootstate boot.img 7
check "a fresh disk is settled" python3 -c "
import sys
sys.exit(0 if open('plain.img','rb').read()[17] == 0 else 1)"
# ---- Mirroring a host directory ----
#
# What the system disk is built with. Every file goes through put and every directory
# through mkdir, so this is a walk over machinery already checked above - what wants
# checking is the walk: that it goes all the way down, that it leaves behind what it was
# told to, and that it REFUSES a name the format cannot hold rather than skipping it, since
# a disk quietly missing a file is the failure a mirror exists to prevent.
mkdir -p tree/inner/deeper tree/leave
printf 'top' > tree/top.txt
printf 'inner' > tree/inner/middle.txt
printf 'deep' > tree/inner/deeper/bottom.txt
printf 'not this' > tree/leave/ignored.txt
: > tree/.hidden
"$TOOL" format mirror.img 256 8 >/dev/null
check "mirror a directory tree" "$TOOL" mirror mirror.img tree /
"$TOOL" list mirror.img > mirrored.txt 2>&1
grep -q '/inner/deeper/bottom.txt' mirrored.txt \
&& { PASS=$((PASS + 1)); printf " [%sok %s] %s\n" "$GREEN" "$RESET" "it goes all the way down"; } \
|| { FAIL=$((FAIL + 1)); FAILED_NAMES+=("depth"); printf " [%sFAIL%s] %s\n" "$RED" "$RESET" "it goes all the way down"; }
grep -q 'hidden' mirrored.txt \
&& { FAIL=$((FAIL + 1)); FAILED_NAMES+=("hidden"); printf " [%sFAIL%s] %s\n" "$RED" "$RESET" "and leaves dotfiles behind"; } \
|| { PASS=$((PASS + 1)); printf " [%sok %s] %s\n" "$GREEN" "$RESET" "and leaves dotfiles behind"; }
# Named on the command line, which is how a project keeps what it builds out of what it
# wrote.
"$TOOL" format skipped.img 256 8 >/dev/null
check "mirror with something left out" "$TOOL" mirror skipped.img tree / leave
"$TOOL" list skipped.img > skipped.txt 2>&1
grep -q 'ignored.txt' skipped.txt \
&& { FAIL=$((FAIL + 1)); FAILED_NAMES+=("skip"); printf " [%sFAIL%s] %s\n" "$RED" "$RESET" "and the skipped one is not there"; } \
|| { PASS=$((PASS + 1)); printf " [%sok %s] %s\n" "$GREEN" "$RESET" "and the skipped one is not there"; }
# Twenty-three characters, one more than a directory entry holds.
printf 'too long' > tree/aNameOfTwentyThreeChars
"$TOOL" format refused.img 256 8 >/dev/null
refuses "a name too long stops the mirror" "$TOOL" mirror refused.img tree /
rm -f tree/aNameOfTwentyThreeChars
echo
if [ "$FAIL" -eq 0 ]; then
echo "All $PASS disk tool checks passed."
exit 0
fi
echo "$PASS passed, $FAIL failed: ${FAILED_NAMES[*]}"
exit 1
+2 -1
View File
@@ -22,6 +22,7 @@ Break.sbx 149
Grid.sbx 559
Press.sbx 872
Mode.sbx 48
Crash.sbx 632
notes.txt 21
Apps <dir>
hi.script 121
@@ -33,7 +34,7 @@ outer.script 376
inner.script 44
loop.script 35
crossed.txt 560
20 files, 1 directory
21 files, 1 directory
> exit
halted
Execution halted.
+2 -1
View File
@@ -12,6 +12,7 @@ Break.sbx 149
Grid.sbx 559
Press.sbx 872
Mode.sbx 48
Crash.sbx 632
notes.txt 21
Apps <dir>
hi.script 121
@@ -22,7 +23,7 @@ nonl.script 38
outer.script 376
inner.script 44
loop.script 35
19 files, 1 directory
20 files, 1 directory
> drive 1
> dir
other.txt 28
+47
View File
@@ -0,0 +1,47 @@
CosmOS
> Crash opcode
that byte is not an instruction, at 4081
A 00 B 0F Q 00
the program was stopped
> Crash service
nothing is installed at service 28, at 4084
A 00 B 0F Q 00
the program was stopped
> Crash bank
a bank that is not there, or an address past its end, at 409E
A 01 B 0F Q 00
the program was stopped
> Crash device
nothing is installed for the device on port 00, at 40A5
A 02 B 0F Q 00
the program was stopped
> Crash sideways
Crash opcode | service | bank | device | blind
finished
> dir
greet.sbx 211
hello.sbx 53
Life.sbx 1396
Snake.sbx 2164
Keys.sbx 664
Say.sbx 156
Break.sbx 149
Grid.sbx 559
Press.sbx 872
Mode.sbx 48
Crash.sbx 632
notes.txt 21
Apps <dir>
hi.script 121
bad.script 45
plain.script 24
cross.script 280
nonl.script 38
outer.script 376
inner.script 44
loop.script 35
20 files, 1 directory
> exit
halted
Execution halted.
[exit 0]
+9
View File
@@ -0,0 +1,9 @@
CosmOS
> monitor
x examine, d disassemble, a assemble, s set, b bank, g go, exit leaves
* g 3F00
that byte is not an instruction, at 3F00
A 01 B 7F Q 00
that was the system itself, so there is nowhere to carry on from. Start the machine again.
Execution halted.
[exit 0]
+2 -1
View File
@@ -19,6 +19,7 @@ Break.sbx 149
Grid.sbx 559
Press.sbx 872
Mode.sbx 48
Crash.sbx 632
notes.txt 21
Apps <dir>
hi.script 121
@@ -29,7 +30,7 @@ nonl.script 38
outer.script 376
inner.script 44
loop.script 35
19 files, 1 directory
20 files, 1 directory
> exit
halted
Execution halted.
+68 -3
View File
@@ -51,8 +51,73 @@ bank 00
8009 12 AND
800A 00 ?
800B 00 ?
* g 8000Fault: 0x00 at Program Address 0x800A is not an instruction.
* g 8000
H
that byte is not an instruction, at 800A
A 85 B 3F Q 05
the program was stopped
* d 8000
8000 26 48 INIA 48
8002 D1 00 OUTA 00
8004 26 0A INIA 0A
8006 D1 00 OUTA 00
8008 18 SHR
8009 12 AND
800A 00 ?
800B 00 ?
* b data
bank 01
* s 8100 68 65 6C 6C 6F 2C 20 74 79 70 65 64 0A 00
* b program
bank 00
* a 8200
8200: SETD.0 8100
8204: SWI 10
8206: lda.2
8208: LDD.0.1
820B: frobnicate
no such instruction
820B: INIA
that one needs a value after it
820B: SWI 12
820D: .
* d 8200
8200 47 00 81 00 SETD.0 8100
8204 72 10 SWI 10
8206 42 02 LDA.2
8208 4A 00 01 LDD.0.1
820B 72 12 SWI 12
820D 00 ?
820E 00 ?
820F 00 ?
* g 8200
hello, typed
finished
* exit
> dir
greet.sbx 211
hello.sbx 53
Life.sbx 1396
Snake.sbx 2164
Keys.sbx 664
Say.sbx 156
Break.sbx 149
Grid.sbx 559
Press.sbx 872
Mode.sbx 48
Crash.sbx 632
notes.txt 21
Apps <dir>
hi.script 121
bad.script 45
plain.script 24
cross.script 280
nonl.script 38
outer.script 376
inner.script 44
loop.script 35
20 files, 1 directory
> exit
halted
Execution halted.
[exit 1]
[exit 0]
+2 -1
View File
@@ -12,6 +12,7 @@ Break.sbx 149
Grid.sbx 559
Press.sbx 872
Mode.sbx 48
Crash.sbx 632
notes.txt 21
Apps <dir>
hi.script 121
@@ -22,7 +23,7 @@ nonl.script 38
outer.script 376
inner.script 44
loop.script 35
19 files, 1 directory
20 files, 1 directory
> load
load what?
> load nosuch.sbx
+2 -1
View File
@@ -10,6 +10,7 @@ Break.sbx 149
Grid.sbx 559
Press.sbx 872
Mode.sbx 48
Crash.sbx 632
notes.txt 21
Apps <dir>
hi.script 121
@@ -20,7 +21,7 @@ nonl.script 38
outer.script 376
inner.script 44
loop.script 35
19 files, 1 directory
20 files, 1 directory
> load Say.sbx
loaded, starting at 4000
> run the disk took its time
+7
View File
@@ -0,0 +1,7 @@
Crash opcode
Crash service
Crash bank
Crash device
Crash sideways
dir
exit
+2
View File
@@ -0,0 +1,2 @@
monitor
g 3F00
+5
View File
@@ -124,6 +124,11 @@ for i in 1 2 3 4 5 6 7 8; do "$TOOL" put "$DISKS/sbfs.img" "filler$i.txt" >/dev/
"$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" \
"$ROOT/Programs/CosmOS/Apps/Mode.asm" -o "$WORK/Mode.sbx" >/dev/null
"$TOOL" put "$DISKS/cosmos.img" "$WORK/Mode.sbx" >/dev/null
# Crash.sbx breaks in each of the four ways the system now catches, which is the only way to
# reach a fault screen from a recorded test.
"$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" \
"$ROOT/Programs/CosmOS/Apps/Crash.asm" -o "$WORK/Crash.sbx" >/dev/null
"$TOOL" put "$DISKS/cosmos.img" "$WORK/Crash.sbx" >/dev/null
printf 'this is not a program' > notes.txt
"$TOOL" put "$DISKS/cosmos.img" notes.txt >/dev/null
+14
View File
@@ -326,6 +326,20 @@ cosmosPressKeys | CosmOS/Source/cosmos.asm | run | -
# a forty column screen, which is a width nothing else here ever runs at - and the shell
# reads that width for itself now, every line it edits.
cosmosNoData | CosmOS/Source/cosmos.asm | run | cosmosNoData.in | - | disks/cosmos.img
# The system catching a fault instead of the machine stopping on one. All four causes, one
# after another, and a dir at the end: THE POINT IS THE LINE AFTER THE FAULT. Every one of
# these used to stop the machine dead, and behind a window it looked like a hang because the
# diagnosis went to a standard error nobody was looking at.
#
# The addresses are exact and are the reason the recording is worth having. Note that the
# missing service reports the address PAST its SWI, where the others report the instruction
# that failed: those two faults are the only ones where the instruction did dispatch and it
# was the entry that was empty.
cosmosFault | CosmOS/Source/cosmos.asm | run | cosmosFault.in | - | disks/cosmos.img
# And a fault in the system's own code, which has nowhere to go back to. Reached through the
# monitor by jumping into the unwritten space above CosmOS and below where programs load,
# which is zeroes - so it faults at once and corrupts nothing on the way.
cosmosFaultSystem | CosmOS/Source/cosmos.asm | run | cosmosFaultSys.in | - | disks/cosmos.img
# The shell editing the line it is being given, which is the whole reason the keys were
# made to arrive. Every line here is typed wrong and then corrected with a different key, and
# every one of them comes out as the same command - so the recording says both what the
+42
View File
@@ -911,6 +911,48 @@ inked scrollback 2 1 \
&& result ok "what scrolled off is still there" "the origin went back and found it" \
|| result no "what scrolled off is still there" "nothing at 2,1"
# ---- The fault screen, from a screen with nowhere to print on it ----
#
# Every other test of the fault screen reads what came down the serial line, and the serial
# line is not where the problem was: a program that faulted in BITMAP MODE left the console
# with no text rows, so it drew nothing at all, and the machine looked hung while it was
# merely unable to say so. What has to be checked is the PICTURE.
#
# Two things about it, and both matter. The picture is 640 by 400, which is the eighty column
# text mode - so the screen really was put back, from a mode that has no characters in it.
# And it holds the fault red, which says the message was drawn and drawn in a colour that can
# be read whatever palette the program had left behind.
python3 -c "open('$BUILD/blind.keys','wb').write(b'Crash blind\n' + b'\x00'*400)"
timeout 30 "$EMU" --fast --cycles 8000000 --keyboard "$BUILD/blind.keys" \
--screen "$BUILD/blind.ppm" --disk "$ROOT/Tests/build/disks/cosmos.img" \
"$BUILD/cosmos.bin" > "$BUILD/blind.out" 2>&1 || true
if [ -f "$BUILD/blind.ppm" ]; then
SIZE="$(head -c 20 "$BUILD/blind.ppm" | sed -n '2p')"
[ "$SIZE" = "640 400" ] \
&& result ok "a fault puts the screen back where text can be seen" "eighty columns again, from bitmap mode" \
|| result no "a fault puts the screen back where text can be seen" "the picture is $SIZE"
REDDISH="$(python3 - "$BUILD/blind.ppm" <<'PY2'
import sys
data = open(sys.argv[1], "rb").read()
parts = data.split(b"\n", 3)
pixels = parts[3]
# The red the machine wakes up with, which is the one the fault screen writes into the
# entries attribute one draws from. Counted rather than looked for once, so a single stray
# pixel of it could not pass for a message.
red = sum(1 for i in range(0, len(pixels) - 2, 3)
if (pixels[i], pixels[i + 1], pixels[i + 2]) == (0xD0, 0x40, 0x38))
print("yes" if red > 200 else "only %d red pixels" % red)
PY2
)"
[ "$REDDISH" = "yes" ] \
&& result ok "and says what happened in a colour that can be read" "the message is drawn in the fault red" \
|| result no "and says what happened in a colour that can be read" "$REDDISH"
else
result no "a fault puts the screen back where text can be seen" "no picture was written"
fi
echo
if [ "$FAIL" -eq 0 ]; then
echo "All $PASS video checks passed."