79727044b7016d561d851624063129ffcfa89772
176
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
36ce9f6ccf |
D3: the machine knows where it is
cd moves it, dir lists the directory it is in, and the prompt says which one - but only when that is not the root, so a machine nobody has moved about on looks exactly as it always did and every recorded test that never says "cd" keeps its recorded prompt. A path beginning with a separator is measured from the root and anything else from where the machine is, so a bare name means a file in the current directory. NO PROGRAM HAD TO BE TOLD: the working directory lives in sbfs.asm beside the thing that resolves paths, because it is what a relative path MEANS. Keeping it in the shell would have meant either handing it down on every call or pasting it onto the front of every name, and the second of those is how a name that is already absolute gets ruined. Nothing stores the path. The working directory is an entry index and two bytes, and the text on the prompt is built each time by walking the chain of parents upward, writing names from the end of a buffer towards the front - which is the order they arrive in, and saves reversing them afterwards. sbfsFind splits into a walk and a check. "cd /" and "cd .." both end at the root quite legitimately, and had no way to say so through a routine whose only word for the root was "missing". Typing a program's name now tries two places in order: where you are, then /Apps. The first makes a program you are working on the one that runs; the second lets Snake work from anywhere. A word already beginning with a separator has said where to look, so only that place is tried. osChangeDir exists so that "a program may move about, and the shell puts the working directory back" is a thing that can happen rather than a promise about nothing. Both halves of that were unfalsifiable without it: with no way for a program to move, removing the restore changed no test. Wander is the program that moves - it goes where it is told and reads a file there by a bare name - and with it on the disk, removing the restore fails. The remembered file is dropped whenever what a relative path means changes: a cd, a program calling osChangeDir, a program exiting. Removing all of them fails the test and removing any one of them does not, because today every path into that cache belongs to a program that exits. It is kept in all three because the cost is a call and the failure is a file's blocks being handed out under another file's name. The cwd fixture holds two files called notes.txt saying different things, and a Say.sbx in /A that is really hello. Two copies of one program, or two copies of one file, would have passed with the whole of this deleted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
588e02aff5 |
Double CosmOS's half of the machine, and check that it fits
The memory map gave CosmOS 0x0000 through 0x1FFF of Program Memory and applications 0x2000 and above. CosmOS is 8141 bytes at the previous commit, which is fifty one bytes short of the line, and the next thing added to it went over. GOING OVER DOES NOT FAIL WHERE IT HAPPENS. Nothing enforces the division: an application says where it goes with #Base and the loader puts it there, so a CosmOS that has grown past 0x1FFF simply has the next program loaded written over the end of it. What breaks is whichever part of the shell that program happened to cover, at whatever later moment somebody uses it. It turned up here as the monitor's assemble command answering "I do not know" to valid instructions, several commands into a session, on a machine that had booted perfectly well. Both halves are doubled: applications now start at 0x4000 in Program Memory and 0x2000 in Data Memory. That is 16K of code and 8K of data for the system, against the 8775 and 2948 it uses today. Both were on the same trajectory, and moving them together means the twenty files that say #Base are edited once rather than twice. The standalone loader's loadable.asm keeps its old base: it belongs to the loader CosmOS grew out of, not to CosmOS, and its addresses answer to a different program. The unbased-segment diagnostic keeps its old base too - it exists to produce an error message that names the address, and the message is what is recorded. Tests/docs.sh now reads the two limits out of the table in the README and measures both segments against them. It reads them rather than being told them because the table is the specification, and this is the second time in this project that the thing nobody checked is the thing that rotted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
36a1b07b5b |
D2: the machine walks a path
sbfsFind takes a path where it used to take a name: names with '/' between them, walked from the root, with '.' and '..'. Each name is looked for among the entries whose parent is where the walk has got to. A bare name is a path of one name, so everything written before directories existed still works and still costs one walk of the directory. sbfsMount takes either version. On a version one disk every entry has zeroes where a parent goes and the walk starts at zero, so the comparison always agrees - which is how a flat disk reads correctly here with nothing done to it. PROGRAMS DID NOT HAVE TO BE TAUGHT ANY OF THIS. Resolution sits inside sbfsFind, below the services, so every osFile call keeps its signature and a path is simply a longer name. Type, More, Edit and the assembler gained subdirectories without a line changing in any of them. Four things this turned up, none of which was the path walk: load copied the path into a buffer sized for a NAME, so anything over 22 characters was cut short - and cut short into a path that often still resolved. "/Apps/Deep/../../Apps/Say.sbx" became "/Apps/Deep/../../Apps/" and reported that the program was a directory. That is the whole of what looked like a bug in '..', and it cost most of the time here. load on a directory SUCCEEDED. A directory has no blocks, so reading it reads nothing and leaves the staging area holding whatever was staged last - which, if that was a program, still says SBEX and still has a working entry address. It handed back the program before it. Refused outright now. delete and rename on a directory are refused, and save refuses one up front rather than failing at the rename and leaving a temporary behind. Deleting a directory frees an entry index, and a parent IS an index, so the next file created would take it and inherit the children. create writes the parent rather than leaving it zero by luck. It would be zero - delete wipes all thirty two bytes and a fresh entry never had any - but that is a fact about two other routines, and a file appearing inside a directory it was never put in is not a failure anybody would think to look for. dir marks directories and counts them apart from files, because at this point it was calling them files of no bytes. Two hazards written down in the design note turned out not to be real, and both were checked rather than argued about: The lookup cache holding 22 bytes of a longer path cannot hand back the wrong file - textSame wants both strings to end in the same place, so a cut down entry misses. It can never HIT either, though, so every path longer than a name went to the disk every time; it holds a whole path now. The allocator stepping over directories changes nothing any test can see. A directory has no start as well as no blocks, so its bounds are nought to nought and no candidate begins before it ends. The four instructions stay, with a comment saying they are not load bearing today and why they are there anyway. makedisks.sh resolves its build path before it cds. Given a relative one it carried on and quietly built disks missing some of their files, which is how the tree fixture lost a file and sent me looking for a bug in '..'. 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 |
||
|
|
f4fb56606e |
Give the demo disk room for names, not just for bytes
The demo disk had eight directory blocks, which is sixty four names, and thirty nine of them were already spoken for. Its two ceilings were nowhere near each other: the average file on it is twenty six blocks, so sixty four names run out with the disk forty one per cent full. Names were going to be gone long before space was, and space is what a disk is usually short of. Twenty four blocks is one hundred and ninety two names. The difference costs sixteen blocks of four thousand and ninety six, three tenths of one per cent, and the superblock has carried this number per disk since the format was written - so nothing but the makefile line knows or cares what it is. This is provisioning and not format. Directories, when they arrive, will not relieve this pressure; under the settled design they add to it, because a directory spends an entry of its own. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
2b0aeeefd4 |
Start a program by typing its name
A word the shell has no command for is now looked for on the disk as "<name>.sbx", and if it is there it is loaded and started exactly as load and run would do it. Whatever followed the word reaches the program through osArgument by the same route as whatever follows run, so "Say hello there" and "Type notes.txt" work without either program knowing how it was started. load and run are unchanged and both stay. load is how the monitor puts an arbitrary file in front of itself, which typing a name deliberately cannot do: the extension is added rather than assumed, so "notes.txt" looks for notes.txt.sbx and a text file is unreachable by name whatever is inside it. Three things this had to get right: The built-ins are tried first and always win. The search hangs off the end of the dispatch chain, so a file called dir.sbx cannot become dir, and the commands worth trusting when the disk is what you are doubting stay trustworthy. The invoke disk carries a working dir.sbx so that this is checked rather than asserted. A file that is found but is broken says so. "not a program" and "I do not know" are different answers, and giving the second about a file sitting on the disk would send somebody looking in the wrong place. loadProgram therefore hands back a status as well as a message, since only "no file of that name" can fairly be reported as anything other than a fault. doLoad became that subroutine rather than being copied. It ends in RET instead of a jump to the prompt, and each way of failing sets its number and its text together so a new one cannot leave half of the answer behind. cosmosBreak moves because Break prints the pointers it was handed and those are the shell's leftovers, which a CALL now puts back. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
dbe58db660 |
Add Type and More, and the file stream they are built on
Two applications that read a file too big for Data Memory: Type prints one, More pages it. Both sit on fileStream.asm, which wraps osFileInfo and osFileBlock into open-and-next so an application walks a file's blocks without repeating the service calls. The disk fixture is deliberately awkward: readable.txt crosses several blocks and carries no zero byte to be mistaken for an end marker, and empty.txt says that zero blocks is a valid file rather than an error. These three files were written by ChatGPT, as their headers record. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
4fd8bf7b3f |
Step a Data Pointer with INCD and DECD, not DPUP and DPDN by one
DPUP takes an immediate, so an offset of one is legal and does exactly the right thing. It is also three bytes where INCD is two, and reads as "offset the pointer up by one" where INCD reads as "step the pointer". 56 of them across 15 files: the system, the assembler, the editor, and eight test programs. CosmOS is 9,564 bytes to 9,537, the native assembler 11,648 to 11,635, and every program in the repository together 49 bytes lighter. The worst offender was numbers.asm, written this week, where every sixteen bit helper reaches the low byte and comes back the long way round. It is the file every other part of the assembler includes, so it is the first thing anybody reads when they go looking - and it was teaching them the long way. Pattern matched off sbfs.asm rather than off the instruction table I had just embedded in two programs. THIS IS NOT TWO WAYS TO DO ONE THING. DPUP takes an arbitrary number, so one is inevitably among them; INCD earns its place by making the common case a byte cheaper. The overlap is structural and the choice is a usage question, which is a linter's job rather than an ISA's - "DPUP.n 0d01: INCD.n does this in a byte less" is a mechanical rule with no judgement in it. Nothing needed re-recording, which was not a foregone conclusion: cosmosBreak prints the system addresses the registers happened to hold, and they did not move. Both assemblers still produce identical bytes and CosmOS still builds itself to a fixed point. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
12cb489268 | Minor typo fix to assembler manual. | ||
|
|
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 |
||
|
|
3f95056eec |
Three papercuts a visitor would hit first
Found by a review, all three confirmed by trying them rather than by reading. 1. THE STRICT BUILD CLAIM WAS FALSE. The README says the sources build clean under -std=c11 -pedantic with -Wall -Wextra, and they did not: realpath is an XSI interface, and _POSIX_C_SOURCE=200809L does not reach it, so the assembler would not compile. The ordinary -Os build never saw it, because without -std=c11 the compiler's own default declares realpath anyway. _XOPEN_SOURCE=700 is POSIX.1-2008 plus XSI, and covers every file on its own. Narrowed by compiling each source with each candidate macro rather than by adding one and hoping. And 'make strict' now checks it, as part of 'make test'. The README makes a claim somebody may check by typing it, so the suite types it. Verified the check bites by putting the old macro back. 2. THE COSMOS README HAD NOT CAUGHT UP WITH THIS WEEK. It said there were no breakpoints and proposed writing a spare byte over an instruction - two commits after SWI osBreak was built, which does it without overwriting anything and is described correctly further down the same file. Its Current Scope said there was no native assembler and that self-hosting was the intended long-term milestone. And the streaming section spoke of a machine "one day going to assemble itself". All three now say what is true. Self-hosting is described as done, with what is left of it named: a linker, and an editor that knows what assembly is. 3. 'make run-hello' DID NOT WORK, and it is the one command the makefile's own header advertises. It was a pattern rule against $(BUILD)/%.bin, which worked while every program sat at the top of Programs/ and broke the moment they were filed into Examples/ - which I did, four commits ago, without trying it. The name is looked up among the programs now, so it is the program's name rather than its path, and an unknown one lists what there is instead of saying "No rule to make target". The same sweep was run across all four documents for other claims this week invalidated. The remaining "not yet" phrases are about faults that genuinely are not defined yet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
7e8639489b |
Take the direction out of the last cross references
Includes two edits of Anachronaut's: dropping a "See the Assembler Manual" from text that is now IN the Assembler Manual, and tightening a sentence about when a vector number has to be agreed. The first of those was mine to have caught. Moving "Loading A Program From A Disk" into that manual turned a cross reference into a document telling you to go and read itself. I checked for dangling references before the move and found none, because I checked for references by SECTION NAME and not for references to the manuals themselves. Checked properly now, both directions: no other document refers to itself, and nothing that moved says "above" or "below" about something that ended up in a different file. The three remaining directional references are gone with them - two that were correct today and one, "See Numbers You Write Down below", pointing at a section twenty four lines further down that nothing guaranteed would stay there. A reference that carries a direction is a reference that goes wrong the next time anything moves, and this week moved a great deal. There are none left anywhere in the repository. 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 |
||
|
|
306b4dce92 |
Give the Programming Manual a title, and send the boot image format away
Last of the four. What was left after the reorder was a document whose first heading was "General Description" doing a part title's job without being one, and a section called "Input and Output In the Emulator" that held two console ports, a worked program, and a file format. A title and an opening that says what this document is FOR, and what the other two are for, so a reader who wants the operating system or the language knows immediately they are in the wrong file. "General Description" is "The Machine", which matches the three part headings the reorder gave the rest. "Input and Output In the Emulator" is "Making It Print Something", which is what the section is: port 0, and the shortest program that uses it. THE BOOT IMAGE FORMAT MOVES TO THE ASSEMBLER MANUAL, beside the loadable program format, for the reason SBEX went there: it is a thing the assembler WRITES. It is fair that the emulator reads them too - both tools speak it, the way SplitDisk and sbfs.asm both speak the filesystem - but only one of them makes one. And it is called a boot image now, in the text as well as the heading. That is what this project has been calling these files for a while; the manual was still saying "binary", which now means either kind of output file and so means neither. A CHECK THAT GOT BETTER BY BEING SPLIT. The hello world program and the hex dump of it were both in the Programming Manual, and docs.sh compared them with each other and with the assembler. The program stays with the machine, where the reorder put it just after the instruction list; the dump goes with the format it demonstrates. So the check now settles THREE things against each other: what one manual prints, what the other prints, and what the assembler actually makes. Verified both ways - a wrong byte in the dump, and the anchor renamed. The manual is 692 lines and four parts. It was 1,116 lines and nineteen flat sections when this started. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
e594f44cce |
Reorder the Programming Manual so it can be read from the front
The order was the order things were written in. A newcomer read the register
list and the very next heading was the vector table - an interrupt
mechanism, before a single instruction had been shown. The list of
instructions was section eighteen of nineteen, and how to make the machine
print something was dead last.
Four parts now, and each answers a question the one before it raises:
General Description the registers, the memories, the flags
Naming a Data Pointer
List of Instructions was 18th, and nothing before it could be read
without it
Making It Do Something
Input and Output was 19th and last. It carries the hello world
program, which now arrives on the heels of the
instruction list rather than after everything
The Console
The Stack Pointer, Set By Hand
When Something Else Wants Attention
Interrupts the mechanism before the table, which was the
The Vector Table other way round and made no sense that way
Hardware Interrupts
Faults
Refusing
What A Machine Is Made Of
Devices
Asking What Is There
The Memory Controller
Storage
Nothing is rewritten. Every section is the text it was, in a different
place, so the diff is a move and can be read as one.
CROSS REFERENCES NO LONGER SAY WHICH WAY TO LOOK. "See The Console below"
was true until this commit and false after it, and three of them flipped at
once. The name is enough to find a section with, and a reference that
carries a direction is a reference that goes wrong the next time anything
moves. There will be a next time.
Left for the last commit of the four: "General Description" is doing the
work of a part title without being one, and "Input and Output In the
Emulator" now holds a worked example and a file format as well as two
ports, so it wants a better name or a split.
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 |
||
|
|
460a687939 |
Make the manuals plain ASCII, and check that they stay that way
"All files must be plain ASCII, the user's tooling doesn't support Unicode" is a standing rule of this repository. Nothing enforced it, so it drifted: 39 em dashes and an ellipsis had collected in the two manuals, every one of them typed by something that helpfully substituted a nicer character. The spaced em dash becomes a spaced hyphen, which is what the source comments and both READMEs use for the same job. Tests/docs.sh now checks every tracked file and says which line and which character. Verified that it bites. THE CHECK READS git ls-files NUL SEPARATED, and that is the whole reason this went unnoticed. I ran the obvious shell version of this audit two commits ago - a loop over $(git ls-files) - and reported the repository clean. It splits on whitespace, so it looked for a file called "SplitBit", failed into /dev/null, and found nothing wrong with either manual because it never opened them. Both have spaces in their names. A check that cannot see the files with spaces in their names is worse than no check at all, because it answers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
b2945e41c4 |
docs.sh: say which heading went missing, rather than raising IndexError
The manual checks find what they examine by splitting the file on an exact heading. Ten of the eleven anchors already say what they could not find - "the Programming Manual has lost its Devices table" and so on. Two did not: the worked hello world program and the hex dump beside it were reached with pm.split(anchor)[1] and nothing else, so renaming either produced a Python traceback and an IndexError. A traceback is a worse answer than a stale manual. It says a check broke without saying which heading moved, and it stops the rest of the run, so whatever else was wrong stays unreported. Both anchors are now tested before they are used, and both say which one is missing and what that means. Verified by renaming each and reading the message. This is the first of four commits restructuring the Programming Manual, and it comes first on purpose: the next three move headings around, and they should be watched by checks that would notice. IT ALSO CORRECTS THE PLAN. I had written that renaming a heading fails silently, and set out to fix all eleven. Probing them one at a time showed that was wrong - ten were already fine, and the job was one check rather than the whole file. The claim was worth testing before acting on it. 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 |
||
|
|
ccf4b384e1 |
Give Programs/ one rule: a directory per kind, nothing loose
Five .asm files sat at the top of Programs/ beside six directories, with
nothing to say which a new file should join - and hello.asm, which is the
native assembler's first target and named in sixteen places, looked like a
stray.
Programs/
Examples/ what you read to learn: hello, printHello, inputTest,
replCalculator, and Fibonacci, primeSieve and gameOfLife
as sets of their own
Libraries/ included by name, no entry point of their own
Loader/ loader.asm, and the loadable program it reads
CosmOS/ the system, its applications and its assembler
testPrograms/ what 'make test' drives
Loader/ is the one worth explaining. loader.asm is not a demonstration: it
reads a program off a disk, puts the two pieces where the header asks, and
jumps to the entry. CosmOS grew out of it and does the same thing as one of
its commands. It is kept because backward compatibility with the simplest
version of the system is a standing goal, and it was sitting loose next to
the demos as though it were one.
Programs/loadable/ was a directory holding one file called hello.asm - a
third thing of that name, and the name said nothing about why it was there.
It is Loader/loadable.asm now, beside the loader that reads it.
Every reference moved with them: the makefile's program list, twelve
manifest lines, makedisks.sh, native.sh, and four paths across the README
and both manuals. Verified by deleting both build directories and running
the whole suite from nothing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
|
||
|
|
6dbb38b209 |
Stop committing assembled programs, nine of which had gone stale
Thirteen build artifacts were tracked: ten .bin files, a disk image, and the text file that had been put on it. Every one is produced by 'make' in Programs/ from a source file that is also tracked. NINE OF THE TEN BINARIES DISAGREED WITH THEIR OWN SOURCE, including Programs/CosmOS/Bin/cosmos.bin - the operating system. Only hello.bin still matched, and that by accident. That is worse than shipping no binary at all. A repository holding both a source file and a stale build of it has two answers to what a program does, and the wrong one is the one that runs. Somebody reads the source, runs the binary, and sees behaviour that is not in the code with nothing to tell them why. Nothing referenced any of them - not the makefiles, not the tests, not the manuals. Checked before removing, and checked after: the suite passes, and a clean clone with all of them gone builds both tools and assembles every program from source alone. .gitignore now covers *.bin, *.sbx and *.img, unanchored, because a build output may appear anywhere a source file lives. Programs/CosmOS/Bin/ held cosmos.bin, disk.img and a 17 byte hello.txt that existed only to be put on that image. It was a snapshot of a demo from before Tests/makedisks.sh built its own fixtures, and it goes with them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
fb335681d2 |
M4: SplitBit assembles SplitBit, and then assembles itself
> load Asm.sbx
> run cosmos.asm
wrote cosmos.bin: program 7036, data 2448, labels 475
> run Asm.asm
wrote Asm.sbx: program 7533, data 4099, labels 555
Both byte for byte identical to what the host assembler builds from the
same source. The machine now builds the operating system it is running on,
and builds the thing that built it.
THE CHECK THAT MATTERS MOST IS THE THIRD ONE. A binary that matches could
still have come from an assembler wrong in some way this particular source
happens not to exercise. So Tests/native.sh boots the CosmOS that CosmOS
built and has THAT assemble CosmOS again - and the second generation is
identical to the first, down to the cycle count. It is a fixed point: the
machinery has been through itself. After this the host is a convenience
rather than a necessity.
WHAT STOOD IN THE WAY was not the assembler. It loaded, faulted at 7,780
cycles, and the fault was in CosmOS: a loaded program is staged at 0x8000
before being blitted into place, so the whole FILE has to fit in the 32,768
bytes above it. The assembler's file was 33,983, and 22K of that was
zeroed scratch buffers - because #Reserve emits what it reserves.
None of that is initialised data. It is scratch, wanted only while the
assembler runs, and while it runs everything above its own data is free.
So the buffers are a MAP now rather than declarations - Assembler/scratch.asm
writes down six addresses and the file carries none of it. 33,983 bytes
became 11,648, and the assembler could load itself.
The map has a file of its own because the reader and the label table both
need addresses out of it while neither includes the other.
The sizes are cut to the largest thing it is asked to build, and that turns
out not to be the operating system: the assembler is 555 labels and 11,648
bytes of output against CosmOS's 475 and 9,564. The hardest thing this
assembles is itself.
Also: sizing it for CosmOS meant raising the label table, and raising the
label table is what pushed the file over the staging limit. The two facts
only met because the first one was tried.
Speed, measured rather than guessed: CosmOS takes 80,168,646 cycles, which
is eighty seconds of emulated time and under a second under --fast. Most of
it is a straight walk of 475 label names, several thousand times. Sorting
or bucketing that is easy and was deliberately not written before there was
something to measure.
make run-cosmos now puts every source file on the disk, so the whole thing
can be done rather than read about.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
|
||
|
|
20989c3439 |
M3: programs that bring their own vectors
> load Asm.sbx
> run Keys.asm
wrote Keys.sbx: program 558, data 85, labels 52
> load Keys.sbx
> run
keys, by interrupt. q stops.
ab
the console has been handed back
The machine assembles a program carrying an interrupt handler, the loader
installs its vector, the console interrupts into it, and the shell takes the
vector back at exit. Byte for byte identical to the C assembler's, and
Tests/native.sh now checks a boot image and four loadable programs on every
run.
WHAT IT TOOK:
A declaration and an implementation are the SAME ENTRY. services.asm says
a service is called osPrintString and has number 16; cosmos.asm says
osPrintString is handled by handlePrintString. The name is met twice and
the second time fills in the handler, which is what lets one shared file
serve both the caller and the implementer.
So the first pass declares and the second implements. That is forced: a
handler is an address, and no address is known until every label has been
placed.
Boot in a loadable program fills the entry field rather than being
installed - vector zero is where the whole machine starts, and a program
loaded into a running system has no business saying anything about that.
A boot image is the one thing that does, so there it is installed like any
other, behind a "VEC" marker in the SPBT file.
Device is named by the port, and Device with the five reserved names are
matched without regard to case, the way mnemonics are: they are part of
the language rather than names the programmer chose. Devices have no names
of their own, so they are given one nothing can type.
TWO BUGS, both of a kind worth naming.
The first: "is this a loadable program" was written out as an OR of the two
segment bases in seven places, and the sense wanted is the opposite in most
of them. One of the seven had it backwards and put a version ONE header on a
file carrying vectors, which a loader is right to refuse. It is one flag
now, settled once and tested the same way everywhere.
The second: finding the entry to write a handler into means calling vecFind,
which reads the entry's fields out - including the handler it does not have
yet. An address resolved into VecHandler before the find was overwritten
with zero by the find itself, and the file came out with a vector pointing
at address zero: a slot that looked installed and went nowhere. The
resolved address has a variable of its own now.
Keys.asm and console.asm go on the CosmOS disk, so the whole path can be
watched rather than only tested.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
|
||
|
|
04424a8917 |
Put the application hello.asm on the disk, not the bare metal one
Both are called hello.asm and they are different programs: Programs/hello.asm
is the 1978-style boot image that writes straight to port 0x00 and HALTs,
and CosmOS/Apps/hello.asm is the loadable one that ends with SWI osExit.
The disk had the first, which assembles to a .bin the shell cannot load, so
watching it work meant taking the file off the disk afterwards.
Now it has the second, and the whole thing is two commands:
> load Asm.sbx
> run hello.asm
wrote hello.sbx: program 22, data 14, labels 3
> load hello.sbx
> run
Hello, World!
It writes over the hello.sbx the host tool put there, so what runs is the
machine's own work. Byte for byte identical to the C assembler's, as ever.
strings.asm stays as it is, and now earns its place by being the odd one
out: no #Include and no #Base, so it assembles to a boot image rather than
a loadable program and the difference between the two is visible on one
disk.
Tests/native.sh still builds the bare metal hello.asm, and should: it is the
only thing that checks the SPBT path end to end, including running what came
out.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
|
||
|
|
c5e4ec3455 |
M2: the native assembler builds applications
> load Asm.sbx
> run Say.asm
wrote Say.sbx: program 46, data 93, labels 7
> load Say.sbx
> run built by the machine itself
it says: built by the machine itself
The machine assembles an application and then runs what it built. Say,
greet and Files all come out byte for byte identical to the C assembler's,
and Tests/native.sh checks all three on every run alongside the boot image
M1 already covered.
WHAT IT TOOK, and it was more than #Include and #Base:
#Include The reader is a stack of readers. The current file's whole
state goes aside - buffer and all, 292 bytes - the new one
opens, and the end of it pops the old one back. A file goes in
once; including it twice does nothing, which is what lets two
libraries depend on a third. The list is forgotten between the
passes, because the second has to walk the same tree.
#Base Cursors start there, so labels hold the addresses the program
will really have. A program that says where it goes gets the
SBEX header and a .sbx name; one that says nothing gets SPBT
and .bin. A program that bases one segment and leaves the
other unbased with content in it is refused.
#Reserve Runs of zeroes, moved over in the first pass and written in
#Align the second. How many an #Align comes to depends on where the
cursor has reached, which is why both passes keep a cursor.
#Vectors Names are read and numbered, pinned where the source pins
them, so SWI osPrintString resolves. Every application needs
this - a program that calls a service names a vector declared
in a file it includes.
THE TWO PASSES ARE NOW ONE LOOP, walked twice, with Emitting the only
difference. They have to agree about the length of every token, and the way
they stop agreeing is by being two pieces of code that drifted apart -
which is the exact shape of the bug this assembler found in the C one.
Sharing the body means there is nothing to drift. What is left is checked
anyway: the second pass compares its own totals against the first's and
refuses to write the file if they differ.
THE BUG WORTH RECORDING. The tokenizer holds one character of lookahead,
and at an #Include that character belongs to the file being put aside. It
was carried across and handed back on the way out, which is wrong: a file
runs out in the middle of whatever the tokenizer happens to be doing, so
the character arrives in the middle of a word. `start:` came back as `s`
and then `tart:` - and the result assembled into a perfectly plausible
file. The fix is to undo the read instead, so the character is simply still
there when the file is opened again and the question of when to hand it
back never arises.
Three smaller ones, all old friends: three places took the CONTENTS of a
buffer where they wanted its ADDRESS; vecTakeAuto returned its answer in A,
which a RET puts back; and pass two re-declared every vector because only
the label table was being skipped on the second walk.
sameText moved down into numbers.asm from the label table - four parts want
it now, and a reader test that includes neither labels nor tokens has to
build on its own.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
|
||
|
|
affe9d09ea |
Put the assembler and some source on the CosmOS disk
'make run-cosmos' now boots a machine with Asm.sbx on the disk and two
source files to point it at, so the thing can be watched working rather
than only tested:
> load Asm.sbx
> run hello.asm
wrote hello.bin: program 17, data 14, labels 2
> run strings.asm
wrote strings.bin: program 62, data 88, labels 4
Both come out byte for byte identical to the C assembler's, and both run.
hello.asm and testPrograms/stringKeyword.asm are the two single-file
programs with no #Include, which is what the native assembler handles so
far; strings.asm is the second because it has a subroutine and a label
used before it is defined, so it exercises a forward reference that
hello.asm does not.
The assembler builds from its own directory rather than from Apps/,
because it is not one file. Its pieces are found beside it without being
told, since an include is looked for next to the file that asked for it
before anywhere else; only services.asm needs the include path.
ALSO A CORRECTION. Asm.asm's header, its usage message and the Assembler
Manual all said 'run Asm.sbx hello.asm'. That is the convention we talked
about wanting later, not the one this shell has: load and run are separate
commands, so the whole rest of the run line is the argument and that form
asks for a file called "Asm.sbx hello.asm". All three now say load first.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
|
||
|
|
dcb331c151 |
SplitBit assembles SplitBit: M1, a single file with no includes
Programs/CosmOS/Assembler/ is an assembler written in SplitBit assembly. It
runs under CosmOS, reads source off a SplitBit disk, and writes a binary back
to it with no host involved anywhere:
> run Asm.sbx hello.asm
wrote hello.bin: program 17, data 14, labels 2
THE ACCEPTANCE TEST IS THE BYTES. Tests/native.sh assembles Programs/hello.asm
both ways and compares the two files byte for byte, then runs the one the
machine built. "It ran" and "the sizes look right" both pass for a binary with
a label one byte out, which is a program that jumps into the middle of an
instruction - so the only honest test is the one SplitDisk and sbfs.asm
already work under: two implementations of one written specification, each
checking the other. The files are identical and the result prints Hello,
World! in 70 cycles.
hello.asm is the target because it is the oldest program in the repository.
The first thing this machine ever ran is now the first thing it assembles for
itself.
TWO PASSES OVER STREAMED SOURCE. The C assembler reads every token of every
file into one array; that cannot port, because cosmos.asm alone is 56,047
bytes against 64K of Data Memory. The native one streams through a 256 byte
window, twice, and keeps only the label table between the passes. Two passes
suffice because every length is known without resolving anything - an
instruction's from its shape, a value's is one, a string's is its characters
and a zero - so the first pass fixes every address and the second never needs
a fixup list. A forward reference stops being a special case and becomes the
reason there are two passes at all.
The parts, each checked before anything was built on it:
source.asm characters out of a file of any size, with a line number
token.asm tokens out of characters, one character of lookahead
classify.asm what a token is, in the C assembler's order, which IS the
language: keyword, instruction, value, string, label
labels.asm names packed in an arena, four bytes of index each
numbers.asm sixteen bit arithmetic, since sbfs.asm's cannot be reached
table.asm the instruction set, generated by the same script the
monitor's copy is, and now BOTH are checked by docs.sh
readTest.asm and tokenTest.asm check the reader and the tokenizer on their
own, recorded as cosmosSource and cosmosTokens. A wrong classification does
not produce a wrong byte somewhere obvious; it produces a right looking
program of the wrong length, so it is worth catching where it happens.
WHAT IT REFUSES: #Include, #Base, #Align, #Reserve and #Vectors are refused
by name rather than ignored. Skipping a directive would produce a file that
looked right and was the wrong length, which is the worst thing an assembler
can do.
Two traps worth recording, both already known to this project and both hit
again: CALL restores A, B and DP0-DP2, so three routines returning an answer
in A had it undone by their own return; and numStep works on DP0, so three
sites that set DP1 left a pointer that never advanced.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
|
||
|
|
a131a90c67 |
A string that spells a directive is a string, not a directive
The quotes are gone by the time a token is classified, so checkIfKeyword's test of token[0] == '#' matched the STRING "#Program" sitting in a program's Data Segment. It was read as the directive: the segment silently changed in the middle of the data, the string's nine bytes were charged to the Program cursor instead of the Data one, and every label defined after it came out nine bytes wrong - in a file that still had a valid header, a plausible length, and nothing to say about any of it. The only symptom was a program that jumped into the middle of an instruction. This is the FOURTH of the family. A string spelling a mnemonic assembled as that instruction; a string beginning with a zero was rejected as a malformed literal; a string in the Program Segment was discarded in silence. The instruction check and the literal check both carry a "not a STRING" guard already. This one did not, so it has one now, and it lives inside checkIfKeyword rather than at the call site so it cannot be left off again. Nothing had ever triggered it, because nothing had ever needed a directive's name as data. An assembler written FOR this machine necessarily does: it has to compare tokens against "#Program" and "#Data". It was found by building one and watching it fault on its second instruction. Test stringKeyword puts every directive name in a program's data and prints a label defined after them. Verified that it bites: without the fix the assembler refuses the file outright. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
ca243895ca |
Grow the CosmOS disk to 512K, because it has to hold its own source
cosmos.img was 256 blocks and two directory blocks: 64K, sixteen files. Ample for a disk that held nothing but programs, and not enough for the thing that comes next. The native assembler reads SOURCE from this disk, and the CosmOS sources alone are 104,142 bytes against the 65,536 a 256 block disk holds - so the machine could not hold its own source code. 2048 blocks and four directory blocks: 512K and room for 32 files. Nothing was recorded that had to change, which is worth saying rather than assuming: dir lists names and does not report free space, so no golden file names a block count. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
3d2ab34229 |
Streaming: read a file bigger than the machine's memory
osFileRead hands over a whole file, which settles anything under 64K and settles nothing above it. CosmOS's own source is above it - the sources together are 104K against 64K of Data Memory - so a machine that is going to assemble itself needs another way to ask. osFileInfo (0d26) says how many blocks a file occupies. osFileBlock (0d27) hands over one of them and says how many of its bytes belong to the file. Between them a program reads a file of any size through a buffer of 256. Blocks rather than bytes from osFileInfo is forced, not chosen: a file on a sixteen megabyte disk is up to twenty four bits long and a pointer holds sixteen. osFileBlock's count answers in DP3 for the same kind of reason - a whole block is 256 bytes, which does not fit in a register, and a count that reported it as zero would make every reader special-case the end. Nothing is kept open. Every call names the file, so there is no handle to leak and nothing left behind by a program that stops halfway. Taken at its word that means searching the directory once per block, so the system remembers where the last file it was asked about lives; every path that can change what a name means calls fileForget, including the shell's own delete and rename, which do not go through the services. Correctness never depends on the cache - a cache thrown away is indistinguishable from one never filled. Measured on a 329 block file: 7% of the run saved when the file is the first directory entry, 11% when it is the sixteenth. These two say WHY when the answer is no, which the others do not. Elsewhere the only useful response to a failure is to give up, so one value suffices. These are asked questions, and running off the end is how a reader learns it has finished, so it gets an answer of its own: 1 no disk, 2 no such file, 3 past the end, 4 the disk refused. Apps/Stream.asm reads an 84,000 byte file through 256 bytes. The check that matters is the second one: a small file read BOTH ways - whole with osFileRead and streamed - with the two checksums compared, so streaming is measured against the path already known to work rather than against a number someone wrote down. The checksum is Fletcher's rather than a sum, because a sum is the same whatever order the bytes arrived in and the order is exactly what streaming has to get right. Both checksums were also confirmed against the same arithmetic run on the host. The rest of the test is the cache: two files read alternately catch a memory that missed the name changing, and a rename catches one that missed the file moving - and that one would otherwise pass, since the blocks are still there holding the same bytes. The test file is generated rather than taken from the repository. The CosmOS sources would be a truer picture and would move the recorded checksum every time a line of CosmOS was edited, putting a real difference in a crowd of meaningless ones - the same trap the cycle counts used to set. cosmosBreak's recorded output moves by two bytes in two pointers: SbfsIndex added two bytes to the filesystem's data and Break prints the system addresses the registers happened to hold. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
3b800a69e8 |
Monitor: a line assembler
a <address>, then instructions until a line that is just a dot. The syntax is the assembler's own: a selector rides on the mnemonic as LDA.0 or LDD.0.1, and leaving one off means Data Pointer 0 exactly as it does in a source file, so nothing learned at the monitor has to be unlearned when writing a program. Case is folded, since the assembler does not care either. Numbers are hexadecimal and bare. A source file writes 0x2000 or 0d16 because it has both and must say which; a monitor has one and says so once, in the manual, rather than on every line. It reads the same table the disassembler does, searched the other way round, which is the point of it being a table rather than two lists: what a writes, d reads back, and neither can drift from the other or from the assembler both were generated from. Instruction lengths come from the shared shape table too, so the cursor cannot get out of step with what was written. THE WHOLE LINE IS UNDERSTOOD BEFORE ANYTHING IS WRITTEN. Emitting the opcode first and discovering a missing operand afterwards leaves half an instruction in memory, which the next line usually covers up and the last line of a session does not. Written that way first and fixed. What cannot be written is a label, and that is the whole difference between this and the assembler proper: a label is a promise to fill an address in later, and later is what a line at a time does not have. The recorded test now types in a complete program - a string poked into Data Memory, instructions assembled into Program Memory, and the result run - and includes a lower case mnemonic, both selector forms, an instruction that does not exist and one missing its value, so the refusals sit beside the successes. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c23adb2836 |
Break: name the status register properly and show the Stack Pointer
The dump labelled the status register "S", which reads as Stack to anybody sensible - and the Stack Pointer was the one register it did not show, so there was nothing to contradict the guess. It is written "status" now, and followed by the bits that are up, because a dump that makes you look the number up is only half a dump. The Stack Pointer is not in the frame, since the frame is where the Stack Pointer is. What the program had is fourteen bytes above it, that being what entering an interrupt puts down, so it is worked out and shown. Apps/Break.asm takes its second stop inside a subroutine, so the recorded output shows the Stack Pointer at FFFF and then at FFF5: a difference of ten, which is the size of a CALL frame. That checks the value is derived rather than constant, which the previous version could not have told you. Reported by Anachronaut, who read the output and asked why a pointer was two digits long. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
5fd995aa62 |
Breakpoints: SWI osBreak, and s refuses a read only bank
A breakpoint that shows every register as the program had them, waits for a key, and carries on. NOTHING IS OVERWRITTEN, and that is the design rather than a shortcut. A breakpoint poked into a running program has to replace an instruction, and putting that instruction back in order to continue is the same act as disarming the breakpoint; firing a second time would mean stepping over the restored instruction and putting the breakpoint back behind it, and this machine cannot step a single instruction. SWI is two bytes, dispatches through a vector, and its frame already holds the address after it, so RETI resumes at the next instruction with nothing to restore and nothing to re-arm. It fires every time it is reached. The price is that a breakpoint is part of the program: a build with them in has different addresses from a build without. That is the bargain every machine with a break instruction makes. Every value shown comes out of the frame rather than the registers, because by the time the handler runs the registers are the handler's. Apps/Break.asm stops twice so that the second stop is checked as well as the first. Also here, found by the test that came with it: the monitor's s wrote into whichever bank was selected, and bank 2 is the controller's own table, published read only. Writing to it was refused, and a refusal nobody catches stops the machine - so selecting the bank table to look at it and then typing s killed the session. bankPresent now keeps the whole flags byte and s declines. The recorded output of cosmosMonitor had contained that crash, having been blessed without being read. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
b36d438132 |
Add the CosmOS README, brought up to date with today's changes
Written by Anachronaut and not previously committed. Three things in it had
gone stale between writing and now, and are corrected here rather than
committed knowingly wrong:
- the shell command table still described dump, which the monitor
supersedes, so it now describes monitor mode and its commands
- the service table listed four services, and there are nine
- the application list did not mention Files
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
3e87a67809 |
Edit: use the system's services instead of carrying the filesystem
The editor included sbfs.asm and console.asm, which was 3235 of its 4941 bytes. Both come out: reading and writing a file, printing and reading a line are services now, and sbfsMount goes entirely because the system has already mounted the disk. Only text.asm remains, at 357 bytes, because textSame and textNumber own nothing and touch no device - sharing those is a linker's job rather than an operating system's. 4941 bytes to 1983, a little better than predicted because dropping the mount and the blocks-and-tail arithmetic went with the library rather than being translated. ConsoleEndOfInput went with console.asm, so the end of input is now read from the console status port. That is better than what it replaced: the port is there whether or not the system is. Nothing it does changed, and that is checked rather than asserted. The recorded output of the cosmosEdit test does not move by a single byte across this commit - a test written days before the rewrite, checking a property rather than an output, and therefore able to verify a change it knew nothing about. One thing is lost and worth naming: there is no way to ask whether a disk is present, so the editor's "there is no disk" message is gone. On a diskless machine it now opens an empty document and fails at w instead. The shell already refuses to load without a disk, so the editor cannot normally be reached that way. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
0b6d2be43f |
CosmOS: a service interface for the disk and console, and the monitor in the shell
Two changes that arrived together because both live in cosmos.asm. THE SERVICES. A loaded program that wanted a file had to include the whole filesystem, carrying two and a half kilobytes of a private copy of code the system already had running, and then mount a disk that was already mounted. Five services are added at pinned numbers 20 to 24: osFileRead, osFileSave, osFileDelete, osFileRename and osPrintNumber. The sizes fit the registers exactly in both directions. A file that can be read into Data Memory is under 64K by definition, so its length is sixteen bits: coming back it is DP3, going out it is A and B together, and neither direction needs a record in memory whose shape both sides must agree on. There is deliberately no service to mount a disk. The system mounts one before its first prompt, and a program mounting it again was only ever a consequence of owning a second copy of the library, so that call disappears rather than moving. Apps/Files.asm writes, reads, renames and deletes a file in 645 bytes and includes nothing but the service names. THE MONITOR. Previously an application, now part of the shell, because an application occupies the one region a loaded application is given: a monitor that was an application could never examine another one, since loading the thing to be inspected would replace the thing doing the inspecting. "monitor" turns it on and the prompt becomes "*". It is a mode rather than a sub-prompt, and it persists: because the mode is a variable the prompt reads rather than a second loop, and every path back to the prompt goes through one place including osExit, a program started with "g" that gives the machine back arrives at the monitor prompt it was started from. Examining a program and running it therefore do not interrupt each other. "exit" leaves whatever you are in. It supersedes dump, and adds disassembly, writing bytes, and jumping to an address. Its instruction table is generated from the assembler's own list by Tests/instructiontable.py rather than typed again, and Tests/docs.sh checks both that the system's copy matches the generator and that the lengths that table implies are the ones the manual's Bytes column prints. A disassembler that disagreed about a length would not print one line wrong, it would lose its place and print everything after it wrong. Also here: b refuses a bank that is not registered, since asking the controller for one is refused and a refusal nobody catches stops the machine; g records the Stack the way run does, without which a program returning through osExit restored whatever the last run had left; and make cosmos-disk now depends on the system as well as the image. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
301716e869 |
sbfs: give sbfsMatchEntry a counter of its own
sbfsFind counts a block's eight entries in SbfsCount and calls sbfsMatchEntry for each one, and sbfsMatchEntry used the same variable for its own twenty two byte comparison. A name differing at its first byte returned without decrementing, leaving 22 in the entry counter, so the search then walked twenty two entries through a buffer holding eight: off the end of it, into whatever data happened to follow, matching against rubbish and reporting a file that is not there as found. It had always been there. What decided whether it looked like it worked was the bytes after SbfsBuffer, so it stayed hidden until unrelated data was added and changed them. It showed up as "dir" followed by any failed "load" reporting that the file could not be read rather than that it does not exist. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
be402cc9be |
Assembler: a string that spells an instruction is no longer assembled as one
A token is classified after its quotes have been stripped, so a string literal reading "ADD" looked exactly like the ADD instruction and was assembled as one. It failed with "attempting to assemble outside the Program Segment", a message about a mistake nobody had made. The literal and label checks were already guarded against strings and the instruction check was not. Mnemonics match without regard to case, so "or" and "and" were caught by this too, and those are ordinary enough words to want in a message. Third of its family, after a string beginning with '0' being read as a malformed number and a string in the Program Segment being silently discarded. All three have the same root. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
ca34e077ad |
Add sbex.h, which was never committed
The loadable program format's header has been in the working tree since the loader was written and has never been in the repository: the gitignore rules matched Source/Assembler as a directory, so it was silently untracked. Anything cloning this repository could not build it, because secondPass.c includes this file. This is the failure the previous commit describes, having already happened once without being noticed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
51601545ae |
Anchor the gitignore patterns to the repository root
Without a leading slash a pattern matches at any depth, so the rules for the three built binaries also matched directories: "Assembler" ignored Source/Assembler. The files already in it stayed tracked, which is why nothing had gone wrong yet, but a new source file added there would have been silently untracked and left out of a commit. Found by git refusing to stage a change to Source/Assembler/firstPass.c. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
c4b59acc68 | Infrastructure for system services through software interrupts. | ||
|
|
e3100b4718 | Fixed assembler bug that caused crash on IR array resize. Added line editor app. | ||
|
|
1d1a14318c | Programs can now list and share vectors. | ||
|
|
a842c884e8 | Added assembler check for string outside Data Segment. | ||
|
|
9e3425d34b | Programs can now pin specific routines to specific vectors in SplitBit assembly. Added snake game. | ||
|
|
08624925fe | Interrupt on keypress mode |