Make the manuals plain ASCII, and check that they stay that way

"All files must be plain ASCII, the user's tooling doesn't support Unicode"
is a standing rule of this repository. Nothing enforced it, so it drifted:
39 em dashes and an ellipsis had collected in the two manuals, every one of
them typed by something that helpfully substituted a nicer character. The
spaced em dash becomes a spaced hyphen, which is what the source comments
and both READMEs use for the same job.

Tests/docs.sh now checks every tracked file and says which line and which
character. Verified that it bites.

THE CHECK READS git ls-files NUL SEPARATED, and that is the whole reason
this went unnoticed. I ran the obvious shell version of this audit two
commits ago - a loop over $(git ls-files) - and reported the repository
clean. It splits on whitespace, so it looked for a file called "SplitBit",
failed into /dev/null, and found nothing wrong with either manual because it
never opened them. Both have spaces in their names.

A check that cannot see the files with spaces in their names is worse than
no check at all, because it answers.

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-21 14:16:54 -04:00
co-authored by Claude Opus 5
parent b2945e41c4
commit 460a687939
3 changed files with 63 additions and 35 deletions
+10 -10
View File
@@ -24,7 +24,7 @@ Any token beginning with a '0' is read as a numerical literal, so a malformed on
A string may be up to 255 characters. Each one is written down with a zero byte on the end, which is what lets a program find where it stops, and it means two strings written one after the other are not one longer string: there is a zero between them. A run of bytes longer than a string can hold has to be written as literals, or put there by the program itself while it runs. A string may be up to 255 characters. Each one is written down with a zero byte on the end, which is what lets a program find where it stops, and it means two strings written one after the other are not one longer string: there is a zero between them. A run of bytes longer than a string can hold has to be written as literals, or put there by the program itself while it runs.
**Strings belong in the Data Segment, and only there.** This is a Harvard machine: no instruction reads Program Memory, so a string put in the Program Segment could not be read by the program carrying it, and only the memory controller could reach it at all. The assembler refuses one rather than emitting bytes nothing can use. Single byte literals are a different matter and may go in either segment a table of bytes a program branches through is a reasonable thing to want in Program Memory. **Strings belong in the Data Segment, and only there.** This is a Harvard machine: no instruction reads Program Memory, so a string put in the Program Segment could not be read by the program carrying it, and only the memory controller could reach it at all. The assembler refuses one rather than emitting bytes nothing can use. Single byte literals are a different matter and may go in either segment - a table of bytes a program branches through is a reasonable thing to want in Program Memory.
The one exception to the single byte rule is #Align and #Reserve, whose numbers are never emitted as bytes and may go up to 0xFFFF. See Moving The Cursor Along. The one exception to the single byte rule is #Align and #Reserve, whose numbers are never emitted as bytes and may go up to 0xFFFF. See Moving The Cursor Along.
@@ -233,7 +233,7 @@ A line with a name and nothing after it declares the name and its number without
osExit 0d17 osExit 0d17
``` ```
The numbers are there because this is the case where a number has to be agreed. Everything else in a Vector Segment is numbered by the assembler, which can only see one program at a time and that is exactly the situation where it cannot help. See Numbers You Write Down below. The numbers are there because this is the case where a number has to be agreed. Everything else in a Vector Segment is numbered by the assembler, which can only see one program at a time - and that is exactly the situation where it cannot help. See Numbers You Write Down below.
Five names already mean something: Five names already mean something:
@@ -282,7 +282,7 @@ The software vector space is divided so the two kinds cannot meet:
You may give a number only in the pinned range. Below it belongs to the machine, and above it is where the assembler is allocating, so a number claimed there could be handed to something else in the same breath. You may give a number only in the pinned range. Below it belongs to the machine, and above it is where the assembler is allocating, so a number claimed there could be handed to something else in the same breath.
**Why the split exists**, because it is not obvious and the reason is a real mistake that used to be possible. When both kinds came out of one range, the numbers a program got for its own traps depended on what it had included: adding a line that included a file naming three services pushed every trap after it along by three, and a program that had *not* included that file was given the first number in the range which was a service. Installing its own handler there would have quietly replaced one. Now numbers that must agree are written down, and numbers that need not agree come from somewhere nobody else is looking, so a program's own vectors are its own regardless of how it was built. **Why the split exists**, because it is not obvious and the reason is a real mistake that used to be possible. When both kinds came out of one range, the numbers a program got for its own traps depended on what it had included: adding a line that included a file naming three services pushed every trap after it along by three, and a program that had *not* included that file was given the first number in the range - which was a service. Installing its own handler there would have quietly replaced one. Now numbers that must agree are written down, and numbers that need not agree come from somewhere nobody else is looking, so a program's own vectors are its own regardless of how it was built.
The assembler will refuse a number outside the pinned range, two names given the same number, a number on `Boot`, `SoftReset` or `BadOpcode`, whose numbers are the machine's, and a number that contradicts one the same name was already given. The assembler will refuse a number outside the pinned range, two names given the same number, a number on `Boot`, `SoftReset` or `BadOpcode`, whose numbers are the machine's, and a number that contradicts one the same name was already given.
@@ -491,23 +491,23 @@ wrote hello.bin: program 17, data 14, labels 2
Loading and running are separate commands in CosmOS, so the source file is the argument to `run`. Loading and running are separate commands in CosmOS, so the source file is the argument to `run`.
**Its output must be byte for byte what the host assembler produces from the same source**, and `Tests/native.sh` checks exactly that: it assembles `Programs/Examples/hello.asm` both ways and compares the files, then runs the one the machine built. This is the discipline SplitDisk and `sbfs.asm` already work under two implementations of one written specification, each one checking the other. "It ran" is not good enough for an assembler, because a binary with a label one byte out runs right up until it jumps into the middle of an instruction. **Its output must be byte for byte what the host assembler produces from the same source**, and `Tests/native.sh` checks exactly that: it assembles `Programs/Examples/hello.asm` both ways and compares the files, then runs the one the machine built. This is the discipline SplitDisk and `sbfs.asm` already work under - two implementations of one written specification, each one checking the other. "It ran" is not good enough for an assembler, because a binary with a label one byte out runs right up until it jumps into the middle of an instruction.
### How It Differs Inside: ### How It Differs Inside:
The host assembler reads every token of every file into one array and works on that. **That design cannot port and never could**: `cosmos.asm` alone is 56,047 bytes of source against 64K of Data Memory, and its token array would be several times that. So the native one streams its source through a 256 byte window, twice, and keeps only the label table between the passes. The host assembler reads every token of every file into one array and works on that. **That design cannot port and never could**: `cosmos.asm` alone is 56,047 bytes of source against 64K of Data Memory, and its token array would be several times that. So the native one streams its source through a 256 byte window, twice, and keeps only the label table between the passes.
Two passes are enough because **every length is known without resolving anything**. How many bytes a token comes to falls out of what the token is an instruction's from its shape, a value's is one, a string's is its characters and a zero and never from the value of anything named. So the first pass works out exactly where every label lands and the second never needs a fixup list. A forward reference stops being a special case and becomes the reason there are two passes at all. Two passes are enough because **every length is known without resolving anything**. How many bytes a token comes to falls out of what the token is - an instruction's from its shape, a value's is one, a string's is its characters and a zero - and never from the value of anything named. So the first pass works out exactly where every label lands and the second never needs a fixup list. A forward reference stops being a special case and becomes the reason there are two passes at all.
One thing is genuinely easier here than on a host. The host assembler searches a list of include directories, because a host has directories; **SBFS is flat**, so an include is a file name and there is nowhere else to look. One thing is genuinely easier here than on a host. The host assembler searches a list of include directories, because a host has directories; **SBFS is flat**, so an include is a file name and there is nowhere else to look.
### Building Applications: ### Building Applications:
`#Include` splices another file in where it stands, so the reader is a stack of readers: the current file's whole state goes aside, the new one opens, and the end of it pops the old one back. A file is included **once** including it twice is not an error, it just does nothing, which is what lets two libraries depend on a third. `#Include` splices another file in where it stands, so the reader is a stack of readers: the current file's whole state goes aside, the new one opens, and the end of it pops the old one back. A file is included **once** - including it twice is not an error, it just does nothing, which is what lets two libraries depend on a third.
`#Base` says where a segment is loaded, and a program that says so gets the SBEX loadable header instead of the SPBT boot one, with a `.sbx` name rather than a `.bin`. `#Reserve` and `#Align` lay down runs of zeroes; how many an `#Align` comes to depends on where the cursor has reached, which is why both passes keep a cursor rather than the second one keeping only a write pointer. `#Base` says where a segment is loaded, and a program that says so gets the SBEX loadable header instead of the SPBT boot one, with a `.sbx` name rather than a `.bin`. `#Reserve` and `#Align` lay down runs of zeroes; how many an `#Align` comes to depends on where the cursor has reached, which is why both passes keep a cursor rather than the second one keeping only a write pointer.
Names in `#Vectors` are read and numbered, pinned where the source pins them, so `SWI osPrintString` resolves. **What a name after `SWI` means is settled by what it follows**, not by anything about the name the Vector Segment may live in a file included further down and may not have been read yet. Names in `#Vectors` are read and numbered, pinned where the source pins them, so `SWI osPrintString` resolves. **What a name after `SWI` means is settled by what it follows**, not by anything about the name - the Vector Segment may live in a file included further down and may not have been read yet.
That is everything an application needs: That is everything an application needs:
@@ -535,7 +535,7 @@ A `#Vectors` line that names a **handler** says this program implements that vec
`Device` is named by the port it is plugged into, because that is what decides which vector it arrives through. `Device`, `Boot`, `SoftReset`, `BadOpcode`, `GuardViolation` and `BankFault` are matched **without regard to case**, the way mnemonics are: they are part of the language rather than names the programmer chose. `Device` is named by the port it is plugged into, because that is what decides which vector it arrives through. `Device`, `Boot`, `SoftReset`, `BadOpcode`, `GuardViolation` and `BankFault` are matched **without regard to case**, the way mnemonics are: they are part of the language rather than names the programmer chose.
**A declaration and an implementation are the same entry.** `services.asm` says a service is called `osPrintString` and has number 16; `cosmos.asm` says `osPrintString` is handled by `handlePrintString`. Both sides include the first file, so the name is met twice and the second time fills in the handler. That is what lets one shared file serve both a program that calls a service and the system that implements it and it is why the first pass declares and the second implements, a handler being an address and no address being known until every label has been placed. **A declaration and an implementation are the same entry.** `services.asm` says a service is called `osPrintString` and has number 16; `cosmos.asm` says `osPrintString` is handled by `handlePrintString`. Both sides include the first file, so the name is met twice and the second time fills in the handler. That is what lets one shared file serve both a program that calls a service and the system that implements it - and it is why the first pass declares and the second implements, a handler being an address and no address being known until every label has been placed.
### Self Hosting: ### Self Hosting:
@@ -551,13 +551,13 @@ wrote Asm.sbx: program 7533, data 4099, labels 555
Both come out **byte for byte identical** to what the host assembler builds from the same source. `make run-cosmos` puts every source file on the disk, so this can be done rather than read about. Both come out **byte for byte identical** to what the host assembler builds from the same source. `make run-cosmos` puts every source file on the disk, so this can be done rather than read about.
**The check that matters most is the third one.** A binary that matches could still have been built by an assembler wrong in some way this particular source happens not to exercise. So `Tests/native.sh` boots the CosmOS that CosmOS built and has *that* assemble CosmOS again and the second generation is identical to the first, down to the cycle count. It is a fixed point, which means the machinery has been through itself. **The check that matters most is the third one.** A binary that matches could still have been built by an assembler wrong in some way this particular source happens not to exercise. So `Tests/native.sh` boots the CosmOS that CosmOS built and has *that* assemble CosmOS again - and the second generation is identical to the first, down to the cycle count. It is a fixed point, which means the machinery has been through itself.
After that the host is a convenience rather than a necessity. After that the host is a convenience rather than a necessity.
CosmOS takes about 80 million cycles, which is eighty seconds of emulated time and under a second under `--fast`. Most of that is the label table: a straight walk of 475 names, several thousand times. Sorting it or bucketing it on the first character are both easy, and neither was worth writing before there was something to measure. CosmOS takes about 80 million cycles, which is eighty seconds of emulated time and under a second under `--fast`. Most of that is the label table: a straight walk of 475 names, several thousand times. Sorting it or bucketing it on the first character are both easy, and neither was worth writing before there was something to measure.
**The hardest thing it assembles is not the operating system, it is itself** 555 labels and 6,770 bytes of name against CosmOS's 475 and 5,881, and a larger output. That is what the buffer sizes are cut to. **The hardest thing it assembles is not the operating system, it is itself** - 555 labels and 6,770 bytes of name against CosmOS's 475 and 5,881, and a larger output. That is what the buffer sizes are cut to.
### What It Does Not Do Yet: ### What It Does Not Do Yet:
+24 -24
View File
@@ -477,7 +477,7 @@ A file's size is settled when it is made, because nothing can grow one afterward
### Saving Something Twice: ### 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**: 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 delete the old one
@@ -494,7 +494,7 @@ A create can be refused for want of a run long enough even on a disk with plenty
rename the temporary 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. **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.
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. 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.
@@ -545,7 +545,7 @@ readLine cuts a line short if it is longer than the buffer, and then reads the r
## What A Program May Ask The System For: ## 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. 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. 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.
@@ -584,17 +584,17 @@ 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. 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 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. 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. **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 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: ### 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. 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. 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.
@@ -625,7 +625,7 @@ readLoop:
... ; DP3 is how many of its bytes are the file's. ... ; 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. **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. `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.
@@ -643,9 +643,9 @@ readLoop:
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. 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/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 645 bytes, and includes nothing but the service names. `Programs/CosmOS/Apps/Files.asm` does the whole round trip - write, read, report, rename, delete - in 645 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. `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.
@@ -653,7 +653,7 @@ A handler is entered with the caller's registers exactly as they were, because a
### How A Service Answers: ### How A Service Answers:
The same thing that makes an interrupt safe makes a service mute. RETI restores every register from the frame, so whatever a handler worked out is thrown away on the way out which is exactly right for a device interrupting at a moment nobody chose, and useless for a service that was asked a question. The same thing that makes an interrupt safe makes a service mute. RETI restores every register from the frame, so whatever a handler worked out is thrown away on the way out - which is exactly right for a device interrupting at a moment nobody chose, and useless for a service that was asked a question.
A service answers by **writing into its own frame**, over the saved register, and letting RETI put it back. MVSD copies the Stack Pointer into a Data Pointer and the frame sits just above it, so returning a byte in Q is three instructions: A service answers by **writing into its own frame**, over the saved register, and letting RETI put it back. MVSD copies the Stack Pointer into a Data Pointer and the frame sits just above it, so returning a byte in Q is three instructions:
@@ -668,7 +668,7 @@ answer:
**Which registers a service may answer in is the convention CALL already has: Q and DP3.** A subroutine cannot hand back A, B or Data Pointers 0 to 2 because RET puts them back; a service *could* write over any of them and should not, for exactly the reason that list exists. A caller is entitled to find what it kept still there. **Which registers a service may answer in is the convention CALL already has: Q and DP3.** A subroutine cannot hand back A, B or Data Pointers 0 to 2 because RET puts them back; a service *could* write over any of them and should not, for exactly the reason that list exists. A caller is entitled to find what it kept still there.
**Only the handler itself can do this.** The offsets are from wherever the Stack Pointer is, and a CALL moves it by ten so a routine called by a handler that tried the same thing would be writing into its own return address. The poke belongs inline, next to the RETI. **Only the handler itself can do this.** The offsets are from wherever the Stack Pointer is, and a CALL moves it by ten - so a routine called by a handler that tried the same thing would be writing into its own return address. The poke belongs inline, next to the RETI.
A service that has nothing to say does nothing, and the caller's registers arrive back untouched. That is worth knowing from the other side too: a service cannot corrupt a register by accident, only by deciding to. A service that has nothing to say does nothing, and the caller's registers arrive back untouched. That is worth knowing from the other side too: a service cannot corrupt a register by accident, only by deciding to.
@@ -698,11 +698,11 @@ A program that only wants to be run needs nothing here and says version one. A p
Each vector is four bytes: the address of the slot in the vector table, then the address to put in it, both most significant byte first. Naming the slot rather than the vector number means the loader does no arithmetic and does not have to know where either vector table begins, and one entry can be a software or a hardware vector without saying which it is. Each vector is four bytes: the address of the slot in the vector table, then the address to put in it, both most significant byte first. Naming the slot rather than the vector number means the loader does no arithmetic and does not have to know where either vector table begins, and one entry can be a software or a hardware vector without saying which it is.
**A version two file is refused by a loader that cannot install them.** That is the point of the version rather than an inconvenience of it. A program whose handlers were quietly dropped would load, run, and then go wrong somewhere with nothing to connect the failure back to loading a game waiting for keys that no longer arrive. Failing once, at load, with a reason, is worth more than running. **A version two file is refused by a loader that cannot install them.** That is the point of the version rather than an inconvenience of it. A program whose handlers were quietly dropped would load, run, and then go wrong somewhere with nothing to connect the failure back to loading - a game waiting for keys that no longer arrive. Failing once, at load, with a reason, is worth more than running.
**Whoever installs them takes them back.** A vector points into the program that supplied it, so one left in the table after that program has gone aims an interrupt at whatever occupies those addresses next. CosmOS keeps its own copy of what a program brought, puts them in when the program is run and not when it is loaded, and restores what was underneath them when the program gives the machine back. Restoring, rather than clearing: a program is allowed to install a handler over one the system was already using, and when it goes, what it covered up has to come back rather than become a hole. **Whoever installs them takes them back.** A vector points into the program that supplied it, so one left in the table after that program has gone aims an interrupt at whatever occupies those addresses next. CosmOS keeps its own copy of what a program brought, puts them in when the program is run and not when it is loaded, and restores what was underneath them when the program gives the machine back. Restoring, rather than clearing: a program is allowed to install a handler over one the system was already using, and when it goes, what it covered up has to come back rather than become a hole.
`Boot` in a loadable program fills in the entry field, since that is what it means, and is not installed as vector 0 where the machine starts is not a loaded program's business. Without one, a program begins at the first byte of its code. `Boot` in a loadable program fills in the entry field, since that is what it means, and is not installed as vector 0 - where the machine starts is not a loaded program's business. Without one, a program begins at the first byte of its code.
### Where A Program Says It Lives: ### Where A Program Says It Lives:
@@ -726,7 +726,7 @@ Whoever does the loading keeps its own code and data below the addresses the loa
## Programs That Come With The System: ## Programs That Come With The System:
`Programs/CosmOS/Apps` holds what the shell can load. 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: `Programs/CosmOS/Apps` holds what the shell can load. 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 | | Program | What it is for |
| --- | --- | | --- | --- |
@@ -743,7 +743,7 @@ Whoever does the loading keeps its own code and data below the addresses the loa
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. 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: `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 > load Snake.sbx
@@ -759,14 +759,14 @@ bank 01
**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. **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. `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 | | `x [addr]` | Sixty-four bytes, as hex and as characters |
| `d [addr]` | Eight instructions, disassembled | | `d [addr]` | Eight instructions, disassembled |
| `a addr` | Assemble instructions, until a line that is just a dot | | `a addr` | Assemble instructions, until a line that is just a dot |
| `s addr b b ` | Put those bytes there | | `s addr b b ...` | Put those bytes there |
| `b program\|data\|n` | Which bank to look at | | `b program\|data\|n` | Which bank to look at |
| `g addr` | Go there | | `g addr` | Go there |
@@ -791,13 +791,13 @@ A program and its data, both entered by hand, calling a system service and retur
**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. **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. `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. 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. 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. **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: **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:
@@ -807,7 +807,7 @@ code from 2000, free from 2607
data from 1000, free from 1367 up to the stack 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: 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 > s 8000 26 48 D1 00 26 49 D1 00 26 0A D1 00 18 12
@@ -827,9 +827,9 @@ There are no breakpoints yet, and `g` does not come back. The machinery for both
### The Editor: ### 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. `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 took it from 4,941 bytes to 1,983 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. 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 took it from 4,941 bytes to 1,983 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.
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. 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.
+29 -1
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# Checks the manuals against the code. # Checks the manuals against the code, and the repository against its own rules.
# #
# Documentation goes stale quietly. An instruction added without a table row, or a count # Documentation goes stale quietly. An instruction added without a table row, or a count
# in a heading that nobody updated, is wrong in a way nothing notices until somebody # in a heading that nobody updated, is wrong in a way nothing notices until somebody
@@ -28,6 +28,34 @@ am = read("SplitBit Assembler Manual.md")
asmc = read("Source/Assembler/assembly.c") asmc = read("Source/Assembler/assembly.c")
util = read("Source/Assembler/Assm-util.c") util = read("Source/Assembler/Assm-util.c")
# ---- Every tracked file is plain ASCII ----
#
# A standing rule of this repository, and nothing enforced it, so it drifted: 39 em dashes
# and an ellipsis had collected in the two manuals, all of them typed by something that
# helpfully substituted a nicer character.
#
# GIT LS-FILES IS READ NUL SEPARATED, and that is not fussiness. The obvious shell version
# of this check - looping over $(git ls-files) - splits on whitespace, so it looked for a
# file called "SplitBit" and reported the repository clean while both manuals had drifted.
# A check that cannot see the files with spaces in their names is worse than no check.
import subprocess
tracked = subprocess.run(["git", "ls-files", "-z"], capture_output=True).stdout
for name in tracked.split(b"\0"):
if not name:
continue
path = name.decode()
try:
text = open(path, encoding="utf-8").read()
except (UnicodeDecodeError, OSError):
continue
for number, line in enumerate(text.split("\n"), 1):
odd = sorted({c for c in line if ord(c) > 127})
if odd:
problems.append("%s line %d is not plain ASCII: %s"
% (path, number, ", ".join("%r (U+%04X)" % (c, ord(c)) for c in odd)))
break
# ---- Every instruction has a row, and every row is an instruction ---- # ---- Every instruction has a row, and every row is an instruction ----
# #
# A mnemonic begins with a letter, which is what keeps the offset and size columns of the # A mnemonic begins with a letter, which is what keeps the offset and size columns of the