94bdf713566a17ab9ac0b7485f92afb7b79db342
69
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
dfc9d9ef7f |
A score is written and a tune is what the machine reads
The convention, at the user's asking, and it is the one this project already has everywhere else: a .asm is written and a .sbx or a .bin is what the machine loads. A .score and a .tune are the same pair one subject along. It is not only tidiness. I read the user's splash.tune as a compiled tune yesterday, dumped its header, and got a tick of seven and a half million cycles and ninety seven patches out of what was plainly a text file. Different names make that a thing nobody has to notice. AND THE SPLASH WAS SILENT ON THE DISK THAT MATTERS. The play disk mirrors every .asm and puts every app, and nothing on it put a compiled tune - so make run-cosmos and make run-voyager both booted a Lander that read /splash.tune, did not find one, and held the logo in silence. Only the test disk had it, because I had added it there and stopped. The makefile now compiles Programs/Tunes/splash.score with TuneC and puts the result on the disk, and the mirror's prerequisite list learned about .score files so that changing the music rebuilds the disk. That is the same failure the mirror was built for, in a file type the mirror did not know about yet: tune.asm went into Examples once, the image was not remade, and it was simply not there. Measured on the real disk: music from 0.9 s to 9.6 s. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
fb672d7761 |
A manual for writing tunes
The fifth document, and the one somebody would want first if they had a piece of music and this machine. What a tune is, how to write one, what the compiler refuses and why, how to build and play one, and the bytes it holds for anything that would rather write one itself. The last of those matters more than it looks: Tests/maketune.py writes tunes without going through TuneC, and the suite checks the two agree byte for byte. A format with two implementations needs a specification they are both held to rather than one of them being the specification. The refusals get a table of their own with the reason beside each, because every one of them is something the PLAYER cannot notice - it has no names, no lengths, and no way to tell "no starting instrument" from "instrument nought" once a tune is loaded. Documented as reasons rather than as rules, so that somebody meeting one knows what it saved them from. docs.sh now settles the manual against the compiler both ways: every directive TuneC takes has a row, and the limits the manual quotes are the compiler's own #defines. Verified with break.sh - a directive removed from the manual and a limit raised in the compiler are both caught. One that was not caught first time and should not have been: removing the #Use row from one table left it documented in the other, which is the check being right and my break being wrong. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
2808688fa1 |
TuneC: a written tune becomes the bytes the player reads
The compiler, and the last thing the ladder was waiting for. A tune names
its instruments, writes sequences of notes and durations, and gives each
voice an order list of sequence names - which is where repetition comes
from, since a phrase played four times is written once and named four
times.
#Tick 0d125000
#Patch Oboe oboe.patch
#Voice 0d0 Oboe
#Sequence Verse
0d64 0d4 0d67 0d4 0d72 0d8
#Order 0d0
Verse Verse Ending
"#" is a directive and ";" is a comment, exactly as in SplitBit assembly
and in the shell's scripts, and numbers are written the way the assembler
writes them. One rule across the machine rather than a third dialect -
and the rule earned itself immediately: the first tune I wrote said
"#Voice 0" and was refused, correctly, for a bare number.
WHAT IT REFUSES IS EVERYTHING THE PLAYER CANNOT NOTICE. The machine has
no names, so it cannot say a sequence does not exist. It has no lengths,
so it cannot say the voices will come apart four bars after the mistake.
A duration of nought is counted down to 255 and held, which sounds like a
hang rather than an error. And by the time a tune is loaded, "no starting
instrument" and "instrument nought" are the same byte - so the user's
ruling, that a voice with a part and no instrument is an error, can only
be kept here.
SoundPatch gains --blob, writing the same table as raw bytes. It stays
the only thing that reads soundThing's JSON: a second program parsing
that format is a second opinion about what a patch means, and the seam
between two opinions is where the LFO bug lived for a fortnight. Patches
are found beside the tune and then on a -I path, the way an include is.
THE TEST IS THAT TWO IMPLEMENTATIONS AGREE. maketune.py lays the fixture
out by hand and TuneC compiles a written source, and the suite checks
they match byte for byte - the discipline SplitDisk and sbfs.asm are held
to, for the same reason: either alone is only self-consistent. The
fixture predates the compiler, so this is also TuneC checked against
something written before it existed. Four more checks cover the four
refusals.
Also: the SoundPatch binary was tracked, alone among the six tools, and
.gitignore lists every other one. Untracked, and TuneC added beside it.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
|
||
|
|
de765267c5 |
Lunar Porter's crash is a designed sound now, not a guessed one
Crash1.json, made in soundThing and converted by SoundPatch. Two noise oscillators fifteen cents apart, a filter opened 11.6 kHz by the modulation envelope, and the LEVEL shaped by that same envelope rather than by the amplitude one - which is exactly the freedom the routed level was for, and not a thing the engine could express a week ago. It peaks at 10,501 and is gone in about six tenths of a second, inside the eight tenths the pieces are in the air. The guessed one wandered between 1600 and 8200 and sounded like a drain. The patch predates the gate field, so the program's own trigger write is what makes it a one-shot - which is the argument for having put it there rather than leaving it to the patch. LowFuel.json is here too, saved from the newer soundThing with the gate and LFO mode in it, and is not wired to anything yet. Both JSONs live beside the tables they produce. The build has never needed soundThing and still does not, but a sound can now be regenerated, or simply read, without going to find the editor first. |
||
|
|
a8b6b09a59 |
SoundPatch: design a sound where it can be heard, then convert it
The bang was guessed at directly in bytes and came out as a low gurgle, which is what guessing at bytes gets you: a cutoff of 40 looks small and is 57 Hz, so the filter sweep ended almost shut. Voyager's sound device is soundThing's voice engine with the editor taken off, so a patch designed in soundThing - where there is a screen, a keyboard and a pair of ears - makes the same sound here. What differs is only how it arrives. SoundPatch converts one into the other. Every parameter the device takes is a documented function of a natural value and all of them invert: times are squared into four seconds, cutoff and rate are exponential, depths and detune are centred on 128. It writes a table of a count and that many parameter and value pairs, and playPatch hands it to the device - so every sound after this costs a table and a call rather than forty lines of its own. Two things the conversion has to say out loud. soundThing has no field for the level routing, because there it is always the amplitude envelope, so the table says so explicitly - a channel keeps its patch between notes and a leftover from a previous one would otherwise be carried in. And a field the tool does not recognise STOPS it: a patch format that has moved on would otherwise produce a table that quietly means something else. THE BUILD DOES NOT DEPEND ON IT. soundThing lives in its own repository and is not needed to build anything here; the tables are checked in and the tool is for when a sound is being changed. Lander's crash now plays Kick808 as a stand-in until a bang is designed for it, and the difference is the point: the hand-guessed patch wandered between 1600 and 8200 for eight tenths of a second, and this decays 3492, 2743, 2037, 1515, 1040, 614, 87, nothing. The docs check caught the tool count in two manuals, which is what it is for. |
||
|
|
925388c2f2 |
Keep the personal disk three starts back
Copied before every start of the machine, and kept several deep rather than copied over one file. ONE BACKUP TAKEN AT EVERY START IS WORSE THAN NONE. The way a disk is lost is that something goes wrong - and the very next thing anybody does is start the machine again to see how bad it is, which is exactly when a single backup gets overwritten by the wreckage. Three deep means the damage has to happen and then be started past three times before the copy that would have helped is gone. AND IT IS NOT GUARDED BY A CHECK THAT THE DISK STILL LOOKS RIGHT, because no such check can be written. The disk that went missing this week was a perfectly valid and perfectly empty filesystem: the format had succeeded, and there is nothing to look at that says a disk has lost anything. That is the whole reason to keep the old ones rather than to judge the new one. The backups sit outside clean's reach like the disk itself. A backup a rebuild deletes is not one. The message the disk prints when it is first made said "nothing in this makefile will touch it again", which stopped being true the moment this was added. It now says written to, and says where the copies go. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
3e48e9690d |
Put a disk of your own in drive 1
make run-voyager and make run-cosmos now attach Disks/personal.img as the second drive. It is made the first time it is wanted and then left alone: a rule with no prerequisites, so make never looks at it again. NOT UNDER build/, and that is the whole point. Everything else in this repository is made from source and can be thrown away without losing anything, so 'clean' empties build/ without a thought - and a disk of your own that a clean deletes is not a disk of your own. It is the one place here where something MADE ON THE MACHINE can live, which starts to matter the moment there are tools on it that make things. Disks/ is in .gitignore for the same reason: what you make on the machine is yours and not the repository's. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
e52d8d84f8 |
Collapse the two makefiles into one
Programs/makefile is gone and everything it did is here. The split was not paying for itself: nothing at this level ever ran that file, so it rotted, and all three bugs found in it this week share that one cause. It named two source files that had been renamed months earlier and failed outright. The disk did not depend on the tree it mirrors, so a new file silently was not on it. And the disk was in no default target, so 'make clean' threw it away and 'make' did not bring it back - which is what prompted this. The platform and the system stay separate, as TARGETS rather than as files: 'make SplitBit Assembler' builds the machine and its tools, 'make cosmos' and 'make disk' build the system, and somebody who wants to write their own system can ignore the second group entirely. That boundary is now one make enforces, which the directory boundary never did - Tests/makedisks.sh has always reached across it to build its own fixtures. 'make' now builds a bootable disk as well as the tools, because a machine with nothing in the drive does not do anything and the first thing anybody wants after building this is to watch CosmOS come up. It costs half a second: the whole system, twenty six apps and the native assembler assemble in less time than the emulator links. Two things the merge needed that the split did not. Assembling now takes an order-only dependency on the assembler, which came free when you had already built the tools before changing directory. And the mirror is told which directory to walk: it was ".", meaning Programs/, and left alone it would have meant the whole repository - the C sources, the tests and the manuals mirrored onto a disk for an 8-bit machine. The disk comes out at 188 files where it was 189. The one that went is Programs/makefile, which was itself being mirrored onto it as /Source/makefile. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
adefce975b |
Let the reset button reach a machine that has stopped
The gesture rebooted CosmOS and could not reboot picture.bin, which is the case it was added for. picture.asm ends in HALT, and a halted machine runs no instructions - so nothing ever reached the code that notices a reset, because a reset is noticed BETWEEN INSTRUCTIONS and there are none. It only ever worked because CosmOS was still going. Which is backwards: a machine that is not going anywhere is exactly the one worth restarting, and it is the one that cannot hear a request by itself. The restart is lifted out of the run loop into machineTakeReset, and the window asks every frame whether the machine is running or not. NAMED AS EMULATOR MAGIC, because it is. There is no reset line on this machine and no keyboard controller to assert one; the window reaches in and sets the same flag the machine port sets. When those are designed, a keyboard controller will have to see the gesture and pull reset regardless of what the CPU is doing - which is the property that matters and the one a port write can never have, since a port write needs a program willing and able to make it. The shape of that is already visible here: asking every frame rather than leaving it to the machine to notice is what a line does. A restart now clears the cycle limit as well, since a machine stopped for reaching one is another thing somebody would press the button over. The three existing reset tests still pass, and they are the ones that matter: they exercise the same restart through the machine port. What no test reaches is the gesture itself, which exists only when there is a window. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
8c6ed62044 |
Reset on a gesture the host has no opinion about
Control, Shift and R. It was Control, Alt and Delete, which brings up a shutdown prompt rather than reaching the machine - and no amount of asking will change that. IT IS A SECURE ATTENTION KEY. Every serious operating system reserves it so that it always reaches the system and never an application, precisely so a program cannot imitate a login screen. On Windows an application cannot see it without a kernel driver; on Linux the desktop takes it. That is not an oversight to work around, it is the same guarantee a reset button wants being enforced one layer further down, and there is no call this program could make that would win the argument. It is unavailable for exactly the reason it seemed right. So the requirement list grew a third item that was not obvious until it bit: the gesture has to be one the host has no opinion about. Control and Shift with a letter is about as free as a combination gets - not window management, not a virtual terminal switch, and not a shortcut any desktop claims by default. It keeps the other two properties: plain F12 and plain Delete stay free for software, and three keys are not pressed by accident. If some platform does send a character for it, nothing comes of that either. Whatever arrives is in memory that is about to be thrown away. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
0afead00de |
Make the reset a gesture rather than a key
Control, Alt and Delete, which was F12. Two reasons, and the first one is about hardware rather than about the emulator. ON REAL HARDWARE THIS IS NOT A KEY AT ALL. A Voyager has a button on the case, and what a window has instead of a case is a gesture standing in for one. So it must not be a key software might want - a machine with a keyboard has function keys on it and something will eventually have a use for F12, which would have meant taking it away again later, from programs already written to expect it. And it must not be reachable by accident. Restarting throws away everything in memory, which is the same data loss Escape was one keypress from causing, arriving by a different door. Three keys together are not pressed by mistake. It has meant this since 1981, so it is also the one gesture nobody has to be told the meaning of - and Delete stays free for software, since the gesture is all three or nothing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
f7657081be |
Put a reset button on the case, and stop Escape closing the window
ESCAPE WAS A BUG I LEFT. This machine sends Escape to the console like any other key, and Raylib closes a window on Escape unless it is told not to - so a program reading keys could be ended by one of them, taking whatever was in memory with it. SetExitKey(KEY_NULL), and it is a byte again. F12 is the reset button. A button on the case rather than a key the machine can see: nothing sends a function key to the console, so nothing can be surprised by one. It does what writing MACHINE_RESET does, which is that the machine starts the way it started - the boot chain runs again and finds whatever the disk now says to run. Which is what makes a bare metal program escapable. Once puts a demo in front of the next start and deletes the request before jumping, so a demo that has taken the whole machine is one keypress from the system coming back, instead of closing the window and opening it again. IT HAD TO REACH A MACHINE THAT IS WAITING, and that took two more things. A reset is acted on between instructions, and a machine blocked on a key is part way through one - so the button would have set a flag that nothing ever came along to notice, in exactly the situation a reset button is for. The wait ends now: the console is told its input is over, which it is for a machine about to stop existing. And the reset puts the console's input back - nothing pushed back, no line half gathered, and not at the end of input. That was already wrong before the button existed: a reset after the input ran out left a console that had run out afterwards, so a machine could be restarted once and then never typed at again. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
3da020898c |
Write down what the machine costs, before it has to be worked out again
Three measurements that would otherwise be re-derived from scratch in three months, put where a reader will meet them rather than where somebody has to know to look. The README's cost model section says what a nominal 1 MHz actually means: about 270,000 instructions a second, since the average instruction costs 3.72 cycles. The same program therefore takes 3.72 times the wall clock it did when a cycle was an instruction - nothing got slower, the number got honest, and the number it replaced described no machine anybody could build. Beside it, the two figures that put that in proportion: the emulator runs 195 million cycles a second in --fast, and CosmOS assembling CosmOS is 654 million of them, which is eleven minutes at a megahertz, 3.3 seconds at --fast, and six and a half seconds at a hypothetical 100 MHz. That last is pessimistic rather than hopeful: it assumes hardware overlaps nothing, which is the same conservative reading the controller's cost model takes. The CosmOS README gets what that means for its assembler, which is superlinear in what it reads: 1,383 cycles a byte for a four kilobyte source, 6,290 for a hundred kilobyte one. IT IS NOT THE DISK, and the number that says so is worth keeping - 654 million on a disk carrying the whole source tree against 653 million on a flat one with a sixth as many files. The suspected cause is walking a table of some nine hundred labels once per reference, suspected rather than measured. Nothing is being done about it, and the reason is written down too: development happens with the host assembler. But faster hardware buys a constant factor and does not change the shape of the curve, so the program that forces this is not CosmOS - it is the first one twice its size. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
0852666e73 |
Mirror the source tree onto the system disk
A list of files in a makefile goes stale the moment somebody adds a program and forgets to name it, and what they forgot is invisible until they go looking for it on the machine. So SplitDisk gained a mirror command and the disk rule is one line: putting a file where the others live is now the whole of putting it on the disk. EVERY FILE GOES THROUGH put AND EVERY DIRECTORY THROUGH mkdir. That is the point of it - mirror adds a walk and no filesystem code at all, so anything the format refuses here it refuses everywhere, in the same words. What is new is the walk, and the walk is what the six checks in Tests/disk.sh are about: that it goes all the way down, that it leaves dotfiles and named directories behind, and that a name too long stops it. REFUSED RATHER THAN SKIPPED, because a disk quietly missing a file is the exact failure a mirror exists to prevent. Which meant four sources had to be renamed - a directory entry holds 22 characters and they were 23, 23, 24 and 29: 16bitSegmentedSieve.asm -> 16bitSieve.asm 16bitSegmentedSieveModern.asm -> 16bitSieveModern.asm consoleInterruptTest.asm -> consoleInterrupt.asm controllerWriteTest.asm -> controllerWrite.asm The test names in the manifest are unchanged, since those are identifiers and every recorded result is filed under them. Only where the source lives has moved. The entries are sorted before anything is written. readdir hands them back in whatever order the host filesystem feels like, and a disk image that comes out different from one run to the next is an image no test could compare against another. The disk grew from one megabyte to four and from 192 directory entries to 1,024. The sources are 2,850 blocks and the mirror filled the old directory on its first run, which is a thing that should not need thinking about again. The Tests fixture disk is deliberately NOT mirrored. It is a controlled fixture with known contents, and the shipped disk is the one meant to be useful; they want different things. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
ff4b025058 |
Make the cursor blink while the machine is waiting, and show how the palette works
THE CURSOR DID NOT BLINK, and the reason is worth stating: it blinks on the machine's clock, and the machine's clock had stopped. A console waiting on a key stops the CPU, so no cycles passed, so the phase never moved - and the one moment somebody is looking at a cursor is the moment they are being asked to type. Waiting is now charged as IDLE CYCLES, which is what they were built for: a machine stopped on a device is not using memory, the same distinction WAIT makes, arrived at from the other direction. And the devices are told as it happens rather than when the instruction finally finishes, because a display controller does not stop blinking because the processor is waiting on a keyboard, any more than a disk stops turning. A keyboard file can now say NOTHING happened. A zero is a byte no keyboard sends, so it is free to mean "a moment went by with nobody typing" - which is the commonest thing behind a window and the only thing a file otherwise could not express. That unlocked the whole waiting path: three checks that the cursor is lit, then dark half a second later, then lit again, which is what blinking is. And Programs/Examples/colours.asm, because the palette had nowhere a newcomer could read it. It prints the sixteen pairs, prints each one again turned inside out, and then CHANGES ONE by writing three bytes into the palette - so the difference between using the colours a machine wakes up with and choosing your own is visible in one program. Its header explains what a cell is, what the attribute nibble does, why palette entries are four bytes rather than three, and why video memory has to be reached through the controller. The manual now says where the palette lives and points at it. SplitLint found a redundant RSTA in the example, which was worth acting on rather than suppressing: the zero was already in A from the mode write two lines up, and saying so in a comment teaches that SETD does not touch A, which is a thing worth knowing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
761c11a66b |
Add run-voyager, and make running depend on the disk
The jitter was a stale disk. An image built before the escape sequences came out still had the old Snake on it, which sends ESC [ H every frame: the console swallows the escape, because it is below the font's first character, and then draws "[H" as two ordinary letters. So every frame began two characters to the right and one line further down than the last, and the board walked down the screen. Not timing at all. WHAT IS ON A DISK IS WHATEVER WAS BUILT WHEN THE DISK WAS MADE, and a machine whose console has changed will start that image quite happily. That is the compatibility break we chose when the parser came out, and it is fine - but it should not be a puzzle, so both run targets depend on the disk rather than merely using it, and both READMEs say why. run-voyager boots the same disk on the machine with a screen. It existed only as EMU=../Voyager in front of run-cosmos, which is not a thing anybody should have to know. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
773b0f8add |
Put CosmOS on the screen without changing a line of it
The console is now a display controller as well as a port: it owns a font, keeps a cursor, handles newline, carriage return, backspace and wrapping, and scrolls. That is an ordinary kind of chip - it is what a video terminal's character generator did - and it is the reason this rung needed no changes to CosmOS at all. CosmOS already writes bytes to port 0x00. It writes to BOTH the screen and standard output, which is deliberate. A machine with a screen and a serial line is an ordinary machine, the emulator's standard output is that serial line, and one console drives both. It is also what keeps all 165 recorded results passing under Voyager, and what makes --screen work on the plain SplitBit: there is one console and it drives everything it has. Scrolling moves the video device's origin and no memory. The row arriving at the bottom is cleared because the map is a ring and it holds what was there 128 rows ago; the rows going off the top are not, and that is a hundred rows of scrollback nothing had to keep. The test reads the register back rather than looking at the screen, because a console blitting rows instead would look identical and cost twelve percent of a frame for every line printed. The font is vendored from Hatchet-GPU with a note saying where it came from, since that repository is not part of this one. 135 glyphs in ASCII order, which is the thing that makes it worth keeping - PETSCII's whole inconvenience was that its order was not ASCII's, so a machine using it needed a translation table in front of every string. Here the machine subtracts 32. It is stored one bit a pixel and expanded into tile memory at reset: 1,088 bytes against 16 kilobytes. Voyager gets a keyboard. A window has no standard input, and a machine blocking on it inside a frame would stop drawing and stop answering, so a front end with a window installs a hook that the console calls while it has nothing: it keeps the window alive and hands back a key. The hook has to tell "nobody has typed yet", which happens sixty times a second, apart from "the window has gone", which is the end of input - one value for both would have made the first keystroke look like a closed machine. In line mode the console echoes what it is given, because there is no terminal behind a window to do it and that was always the terminal's job. Tests/video.sh grew from 14 checks to 26, half of them about the console rather than the device: those programs ask the video device for nothing and write bytes to port 0x00 like every SplitBit program always has. Verified by breaking two things - removing the scroll failed exactly the two checks about scrolling, and removing the cursor advance failed exactly the three that depend on it. Two video checks had quietly depended on palette entry 0 being black, which stopped being true the moment a machine woke up able to show text. They now set what they are about to look at, and a new check pins the waking state itself. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
83623a3df3 |
Give the Voyager a screen
A tile engine on ports 0x30 to 0x3F, bringing one bank of video memory registered the way the disk's buffer is. The CPU writes cell indices and the device turns them into pixels, which is the whole reason a screen is affordable at a megahertz: a frame is 16,667 cycles, a full 320 by 200 picture is 64,000 bytes, and a 40 by 25 map is 2,000. A program that changes two cells writes four bytes. The cost of a screen becomes the number of cells that changed rather than the number of pixels on it. Which makes colour depth free, so the tiles are eight bits: an 8 by 8 cell is 64 pixels and each picks independently out of 256 colours, with no per-cell limit of the kind that made a Spectrum two and C64 multicolour four. The low nibble of a cell's attribute is ADDED to every index in its tile, sixteen at a time, so a tile drawn in 0 to 15 appears in any of sixteen schemes without a second copy in tile memory - and a tile wanting all 256 leaves the nibble at zero and gets them. Neither use costs the other anything. Two decisions are arithmetic rather than taste, and both come from the machine having no multiply. A map row is a page whether the mode fills it or not, so a cell address is the row number as the high byte and the doubled column as the low byte with no arithmetic at all; otherwise every cursor move on a 40 column screen would cost a row-times-40 in software. And a palette entry is four bytes rather than three, so entry n is at n times four, a shift. THE MAP IS A RING and the Scroll register says which of its 128 rows is on top. Scrolling moves a register and no memory: blitting a 40 by 25 screen up one line is 1,920 bytes inside one bank, which is twelve percent of a frame even with the controller widened, and a program printing one page would spend six frames shuffling memory. It is now one port write - and the rows that scrolled off are still there, which is where a terminal gets scrollback it never had. The device is part of the machine rather than part of the window. It renders into a buffer that is a pure function of video memory, so the same program draws the same picture with nobody watching; Voyager puts that buffer on the glass and decides nothing. Both binaries take --screen, which saves a PPM when the machine stops, and that is what makes a screen checkable on a host with no display at all. Tests/video.sh checks fourteen named behaviours rather than comparing a recorded image, because a recorded image would say "something changed" and leave which of the palette, the tile, the attribute, the map or the scroll register broke to be found by hand. Verified by breaking three things in turn: the additive nibble failed exactly one check, the scroll origin exactly two, and moving every cell one pixel sideways exactly the four about placement. Tests/docs.sh could not count past nine, which is how a suite of ten scripts reported itself as wrong for the wrong reason. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
e3ef25e3b3 |
Split the machine from its front end, and add Voyager
The Segan Voyager is the same SplitBit with a screen and a speaker instead of a terminal, and this is the rung that makes there be two of them at all. Everything that is actually the machine - the CPU, the controller, the devices, the run loop, the reporting - moves to machine.c, and each front end brings one file of its own. emulator.c is now sixty lines of argument handling and a three line loop. The machine runs in SLICES rather than to completion, because that is the cut a window needs: run a slice, present a frame, run another. A terminal runs slices until the machine stops. Both loops are three lines, which is why the cut is there rather than anywhere else. At this stage Voyager's window is empty. There is no video device yet and inventing a temporary way to draw would mean building something to throw away. PLAIN MAKE STILL WORKS WITH NO GRAPHICS LIBRARY. Raylib is probed by compiling and linking against it rather than by looking for a file, because a header with no library behind it passes a file check and then fails at link time. Where it is missing, make says so once and builds everything else - the machine, the assembler, the disk tool, the linter and the whole suite. A project about a small understandable CPU should not need OpenGL to run its tests. That nearly broke here: make strict globs Source/Emulator/*.c, so it would have tried to compile voyager.c and failed on precisely the machines the split exists to support, and this machine has Raylib so nothing would have caught it. Tests/voyager.sh runs the WHOLE MANIFEST through Voyager and holds it to the recorded results SplitBit is held to. Not that the two look alike: that one satisfies every recording the other does, byte for byte, exit status included. It reuses run.sh, which now takes the machine from SPLITBIT_EMULATOR, rather than keeping a second copy of the runner that would drift. Voyager not being built is not a failure - it says so and passes. Verified both ways. Made Voyager print one extra line, and 114 of 165 failed: exactly the tests that run the emulator, with the 51 assemble-only and xfail cases correctly untouched. Removed the binary, and the script skipped. Built with HAVE_RAYLIB=no, and everything else still built and checked clean. --headless is taken out of the arguments in voyager.c rather than in the shared parser, which should not learn about a window only one binary has. It exists so the suite can run this binary at all: a front end that could only be exercised by a person looking at it would be a front end nothing checks. loadFile takes a const char * now, which it always should have. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
4c3eac8d9c |
Widen the memory controller's path to sixteen bits
The controller now reaches bank memory two bytes at a time, so a transfer whose source, destination and length are all even moves two bytes a cycle between banks and one within a bank - twice what each was. A 256 byte block between banks falls from 257 cycles to 129. Alignment is required all three ways because a word is read at an even address and written at an even address; an odd anything would mean shifting bytes across word boundaries to line them up, which is a different design. A misaligned transfer falls back to the byte a cycle it cost before, so nothing already written got slower. THE CPU DOES NOT CHANGE. It still sees eight bits, a Data Pointer still addresses a byte, and no instruction means anything different. This is a peripheral getting faster, which is why it is worth doing now rather than after more is built on top of it. The rule is deliberately visible rather than smoothed over: aligning a buffer costs nothing and halves what moving it costs, and a cost a program cannot see is a cost it cannot avoid. Tests/cycles.sh is new, and is the test the Test Manual has always said this kind of change would need - run.sh strips the cycle count from every recorded result, so nothing else in the suite can see any of this. It pins the RATE rather than a total: each case runs twice from programs whose instructions are identical but for the byte written to the Command port, once asking for the transfer and once for GuardOff, which costs nothing beyond the port write. The difference is the transfer and nothing else. Verified by disabling the widening, which failed exactly the three aligned cases and left the five misaligned ones passing. The Programming Manual gains a section saying what a transfer costs, which it never said at all - it only promised a transfer does not wait, which is a different claim and could be read as promising it is free. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
d6bc416698 |
Write the Test Manual, and make the suite check it
The test system had grown to seven scripts making five genuinely different kinds of claim, and nothing said which was which. A recorded transcript and a byte-for-byte comparison against a second implementation both print [ok ] and are worth wildly different amounts, so the fourth manual exists to say so: what each script can and cannot answer, why every determinism rule is there, how to add a test, and - the part written nowhere else - where the suite is blind. That last section is the reason for the document. Three buffer overruns into adjacent variables were all found by a person using the machine and none by the suite, the sanitizers cannot see them because emulated Data Memory is one legitimate host array, and there is no second opinion about the CPU at all. A document listing only strengths teaches the wrong lesson. The bullets describing each script move out of the README, so docs.sh now reads the manual for them, and five more numbers in it are settled from the source rather than trusted: the shape of the manifest, the xfail count, how many fixture disks makedisks.sh builds, how large the lint baseline is, and the tool count in either document. Each of the new checks was broken on purpose and watched to report before being kept, which is the discipline the manual itself argues for. Also drops the stale "70 instructions" from instructiontable.py's docstring. There are 72, and a number that carries no meaning is better removed than corrected. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
79727044b7 |
Reboot, and the machine device that makes it possible
Until now the only way to restart was to stop the emulator and run it again, which meant the one thing the machine could not do was the thing Once was written for. The loop now closes without leaving it: > Once /System/Boot/bare.bin next start: /System/Boot/bare.bin, once > Reboot starting again stage two just this once: /System/Boot/bare.bin bare metal: no system, just this Writing 1 to port 0x13 asks the machine to start over. A PORT RATHER THAN A SERVICE, because a reset has to work when the system does not: something only askable through SWI would be unavailable in exactly the case that wants it most, and a program that owns the whole machine has no system to ask. It is device class 0x04, in the range kept for the machine rather than among the peripherals, because it is not one - it is not attached to anything and cannot be unplugged. WHAT A RESET REPEATS IS HOW THE MACHINE STARTED. Named an image, the emulator places it again; named none, the ROM is shadowed again and reads the disk. Anything else would mean a reset changed what the machine IS, which is the one thing a reset must not do. Both are tested. Taken between instructions, because a device cannot restart the machine from inside the instruction that asked: the CPU is part way through a step and its state is not yet anything a reset could leave behind consistently. The disk stays attached and keeps everything written to it - that is what warm means. The vector table is cleared, which is the one deliberate departure from leaving memory alone: a vector points into whatever installed it, and after a reset that program is not running, so a handler left behind would aim an interrupt at an address belonging to something gone. It is the argument CosmOS already makes at exit, applied to the machine. Reboot is 45 bytes, most of them the word it prints. |
||
|
|
7b28f48f52 |
Once: start something else on the next start, and only that one
A program that owns the whole machine had nowhere to run. It cannot be started from the shell, because starting it means there is no shell, and pointing boot.cfg at it means a machine that keeps starting it - which is a poor place to find a mistake in something written five minutes ago. Once writes /System/Boot/once.cfg, in the same format as boot.cfg and read with the same routines, because a second format for one setting would be a second format. The loader reads it before boot.cfg and DELETES IT BEFORE IT JUMPS, which is the only moment there is: after the jump the loader does not exist. Consumed by being read rather than by working, so a one shot that hangs cannot hang twice - the request is gone before the image ran, and the next start reads boot.cfg like any other. THE BOOT STATE IS NOT TOUCHED, and the first version got that wrong. It marked the start the way any other start is marked, and then every successful bare metal boot reported that it had never arrived - because a program with the whole machine has no filesystem to clear a mark with and is doing nothing wrong by not having one. Found by running it: the image printed its line and the next start still said the last one did not. Three disks, each a start further along, so none of the tests depends on another having run. The loop is closed on the machine now: write it in Edit, assemble it with Asm, ask for it with Once, restart, watch it own the machine, and the system comes back without being asked. |
||
|
|
c8c9f0b363 |
SRET: a handler answers the way a subroutine does
CALL saves A, B and Data Pointers 0 to 2 and nothing else, which is exactly why Q and DP3 are how a subroutine hands something back. An interrupt saves all of it, so a service with an answer had to reach into its own frame and un-save two fields by hand: MVSD.2 DPUP.2 0d02 ; the saved Q, by an offset it had to know STA.2 RETI Thirty places in CosmOS did that. Every one knew the frame's layout by heart, and all thirty would have gone quietly wrong the day the frame gained a field - the same duplicated fact this project keeps being bitten by, except duplicated into thirty places AND into the CPU. SRET is 0x76, in the seat the block split left for it. It is RETI's frame with RET's rule applied: A, B and DP0 to DP2 come back, the saved Q and DP3 are dropped, and the Interrupt Flag is restored from the frame - only that bit, so carry survives a service the way it survives a call, and there is one rule rather than two. RETI stays exactly as it was: a hardware handler has nothing to say and must leave no trace. CosmOS is 10,969 bytes against 11,122, and no handler knows a frame offset. TWO MISTAKES WORTH RECORDING, both mine, both caught by tests. The first conversion matched STA.2 with a regular expression that did not allow a trailing comment, so it ran past the end of one handler and into the next. The second understood the pattern and still got it wrong: the old frame write carried the answer from A into the saved Q slot, so simply deleting the write left Q holding whatever it happened to hold. Services that answer by calling something were fine - Q already had it - and services that set A directly silently reported success for every failure. cosmosCwd is what noticed, by saying "cannot go there" about a directory that was there. Sixteen handlers move the answer into Q now. Seven MVQA went with it. They copied Q into A so the frame write could carry it; SRET puts A back, so they moved a value nobody would ever read. |
||
|
|
dc74149321 |
B4: the disk remembers whether the last start arrived
The loader marks the superblock before it hands over and the system clears the mark when it reaches its prompt, so a system that crashes on the way there leaves it set. The loader finding it still set next time is how a machine that will not start says so to the only thing in a position to do anything about it. Without that, pointing boot.cfg at something that dies before the shell is a machine that can never be told anything again - the shell is the only way to change the file, and the file is what stops the shell from starting. Three states rather than two, and the third is the one worth having: 0 settled the last start arrived; use the configuration 1 trying handed over, and nothing came back to say it got there 2 fell back a try failed and the fallback was used, until settled With only 0 and 1 the machine alternates for ever: fall back, reach a prompt, clear the mark, retry the broken system, crash, fall back. State 2 stops that. A system known not to start is not tried again until somebody says the situation has changed. REACHING THE PROMPT IS A DELIBERATE THRESHOLD. It is not a claim that the system works - a shell can be reached by something broken in every other way. It is the point where a person can type, which is exactly what the fallback exists to give back: anything wrong past there is fixable from the prompt and nothing wrong before it is fixable at all. The routines live in sbfs.asm because both the loader and the system read and write this byte, and two pieces of code with their own idea of where a byte lives is what this format has two implementations and a byte for byte comparison to avoid. And the trap this system documents in its own manual caught me anyway: the first version handed the state back in A, which CALL restores, so every read got whatever the caller happened to be holding. It comes back in memory now, and the comment says why. Three disks differing only in the state on them, so the tests read as three consecutive starts of one machine while none depends on another running. |
||
|
|
546f336823 |
Configuration files, and boot.cfg as the first of them
One setting to a line: a key, a space, the rest of the line is the value. A semicolon starts a comment. The format was noticed rather than designed - textSplit already cuts the first word off a line and leaves the rest, and textSame already insists two strings end together, so reading a setting is those two routines and a loop. It is also what the shell reads, which makes a configuration line a command line the machine reads instead of a person typing one. The format was chosen by asking what the BOOT LOADER could manage, because it is the worst case in every direction: a few kilobytes, no operating system to report to, and if it fails the machine does not start. Two formats would be worse than one and the loader cannot have the richer one. CONFIGURATION IS ADVICE. A missing file, a missing key, an unusable value, a line too long to read: all of them mean use the default and none is a failure. BUT QUIET IS NOT SILENT - a setting somebody meant, which did not take effect, says so. That was the user's addition and it is the better rule: the default alone leaves the only symptom being that the machine did not do what somebody asked. So two routines. cfgGet reads and says nothing, because reading three settings should not report one bad line three times. cfgCheck reads the file once and reports, and is handed the caller's list of keys - whether a key means anything is the only part of this a shared reader cannot judge. /System/Boot/ holds the boot files, and stage two reads boot.cfg for what to start, with a fallback to try if it does not work and a name compiled in for when the file says nothing. THE TEST FOUND A REAL BUG, and it is the interaction I would not have thought to look for. First-match-wins met an empty value: a file with system system /System/Boot/bare.bin matched the first line, handed back nothing, and the machine tried to start a file with no name while a good setting sat underneath. An unusable value is an absent one - which is what "configuration is advice" says, and this is where it earns its keep. cfgBare starts an image with no operating system in it at all, which is what loading an ordinary boot image buys: a program wanting the whole machine is a file like any other, chosen the same way the system is. Three disks differing ONLY in boot.cfg, so each is a test of the file rather than of the machinery under it. |
||
|
|
54ff7196c9 |
Stage 0: the emulator carries the ROM, so a disk is enough
./SplitBit --disk system.img stage two CosmOS > No boot image named. The emulator shadows its built in stage one into Program Memory - boot vector included - and the CPU then does exactly what it has always done: reads the boot vector and starts where it points. NOTHING ABOUT THE CPU CHANGED to make a machine that starts itself, which is what picking shadowing over a mapped ROM bought. The ROM is generated from Programs/Boot/stage1.asm by the makefile rather than committed beside it, because a copy of a program kept next to the program is a copy that goes stale. That makes the assembler a real dependency of the emulator, which it always sort of was and now says so. od and awk rather than xxd, which is not everywhere, or python, which the README does not ask anybody to install in order to build this. loadROM is loadFile given bytes instead of a path: both go through one reader over an fmemopen stream, because a ROM is a boot image and there is no reason for the machine to have two ways of understanding one. Naming an image still works and is what every other test here does. That path is not a shortcut to apologise for - placing memory from outside is a real thing real machines allow, and it is a debugger. The help says so now. No image and no disk is the one case with nothing to run, and it says that rather than printing a usage message about a missing file. run.sh gained a "rom" mode which hands the emulator a disk and nothing else. The source column still names stage1.asm, because that is what is IN the ROM: assembling it there says the thing the emulator carries is a thing that still assembles. |
||
|
|
c312853f8e |
The machine starts itself
stage two CosmOS > saved it read it back, 22 bytes: Stage one hands over to stage two out of a boot slot; stage two mounts the filesystem, finds /System/cosmos.bin, takes the image apart and places its code, its data and its vector table, and jumps to the entry point the vectors named. Nothing placed memory for it. What it loads is an ORDINARY BOOT IMAGE, the same SPBT file the emulator has always been handed. That was the user's call and it is the whole trick: a second stage that loads the machine's normal image format is not a boot-specific mechanism, so bare metal SplitBit stops being a special case. A program wanting no operating system under it is just an image, written under CosmOS like any other, and startable because it is a file. Three things in it worth knowing: - THE ENTRY POINT IS CAUGHT ON ITS WAY PAST. Program Memory cannot be read back, so the boot vector cannot be looked up after being installed; the vector loop notices the one addressed at 0xFC00 and keeps it. - A missing "VEC" is not a fault. An image written before vectors existed simply ends after its data, and then the entry point is zero, which is what every such image has always relied on. - Feature flags that are set mean an image asking for a machine this may not be, and the honest answer to a request that cannot be understood is to refuse rather than to run it anyway. The test records that the system WORKS afterwards rather than that it started. A loaded program running is what says the vector table arrived, because a program reaches the system through SWI and nothing else; the file written and the directory entered say the filesystem and the console came up with it. A second disk has a boot slot and nothing to start, and says so rather than jumping somewhere. |
||
|
|
d07b23f90b |
Rung 2: the machine starts itself off a disk
Stage one exists and works. It is 330 bytes of program and everything it knows is a thing that will be true forever: which port the disk is on, that a SplitBit disk begins with its own name, and where two numbers sit in that first block. Not what a file is, not what a directory is, not that SBFS has versions. It reads the live boot slot into Program Memory, jumps to the first byte, and prints one character and halts if there is nothing there. It is an ordinary boot image for now, so the whole chain runs on machinery that already exists and the emulator has not been touched. Nothing about it changes when it moves into ROM except who puts it in memory. SplitDisk gained "boot" to write a slot and "bootslot" to choose one, kept apart on purpose: writing a slot and starting from it are different decisions, and joining them would make every write a commitment. A slot is always written WHOLE, because one still holding the tail of what was there before is one whose contents depend on its history, and stage one reads all of it without knowing where the file stopped. Three recorded tests, and the pair is the point: two disks differing only in which slot the superblock names, with payloads that say different things. One prints "booted" and the other does not, so this is a test of CHOOSING a slot rather than a test that some bytes were read. The third boots a disk with no boot area and gets the one character a ROM has room for. Eight more host checks, including that a slot is padded whole. Two things worth recording. The first draft used #Align to put the scratch buffer at 0x8000 and produced a 33K file - thirty two kilobytes of zeroes in something meant to be a ROM. It is an address, not storage, which is exactly what the assembler's own scratch map exists to say. And SplitLint caught the second in code written an hour after the baseline that catches it. In the blit set-up, RSTA writes a source address of zero and then RSTA writes a bank number of zero - two unrelated quantities that are equal by accident, in the most safety critical file in the repository. It is marked with a reason rather than removed. |
||
|
|
612bd1b97c |
B1: a boot area on the disk, reserved by arithmetic that was already there
The first rung of booting from disk. A boot area is blocks between the superblock and the directory that the filesystem never allocates and never sees, and NOTHING WAS ADDED TO RESERVE THEM: both implementations work out the first usable block as directoryStart + directoryBlocks, and directoryStart has always been a field rather than a constant. Formatting with the directory moved up reserves everything below it. Neither allocator changed, on either side. Two new superblock fields in bytes that were reserved: bootBlocks at 14, per slot, and bootSlot at 16. A disk made before this has zero in both, which reads as "no boot area" - true, and the same shape as the version two parent field, where the value an older disk already held was the right answer without conversion. TWO SLOTS, ALWAYS. A boot slot is raw blocks with no entry to rename, so the write-a-temporary-and-rename ordering that protects every file cannot protect it, and a machine interrupted while updating its only slot would not boot at all - the one failure on this disk with no way back. Writing the slot that is not live and then moving one byte makes that a machine that boots what it had before. bootBlocks and directoryStart say the same thing from two sides, so a disk where they disagree is refused rather than guessed at, as is one naming a slot that does not exist. Checked where it matters: the HOST formats a disk with a boot area and the MACHINE fills it, then the reserved blocks are compared against zero. The machine's allocator is the one that had no idea any of this was happening, which is what makes that the check worth having. Six host checks besides, including both halves of the superblock disagreeing. |
||
|
|
0a2965bc63 |
Name the rules, say when there is nothing to say, and hold a baseline
Four things SplitLint wanted, and they build on each other. EVERY WARNING NAMES ITS RULE, in brackets at the end the way a compiler names the flag that produced it. Twelve rules, listed by --help. That makes the other three possible: suppressions can name one rule and leave the line honest about the others, the harness can assert on a rule's identity rather than on the wording of its message, and --machine can print one tab separated line per warning - file, line, rule, message, help - so nothing downstream reads prose. This file's own output was parsed with regular expressions three times in one day before it had a shape to rely on. A CLEAN RUN SAYS SO: No style warnings: 121 files checked against 12 rules. It used to exit in silence, which does not tell you it found nothing - it tells you nothing at all, and from outside the two are identical. A MARKER THAT SILENCES NOTHING IS ITSELF REPORTED, as dead-suppression. An exception that outlived whatever made it necessary is the thing the required reason exists to prevent, and naming the wrong rule now gets you both the warning you meant to silence and a note that your suppression is doing nothing. AND THE CORPUS IS HELD TO A BASELINE. Sixty one warnings are left in it deliberately and nothing stopped a sixty second. Tests/lint-baseline.txt records how many of each rule each file should produce, so a new one fails make test while the sixty one stay quiet; confirmed by adding an INIA 0d0 to Say.asm and watching it name the file, the rule and the count. It counts per file and rule rather than recording line numbers, because line numbers would churn the whole baseline whenever anything was inserted above a warning - the same reason cycle counts are stripped from recorded output here. ./Tests/lint.sh --bless records it again. One thing to know for next time: the rule name was inserted before the line number at all twenty one call sites, and the signature was changed to match rather than the twenty one call sites being fixed. (path, rule, line) reads no worse than (path, line, rule) and one edit has fewer ways to go wrong than twenty one. |
||
|
|
c146d98588 |
Stop the linter recommending a change that a faster helper would break
SplitLint knew that CALL restores A, B and Data Pointers 0 to 2, so a pointer set before a call is still set after it. That is true, and it made the tool give advice that was correct today and unsafe to take. Of the 178 redundant SETDs it found across the corpus, 122 were redundant ONLY because of that restore - the shape is everywhere, because it is how a helper is given its arguments: SETD.0 SbfsBlock SETD.2 SbfsFileStart CALL sbfsSetWord SETD.0 SbfsBlock <- flagged Removing that last line is right until sbfsSetWord is reached with RCAL, which restores nothing - and that is not hypothetical, it is what RCAL was added to this machine for, measured at close to halving the assembler's memory traffic. The failure would also be silent from the linter's side: it forgets everything across an RCAL, so it would stop reporting while the removals stayed removed. So a claim now ends at any call, for pointers and for registers, the way a claim about carry already did. 257 warnings become 127, and the redundant SETDs 178 become 54 - which is exactly the number an independent count of "no CALL in between" had arrived at separately. The fixture gained a SETD and an INIA repeated across a CALL, which must stay quiet, and the harness fails with the old behaviour put back. Two mistakes worth recording: the new expectations first pointed at the LABEL above the repeats rather than the repeats, which passes for free because nothing ever warns about a label; and the block landed in the middle of another check's comment, leaving that comment describing the code below it instead of its own. |
||
|
|
8f4cc5878d |
Repair a table cut in half, and measure the numbers that had drifted
The README's emulator options table was split by forty lines of prose: two rows, then the whole discussion of the cost model, then five more rows with no header above them. Markdown renders that second half as something other than a table, so three of the seven options were not being shown as options. The rows are back together and the prose follows them. Four numbers had gone stale, in three different ways, and none was noticed: - "Five more scripts run alongside it" - there are six, and lint.sh had no bullet saying what it was for. - "rebuild all three tools" - there are four. - Files.asm quoted at 645 bytes in two places; it is 665. - Edit quoted at 1,983; it is 1,996. The last two are the most quotable sentences in the CosmOS README and the least likely to be rechecked by hand: the programs kept being made better and the sentences about how small they are stayed where they were. So docs.sh measures all four now. It counts the scripts in Tests/ that are not the driver or the disk builder and checks the README says that many and explains each one; it counts what the makefile's all target builds and checks the tool count in both phrasings, which took two attempts because one sentence says "the four tools" and the other "all four tools"; and it assembles every app the CosmOS README quotes a size for and compares. Each check was confirmed by making the fact wrong and watching it fail. WAIT also added a second kind of cycle this morning and the cost model section still described only one. It now says what an idle cycle is, why the two are counted apart, and what that distinction is FOR - a machine that slept through a slow disk and one that spun on it take the same elapsed time and print the same characters, and only the split tells them apart. The duplicated sentence about pipelining is gone; it was said twice, eleven lines apart, in nearly the same words. |
||
|
|
2b079324ae |
Add SplitLint, and let it be told when something is deliberate
SplitLint reports valid assembly that has a shorter direct expression: zero loads that could be RSTA or RSTB, Q moved through the stack where MVQA would do, self-cancelling push and pop pairs, assignments overwritten before use, unreachable fallthrough, one-byte pointer moves that could be INCD or DECD, a branch to the label directly below it, a SETD reloading an address the pointer already holds, and branches whose carry is known. Its model is deliberately local and conservative: every label and every directive forgets all known state, so a claim only ever lives inside a straight-line region. It knows the calling convention - CALL forgets DP3 and keeps the rest, RCAL and SWI forget everything - and it shares assembly.o with the assembler, so an added opcode cannot leave it holding a private copy of the instruction table. 260 warnings across the corpus, of which three were wrong in the way that matters: branchTest.asm and interruptFlagTest.asm exist to check that a branch whose carry is known behaves correctly, so a diagnostic saying the outcome is known is exactly right and exactly unwanted. A line whose comment says "splitlint: <reason>" is now not reported on. THE REASON IS REQUIRED and a bare marker is refused, because a suppression nobody explained outlives whatever made it necessary. Suppressed warnings are not counted, so --fatal-warnings does not fail on one, and the number of them is printed at the end so the claim is visible rather than silent. Tests/lint.sh checked a TOTAL: twenty three warnings expected, twenty three found. That number stays right while the thing behind it goes wrong - a rule that stopped firing while another fired twice would pass, and so would a rule reporting at the wrong line. It now checks which warning came out and at which line, that nothing else came out, and that the four lines meant to stay quiet did. Confirmed by breaking one rule's message and watching it name that rule: the old assertion passed the same sabotage, because the warning still fired and the count never moved. Written with the user while I was away; my part is the suppression mechanism, the harness rewrite, and the three marks in the test programs. |
||
|
|
c3188ed657 |
Seventy becomes seventy one: a machine that can wait
HALT is terminal - stepCPU returns at once when the Halt Flag is up, so a halted machine does not execute, service devices, or take an interrupt - and that has to stay true, because every test ends with a halt and "halted" is how a program says it has finished. The consequence was that SplitBit had no way to wait at all. Every wait was a spin, and a spin is bus traffic: 11.5% of Type over a 14K file on a disk of ten thousand cycles, after read-ahead had already hidden three quarters of the latency. WAIT is 0xFE, one byte, no operands, sitting under HALT where the instruction that almost stops the machine belongs. Three decisions in it: - A line already standing means there is nothing to wait for, so WAIT does nothing. That is what makes test-then-wait race-free. - Any line ends the wait, masked or not, so a program can sleep on a device it has no handler for and read its status afterwards. Masking says who answers a request, not whether it happened. - A line that wakes the CPU without being dispatched is taken down by the WAIT. Left standing it would be found by the next WAIT, which would return at once - the program would spin exactly as before while looking as though it slept. Waiting is NOT a Status bit, and that is the trap avoided rather than a gap: Status rides into the interrupt frame and comes back out, so a machine interrupted mid-wait would return from its handler still waiting, and wait again for what it had already been given. An internal field instead. Idle cycles are counted apart from bus cycles and the halt line says so when there are any, which is what makes the difference observable at all - with the line-clearing removed the total moves by ONE cycle, 20,100 against 20,099, and only the idle half changes, halving to 9,976. A test on totals could never have seen it. Tests/terminal.sh asks that question, being the file for things a recorded output cannot see, and fails with the clear removed while "both reads finished" still passes. Three collisions, all found by building it: - 0xFE was the assembler's "not an instruction" sentinel. getOpcode now answers a negative NOT_AN_OPCODE, which is outside the range of every possible answer instead of inside the unused part of it. - 0xFE was also what faultTest and faultResumeTest executed to provoke a fault. They now use 0xFD and say why, because they did not fail when it became an instruction - they HUNG, having started sleeping instead. - Keys.asm has had a label called "wait" for a year, and mnemonics are matched uppercased. What that reported was "Branch without label" at the BRQ thirty lines away. The assembler now refuses a label that is already an instruction, at the label, by name; every instruction added takes a word out of the space of label names, so this will happen again. |
||
|
|
d4cba36c5e |
Devices that take time, and a filesystem that waits for one
The disk's status has always had a bit meaning "still going", and the header beside it has always said to honour it. Nothing did, because nothing could: the host finished the transfer inside the instruction that asked for it, so the bit could never be seen up and asking about it was asking about something that cannot happen. --disk-cycles gives it a latency. The command is still checked at once, because a refusal is not work - a block that is not there fails before any head moves - but the transfer is remembered and done when the machine has run that far. Until then the buffer holds the block BEFORE this one. That last part is the point. A program that does not wait gets the wrong bytes rather than an error, which is the failure the bit exists to prevent and the one that would never have shown up. With a latency of two thousand, CosmOS could not even mount: sbfsMount reads block zero and looks straight at the buffer. deviceTick is the general shape rather than a disk feature. Called once per instruction with the machine's clock, it lets anything whose moment has come finish - which is what a display that refreshes, or a port that waits on the host, would want in exactly the same way. The filesystem watches the bit now, in one small routine reached with RCAL. That is not decoration: what it hands back is the settled status in A, and CALL puts A back the way it found it, so an ordinary call cannot carry the one thing this exists to carry. Two bytes of Stack rather than ten, in a routine that runs on every block the machine ever touches - the first place in the system where the new call is the right one rather than merely a cheaper one. The manifest takes a @N after a disk, the way it already takes :ro, so a test can ask for a slow one. cosmosSlowDisk lists a directory at two thousand cycles a block and gets the same listing as everything else, which is the whole assertion: a filesystem that did not wait would print nonsense rather than fail. Zero is the default and every other test runs at it. What waiting costs, on a directory heavy run: 229k cycles at zero, 275k at five hundred, 415k at two thousand, 1.16M at ten thousand. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
e0cf0a9a25 |
Charge the memory controller for the memory it moves
A blit cost ten cycles, which were the five port writes that set it up. The quarter of a kilobyte that moved cost nothing, and no hardware moves a quarter of a kilobyte for nothing. BANKS ARE SEPARATE MEMORIES, AND THAT IS WHAT SETS THE RATE. A move between two of them can overlap its read and its write - fetch the next byte while the last one is stored - so it settles at a byte a cycle. A move within one bank cannot, and costs two. A fill has nothing to read and costs one whatever the banks are. The odd cycle on each is the pipeline filling. That is not a modelling choice so much as a reading of the structure the machine already has: a Program to Data blit is inherently twice the rate of a Data to Data one, and it is legible why. Measured: 256 bytes is 297 cycles across banks and 518 within one, both including the instructions that ask for it. WHAT IT TAUGHT, which was not what I expected. Charging for movement costs the native assembler 0.4 per cent and costs directory work 13.4. The assembler reads a block and then thinks about it for a long time, so the move is amortised into nothing; the filesystem reads a block in order to look at it and does nothing else in between. So the case for a blitter that runs alongside the CPU is weaker than it sounds. Concurrency pays when there is other work to do during the transfer, and the place that spends its time moving memory is exactly the place with nothing else to do - it blits a block precisely so that it can read it. What that workload wants is a FASTER controller, not a concurrent one: a wider data path halves the wait, and the machine is waiting either way. Video is the case that would still want concurrency, since a frame can be moved while the next one is worked out. That is an argument about software nobody has written yet, and it is now an argument with numbers on the other side of it. The byte at a time port is charged too, for the byte it moves beyond reaching the port. Nothing polls CTRL_STATUS, so the transfer stalls whoever asked for it, which is the conservative reading and the one the software already assumes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
f1e5cc46f6 |
A cycle is an access to memory, not an instruction
cycleCount used to tick once per instruction, so RSTA cost what SETD cost and a CALL moving ten bytes of Stack cost what a branch cost. No machine anybody could build works that way, and the emulator's job is to be the thing the hardware is designed against. Every touch of memory now goes through one of four accessors that charge for it: fetching an opcode, fetching the bytes after it, reading or writing Data Memory, and reaching a device port. One access, one cycle, nothing overlapped. The accessors exist so the cost is counted where the access happens rather than in a table of per instruction costs kept somewhere else - a table like that is a second copy of what the code does, and the two drift. The run loop spends a budget of cycles instead of running a count of instructions, so the emulated rate means something: an instruction costs what it touches, and a batch ends when the cycles are gone. What the numbers say now: RSTA 1 and SETD 4, being one byte and four. LDA 3, DPUA 2. CALL and RET together 24, RCAL and RRET together 8, because the first pair moves twenty bytes of Stack and the second moves four. The average SplitBit instruction costs 3.72 of these, measured over the native assembler assembling a program. And the measurement that prompted all of this: converting the filesystem's hottest leaf routine to RCAL is 3.1 per cent cheaper on a directory heavy workload. The old model said 0.0, which is what a model that cannot see memory traffic must say about a change that is nothing else. Three tests moved. settle() strips the cycle count from recorded output, so nothing should have churned - but it was anchored to the start of a line and replCalculator's last output has no newline on it, which leaves the halt message mid line where the pattern never reached. Not anchored any more. The two Life programs are bounded by a cycle count because they never end, and that number was rescaled from 3,000,000 to 11,200,000 - the same amount of work at 3.72 cycles to the instruction. Nothing about either program changed. No limit reproduces the old output exactly, because the cut now lands elsewhere in a frame, so they are recorded again rather than tuned to match. Whether hardware overlaps a fetch with the end of the previous instruction is left open on purpose. This is the conservative model; pipelining is a decision to make while drawing the hardware, not one to inherit from an emulator. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
19ab36a201 |
The assembler can say where everything ended up
-S writes every label and the address it was given, in address order. Nothing else knows that. A program on the disk is bytes; the monitor can disassemble it but has no idea what any of it is called. So counting which addresses a program calls says a great deal and names nothing - the answer arrives as a column of numbers and somebody works out by hand which routine each one is inside. It was deferred when the native assembler was planned, as a listing and symbol dump nobody needed yet. Finding out where the assembler spends its time is what needed it: the top six call targets were addresses until this existed and are numStep, numCompare, tokGet, srcNext, numAddByte and clsSameName with it. Sorted by address rather than by name, because the question asked of it is always "what is at this address". Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
af0360128b |
Sixty four instructions becomes seventy
The six settled back on the twenty fourth, built now. RCAL and RRET are a call that puts nothing back. CALL restores A, B and Data Pointers 0 through 2, which costs ten bytes of Stack and is why a subroutine here can only hand anything back through Q, DP3 or memory. RCAL costs two and restores nothing, which is what a short leaf routine wants and is unsafe in exactly the way the name says. They are a pair because the frames are different sizes: returning from one through the other walks the Stack to somewhere that was never a return address. That was the user's correction to the original proposal, which had a raw call and no raw return. DPUA and DPDA offset a Data Pointer by A; DPUW and DPDW by A and B together, most significant first. DPUP and DPDN take a byte written into the program, so moving a pointer by something just worked out meant storing it and loading it back. Down as well as up on symmetry grounds, which was also the user's call - the argument against it came from counting uses in a corpus written under the constraint. The opcodes sit where they belong: 0x16 and 0x1E immediately below CALL and RET, and 0x4E through 0x51 at the end of the Data Pointer family. All six fit shapes that already existed, so instructiontable.py needed only set membership and both machine side copies of the table regenerated from it unchanged. Checked at every level it exists at: the emulator runs them, the host assembler encodes them, the monitor disassembles all six with the right lengths, and the assembler that runs on the machine builds a program using them byte for byte identically to the host - and that program runs. The recorded test measures what the two calls COST as well as what they put back, because an RCAL that quietly did what CALL does would still return to the right place. It does not survive that: returned through RRET, it hangs. docs.sh can read a two word number now. The count of instructions taking a Data Pointer went past twenty, and the pattern only allowed one word, so the check would have reported that the manual had stopped saying it rather than that the number was wrong. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
da91a36d92 |
D4: the machine makes directories too
mkdir and rmdir are the machine's own now, and a file goes where its path says rather than always in the root. A disk can be organised without the host tool touching it. Everything below the surface works in terms of a directory and a name rather than a path. sbfsWalkParent splits the last name off, walks the rest, and hands back the two - and the separator stays on the end of the head, which is what makes one rule cover every kind of path: "/x" leaves "/", which is the root; "x" leaves nothing, which is where the machine already is; and "A/x" leaves "A/", which is neither and needs no special case to say so. Saving works in those two as well, and had to. The careful order a save uses - make a temporary, write it, delete the original, rename the temporary - only works if the temporary is made in the SAME directory as the file, because the rename at the end changes a name and does not move anything. Renaming to a path naming a different directory is refused for that reason, rather than quietly being a lie the disk goes along with. Three things this cost, all found by running it: mkdir Apps/Deep made /Apps/Apps. The leaf was worked out into SbfsWanted and then the head was walked - and walking goes through sbfsPathNext, which puts every name it meets into SbfsWanted on the way past. The head's last name landed exactly where the leaf was. It has somewhere of its own now. rmdir took a directory with something still in it, which is the one failure the whole design is arranged to prevent. Looking for children clobbered DP2 and rebuilt it from the buffer and the entry count with the subtraction the wrong way round, so the pointer walked off the end of the block and found nothing. The comparison goes through a CALL now, like the two beside it, and DP2 comes back on the entry because a RET puts it there. SplitDisk's "in use but not reachable from the root" line is what caught it. Refusing a name longer than twenty two used to read the twenty third character of a shorter one, which is somebody else's string. It is measured now. Tests/agree.sh is new and is the gate this rung was for: the same disk built twice, once with SplitDisk and once with CosmOS, compared byte for byte. The two share no code and only a written specification, and every field one writes and the other only reads is checked there and nowhere else - which entry a thing lands in, which block, what a directory's unused fields hold, the version, the free count. It caught a wrong parent immediately when that was broken on purpose. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
78e9eef472 |
D1: teach SplitDisk directories, without moving a byte
SBFS version two adds directories out of space each entry had already set aside: two of the four reserved bytes become a parent, and one of the seven spare flag bits says an entry is a directory. The entry is still thirty two bytes, so it still divides two hundred and fifty six and still never straddles a block, and nothing in the block layer knows anything happened. A directory is an entry with no blocks. That is what keeps the flat array of entries the whole allocation map, which is the property the format is built on: with files laid down contiguously, every block is inside some entry's range or it is not, and an entry with no range is in nobody's way. There is still no allocation table to consult and none to keep right. THE PARENT IS AN INDEX PLUS ONE, so zero means the root. A version one disk has zeroes in those bytes, and "in the root" is exactly where every file on a flat disk is - so a version one image is already a valid version two image, with nothing to convert and no tool to convert it with. A disk is at the lowest version that describes what is on it. format makes a version one disk and mkdir is what raises it, so everything built here stays readable by a reader that has never heard of a directory right up until it really does have one. That is what lets this land before the machine knows anything: the whole existing suite passes untouched. The tool gains mkdir and rmdir, and list, put, get and delete take paths. list also now reports entries used against entries available, because a disk has two ceilings and the entry one is the one nobody notices until it bites. rmdir refuses a directory with anything in it, and that is not politeness: parents are entry indices, a freed index gets handed out again, and the children of a removed directory would reappear inside whatever took its place. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
bd94059c08 |
Put the screenshot at the top of the README
The picture does what three paragraphs of prose were doing, and does it first. It has the whole loop in one frame: Asm.sbx assembles hello.asm into hello.sbx, the shell loads that and runs it, and then the monitor disassembles what is at 0x2000 - which is the program the machine wrote for itself at the top of the same screen. The caption points at that last part, because it is the thing an eye slides past. The disassembly is not of an example; it is of the output. Two notes on getting it right rather than nearly right. The file is Media/CosmOS.png and not the cosmos-screenshot.png it was described as, so the link would have been dead on arrival - checked by looking rather than by typing what I was told. And the caption first said the disassembled program was written "six lines earlier", which was wrong by about four; it now says "at the top of the screen", which cannot drift. docs.sh already covers it: the link check treats an image like any other relative link, and moving the file out makes the suite say so. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
9690ecf21f |
make sanitize runs the whole suite, not half of it
It built all three tools with the sanitizers and then ran run.sh and terminal.sh only. So SplitDisk was compiled sanitized and never once exercised, and native.sh - which drives the assembler and the emulator harder than anything else here, through two self-hosting builds and a fixed point - was skipped entirely. Those are precisely the places worth watching: block and tail arithmetic on disk images, and buffer indexing in two assemblers, one of which was written this week. disk.sh, native.sh and docs.sh now run under the sanitizers with the rest. Timed first rather than guessed at: 0.41s, 6.05s and 0.14s, against a whole sanitize run of 33 seconds. All three pass, which is worth knowing given none of them had ever been run this way. The README said sanitize ran "the suite", which was generous. Now it does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
8ace20629b |
Say what the tests need, and count the tools correctly
Two nitpicks from review, and one of them was only half right, which is why each was checked rather than swept. THE TEST DEPENDENCIES were nowhere. The README said what building needs and stopped, so somebody with a C compiler and nothing else would get through 'make' and fall over on 'make test'. It needs bash and Python 3 - two of the checking scripts are Python, and one of those opens a pseudo-terminal - plus stty, timeout and the usual text utilities. "BOTH TOOLS" WAS WRONG IN TWO PLACES OUT OF SIX. 'make sanitize' builds the default target, which is all three, so the README and the makefile both undercounted what they rebuild. The other four are right and were left alone. assembly.h says both tools have to agree where the vector table begins, and bootstrap.c and cpu.c say they share the boot image format and the vector layout - in all three cases that is the emulator and the assembler, and SplitDisk has no opinion about any of it. The makefile's POSIX comment is the same story: SplitDisk uses not one POSIX interface, so it names the two that do rather than counting to three. Media/ is a home for the screenshot that is coming, with a note saying what belongs in it and that docs.sh will catch a link to something removed from it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
4e5710a1e6 |
Make the README's references clickable, and check that they land
The two manuals and the CosmOS README were named in prose and nothing else, so reading about them and getting to them were separate acts. They are links now, along with every directory in the repository map, which is the other place somebody reading that page wants to click. THE MANUALS HAVE SPACES IN THEIR NAMES, so the links carry %20. That detail is why this is checked rather than eyeballed: a link with a raw space in it points at a file that exists, so nothing about the filesystem is wrong - the renderer just stops at the space and the link goes nowhere useful. Tests/docs.sh now walks every relative link in every tracked markdown file and complains about both failures: a target that is not there, and a target with a space that should have been encoded. Both verified by breaking them. The first attempt at that verification was itself wrong - I removed the %20 and the check passed, because the file really does exist under that name. That is what showed the two failures are different things and both need catching. 13 links, all landing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
f4bc587d0a |
The C assembler names its output for the format, not always .bin
It always wrote .bin, whatever it had built. So assembling a loadable
program without -o produced Say.bin containing SBEX - a boot image name on a
file the machine cannot boot, in a repository whose whole convention is that
a .bin is started from and a .sbx is loaded.
Successfully wrote SplitBit boot image to "hello.bin".
Successfully wrote SplitBit loadable program to "Say.sbx".
programIsLoadable() already existed and is already what decides which
writer runs; the name now asks it too. Nothing in the build depended on the
old behaviour, because everything that assembles anything passes -o.
THE ASSEMBLER THAT RUNS ON SPLITBIT ALREADY DID IT THIS WAY. Two assemblers
naming their output differently from the same source is exactly the kind of
difference that wastes an afternoon, and the newer one was right.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
|
||
|
|
b6004bdcde |
Say "boot image" where that is what is meant
"Binary" was doing three jobs. It meant an SPBT file that the machine starts from; it meant whatever the assembler happened to produce, which is now either that or a loadable program; and it meant a compiled host tool. A word that means three things means none of them, and the first of the three has a name already - this project has been calling them boot images for a while and the manuals had not caught up. Where it means an SPBT file -> boot image Where it means either output -> output Where it means a host executable -> left alone Where it means base two -> left alone The user facing messages move with it: Error: No boot image specified. Usage: ./SplitBit [OPTIONS] <boot image> Error: This is not a SplitBit boot image. Error: This boot image is in format version 2, and this emulator reads 1. Successfully wrote SplitBit boot image to "hello.bin". The assembler's own help was the interesting case. Its -o writes either format, so "the binary" there was never right - it is "the output" now, and the message that names the format is the one that says which it wrote. No recorded output contained the word, so nothing needed re-blessing. Checked before starting rather than after. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
fa3982dbd9 |
Move the CosmOS third of the Programming Manual to CosmOS
386 of the manual's 1,116 lines documented an operating system rather than a machine. The split inside that file was never tutorial against reference; it was the machine against the software that happens to run on it. What A Program May Ask The System For 129 -> CosmOS README Programs That Come With The System 111 -> CosmOS README Reading And Writing The Filesystem 64 -> CosmOS README Loading A Program From A Disk 52 -> Assembler Manual The Console Library 25 -> CosmOS README The services are the clearest case: a hundred and thirty lines describing what CosmOS offers a program, in the manual for a CPU that has no operating system of its own. A different system on the same machine would offer different services and that section would be wrong for it. The loadable program format goes to the Assembler Manual instead, because SBEX is a thing the assembler WRITES. Nothing in the CPU knows what it is. The Programming Manual is 716 lines and fourteen sections now, all of them about the machine. TWO DUPLICATE DESCRIPTIONS COLLAPSED INTO ONE EACH. The application list existed in both documents in different words, and the CosmOS copy had gone stale - no Break, no Stream, no assembler - because only the manual's copy was checked. Moving the checked one in and deleting the other leaves one list, and docs.sh follows it. The second was made by this commit and caught while reading the seams: the CosmOS README already had a service table, so the move briefly produced two. That section now says what services are for and points at the one table. Renaming a section as it moved: "Reading And Writing The Filesystem" is "The Filesystem Library", which says what it is and reads beside "The Console Library". docs.sh follows all five, and each was verified by renaming the heading in its new home and reading the complaint. The README and the CosmOS README both described what the other manuals cover, and both were wrong the moment this landed; they say the division out loud now, since it is the point. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
518be9cb17 |
Rewrite the README around what the system is now
It had become a geological record. The overview was the 2024 one - "load and run binary programs created for SplitBit interactively from the command line" - and the feature list had grown by accretion, so it opened with Debug Mode, CLI Based and Modular Codebase and buried self-hosting at bullet fourteen of seventeen. Three sections were called "Usage". Now it opens with what the machine is and the one fact that makes it worth looking at, with the transcript underneath. The features are grouped into the machine and the software running on it, and the bullets that said nothing are gone: "Binary File Support" and "CLI Based" describe every emulator ever written. The three tools have names in their headings rather than three identical "Usage" ones, and their options are tables rather than bullet lists. There is a map of the repository, which there was not before and which the last two commits made worth having. TWO THINGS IT WAS SAYING THAT WERE NO LONGER TRUE, and nothing checks the README so nothing caught them: "it is still the only way to get a program onto a disk, since nothing running on the machine assembles anything yet" - SplitDisk has not been the only way for two days. "this is what a self-hosted assembler will stand on" - future tense about something that has since happened. The Tests section also claimed five checking scripts and then listed makedisks.sh among them, which builds fixtures rather than checking anything. It is four checks and a runner. Every command in it was run: assembling hello.asm and running the result, and booting the disk to see Snake.sbx load. Three em dashes crept in and were taken out - the whole tracked repository is plain ASCII again, which is now verified rather than assumed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |