Commit Graph
30 Commits
Author SHA1 Message Date
AnachronautandClaude Opus 5 7dca141aae The vector's number goes beside its kind, not at the end of the line
Kind and Number together are what names a thing: Vector 16 and Device 32
are identifiers in a way that Vector on its own is not. Everything after
them - where it lives, what it is called, where it was written - says
something about it rather than naming it, so with the number at the far
end a line opened with a bare kind and closed with the fact that would
have told you what you were reading.

Fields are now Kind, Number, Address, Name, File, Line. A label still
carries a dash where its number would be, which reads as "this kind is
not numbered" rather than as a field that went missing.

Manual and the docs check follow. Verified with break.sh by swapping the
number and the address back.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-05 11:03:11 -04:00
AnachronautandClaude Opus 5 f3d8985bc4 Vectors in the symbol table, with both of the numbers they have
A vector is the one thing about a program that nothing else can tell
you. A pinned vector has its number in the source that pinned it, but a
vector the assembler numbered has that number nowhere at all - not in
the source, not in the binary in any form a reader can find. Until now
there was no way to learn that a vector became number 64.

It also cost two hops to follow by hand. The name in "SWI osPrintString"
is not the name of the routine that implements it, so finding the code
meant searching for the vector, reading the handler's name off the
Vector Segment, and searching again. A vector row now names the handler
and gives the line the two were tied together on.

Both numbers, at the user's asking, because neither can be worked out
from the other without knowing which table the vector is in: the Number
is what a program writes and the machine dispatches on, the Address is
where the handler's address is stored, base plus twice the number. The
slot is computed with the same expression the loader is given, so what
the table says and what gets written there cannot drift apart. A vector
a program only declares is listed too - that is how a program says which
vectors it calls, and how two programs can be checked against each other
for agreeing about a number.

A device has no name of its own, being named by the port it is plugged
into, so it is listed under its handler.

The first field is now Kind rather than Memory, because Vector and
Device are not memories. Sorted Program, Data, Vector, Device.

docs.sh checks the six fields against the manual and against real dumps
of two programs - Keys, a loadable program with all four kinds, and
cosmos, a boot image whose segments both start at zero. It now also
checks that a row's name really appears on the line the row names, which
is what catches the string-newline bug fixed in ca6c8ca coming back.
Verified with break.sh four ways: wrong slot arithmetic, vectors
dropped, a field renamed in the manual, and that bug reintroduced.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-05 10:51:50 -04:00
AnachronautandClaude Opus 5 3527812c41 A symbol table says which memory, and where the name was written
The dump was an address and a name. Both of the questions it gets asked
were only half answered.

"What is at this address" was ambiguous, because Program and Data are
separate memories and an address alone does not say which one. That is
easy to miss in a loadable program, where the segments are usually based
far apart - and immediate in a boot image, where both start at zero:
replCalculator has a Program 0003 and a Data 0003 and the old file
printed both as "0003 <name>".

"Where is this defined" was not answered at all, and it is the one that
matters more as a program grows. A name defined once and called in forty
places is hard to find by searching. Lander's table names five files
besides its own; CosmOS and its libraries define over a thousand names
across a dozen.

So: memory, address, name, file, line, separated by tabs, sorted by
memory and then address with Program first. Tabs because that makes it a
table cut, awk and sort already read, and no heading line because
nothing should have to know to skip one. Everything needed was already
being passed to addLabel and thrown away; the file name points at the
copy the include list owns, which outlives the label table.

The manual describes the five fields, and docs.sh now settles that
description against a real dump - the shape, not the values, so that an
example cannot go stale and turn editing a program into editing a
manual. Verified with break.sh three ways: a reordered field, a dropped
field, and a field renamed in the manual.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-05 10:21:06 -04:00
AnachronautandClaude Opus 5 7a55cfe151 Say that an option's file is one the assembler writes, and check we said it
-S was added without a row in the Assembler Manual, and the usage it
printed listed a bare "-S <file>" with no long name and no statement of
what the file is for. That is not merely incomplete, it is misleading:
"-S <file>" reads just as naturally as "dump the symbols of <file>", and
asking for it that way hands the source to -S, leaves nothing positional
behind it, and is answered with "No source file specified" on a command
line that plainly names a source. The error described the hole the
mistake left and hid the mistake.

So the usage now prints the long names, says outright that every <file>
is a path it writes and the source is the last argument on its own, and
ends with a whole example command. When the source is missing and a
file-taking option was given, the error says which options take a path
to write. The manual gains the -S row it never had, a warning in the
same words, and a sentence on what a symbol dump is for.

Documenting it twice is how it went wrong once, so docs.sh now settles
both against getopt's own option table: every option the assembler takes
has a row in the manual and a line in its own usage. Verified with
break.sh against the manual row and the usage line separately.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-05 09:40:21 -04:00
AnachronautandClaude Opus 5 f8c3db5d56 A tool for breaking things, since doing it by hand went wrong twice
A check that passes proves nothing until it has been seen to fail. Doing
that by hand failed twice in two days, and BOTH TIMES IT LOOKED LIKE A
RESULT - the suite ran, went green, and read exactly like "this check does
not catch that".

Once the edit produced code that would not compile, make failed, the exit
status was not looked at, and the previous binary ran the suite. Once the
anchor was right and the filename was wrong, so nothing was edited at all.

Neither had anything to do with header dependencies, which have always
worked: DEPFLAGS is -MMD -MP and every .d is included. What was missing
was a harness that refuses to report a result it did not earn.

So Tests/break.sh checks every step of its own work and treats anything
unexpected as a hard error rather than a green run. Not finding the break
is the answer it exists to give, and it is worthless if it can also be the
answer when the break never happened. It restores the file on the way out,
including on an interrupt.

It is not in the suite and docs.sh does not count it, for the reason
makedisks.sh is not counted turned round - but being left out of the count
is not being left out of the manual, and that gap is where a script goes
undocumented for months. So docs.sh now requires both of them to be
described, and caught this one being missing.

Also: video.sh reads the fixture disks and does not build them, so after
make sanitize clears the build directory it reported SEVEN product-looking
failures for a missing file. It builds them now and says so.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-02 13:19:22 -04:00
AnachronautandClaude Opus 5 749fef8ce2 The shell's own words, as a table and not just a chain
The dispatch is a run of "is the line this name" comparisons. That is fine to
execute and impossible to WALK, and completing a half typed command needs to
walk them - so the names have to be data as well as code.

They nearly were already: DirName through ExitName were fourteen zero
terminated strings sitting back to back, which is a table by accident of layout.
This makes it deliberate. MonitorName joins them, the run is labelled, and a
count goes underneath because a run of strings does not say where it stops.

WHAT MAKES IT A TABLE IS THE ZEROES. Each name ends in one, so the next begins
after it: no pointers, no lengths, and adding a command costs a line.

Tests/docs.sh reads both the dispatch and the run and compares them, because the
two can disagree and every way they do is quiet. A command added to the dispatch
and not to the run simply never completes, which nobody would think to check by
hand. Something put BETWEEN the strings is worse: the walk ends there and takes
every command after it, and the machine goes on working perfectly except that
Tab knows about six things instead of fifteen.

All three break that way and say something useful. Putting one byte in the
middle of the run reports that it holds ten names against the fifteen claimed,
which points at roughly where.

Groundwork for Tab completion. Nothing uses it yet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-09-01 17:21:30 -04:00
AnachronautandClaude Opus 5 bcd42e75ca Scroll the screen sideways, and by less than a cell
The screen could move one way, a cell at a time. Three registers were
missing and this adds them: a column origin so the map can be wider than
the screen as well as taller, and a pixel remainder for each axis so the
step can be one pixel rather than eight.

  0x36  Scroll column, in cells, wrapping at 128
  0x37  Fine X, 0 to 7 pixels
  0x38  Fine Y, 0 to 7 pixels

FINE DOES NOT CARRY INTO COARSE. Writing 8 to a fine register writes 0,
because only its low three bits mean anything. The alternative was for a
write of 8 to step the coarse register, and it was rejected for one reason:
a program that scrolls has to know where it has got to, and if the hardware
carries then the only way to find out is to read the register back. Keeping
them apart means the program already knows, because it did the arithmetic
itself. It is also what the machines this one is pretending to be did.

The renderer now draws one more row and one more column than fit and clips
them, because with a fine offset the screen no longer begins on a cell
boundary and the cells at two edges are partly off it.

videoPutCell follows the column origin as it has always followed the row -
a caller means a cell of the SCREEN, and the screen is a window onto the
map. The fine offsets are deliberately not applied there: they move the
finished picture by less than a cell, and there is no such thing as less
than a cell to write into. So a program may scroll to any pixel without the
console's idea of where row three, column five is moving underneath it.

Grid now scrolls diagonally, a pixel a frame, in four port writes and two
carries. It moved eight pixels every fourth frame before, which reads as
the picture jumping rather than travelling.

Seven checks, each one the same program with one register changed, so what
is compared is where the picture stopped. Breaking fine X, fine Y, the
column origin, the three-bit mask, or the console's use of the origin each
fails exactly one of them.

Grid's own two checks had to be rewritten, and the reason is worth keeping:
they asked whether pixel 4 was a grid line, which was really a check that
the scroll happened to be at a cell boundary. A picture that moves a pixel
a frame can only be asked things that are true at every offset - that it
repeats every eight pixels, and that one band of eight rows holds different
colours from the next.

Also repairs docs.sh, which found the minimal CosmOS application by taking
the first asm block in the README. Documenting a program with an example
above it made that a different block, and the check complained that the
minimal application had no #Base about something that never claimed to be
one. It looks under System Services now.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-08-30 18:52:15 -04:00
AnachronautandClaude Opus 5 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
2026-08-28 22:11:13 -04:00
AnachronautandClaude Opus 5 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
2026-08-28 21:57:09 -04:00
AnachronautandClaude Opus 5 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
2026-08-27 23:52:12 -04:00
Anachronaut 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.
2026-08-26 17:32:22 -04:00
Anachronaut 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.
2026-08-26 11:11:25 -04:00
Anachronaut 6b41354f8f Delete a comment describing a design that was removed, and check the rest
Two comments in the native assembler survived the changes that made them
false, and both are the kind that misleads rather than merely ages.

Asm.asm still explained an eighteen kilobyte buffer that the whole output
had to fit in "because a file is written in one call and there is nowhere
to put half of one" - which stopped being true when the assembler learned
to stream, and the variable it described, ImgRoom, does not exist any more.
It was sitting in front of the comment that replaced it, so the paragraph a
reader met first described the design that had been taken out. Replaced by
what is actually there: no limit but the disk, one block at a time through
a window in scratch.

scratch.asm said the system keeps below 0x1000 twelve lines above the
paragraph explaining that the system's half of Data Memory was doubled to
0x1FFF. A stale number next to its own correction is worse than a stale
number alone, because which one a reader believes is a coin toss.

docs.sh now checks both of the facts scratch.asm asserts about the machine
around it: the floor it claims the system keeps below, against the CosmOS
README, and the address its buffers start at, against where the assembler's
own data actually ends. Neither is enforced by a line of code anywhere -
the map is a comment, deliberately, because reserving the buffers would put
22K of zeroes in the file and the assembler could not load itself - so a
check is the only thing that can hold them. Both fail when broken; the
second reports the exact overlap.
2026-08-26 10:28:14 -04:00
Anachronaut aa7bdc6acd Check the memory map against itself, not only against the code
The CosmOS README's Data row gave the system 0x0000-0x3FFF and a loaded
application 0x2000 and above: two columns of one row that cannot both be
true. Program was doubled to 0x3FFF when CosmOS outgrew its first map and
that number was copied into the Data row as well, where the answer is
0x1FFF.

docs.sh measured both segments against the CosmOS column and passed the
table anyway, because it never read the column beside it. A number checked
against the code and not against the number next to it is still unchecked,
so it now reads both and compares them - and compares two further copies of
the same fact that had gone stale on their own: the minimal application in
the README, still based where applications lived before the doubling, and
the map cosmos.asm opens with, which somebody reading the system reads
before they read the README.

Each of the three checks was confirmed by breaking the fact and watching it
fail; the first reproduces exactly the text this commit removes. While in
that header, the command list said five commands and CosmOS has eleven and
a search path besides, and "dump is next" outlived the monitor.
2026-08-25 22:54:14 -04:00
AnachronautandClaude Opus 5 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
2026-08-25 17:29:43 -04:00
AnachronautandClaude Opus 5 06bdbf7728 D5: move in, and give the assembler somewhere to look
The demo disk is three directories instead of thirty nine names in one list
with cosmos.asm sitting between fileStream.asm and sbfs.asm:

  /Apps    what you run
  /Source  what you name to the assembler
  /Lib     what those include

The split is by ROLE rather than by which directory the host keeps a file in.
Everything in /Lib is named by an #Include somewhere and by nothing else, which
is what makes it a library rather than a source.

THAT LAYOUT WAS NOT POSSIBLE UNTIL NOW, and finding out why is what this rung
actually cost. An include on the machine was a bare name resolved where you
stood, so every source that calls a service had to sit in the same directory as
services.asm - which is every source worth having. The first arrangement of
this disk put the examples in a directory of their own and none of them would
assemble.

So the native assembler has a search path: beside you, then /Lib. The same rule
the shell already uses for a program it does not recognise, applied to the
thing that reads source, and the same reasoning for it being two fixed places
rather than a list - a list needs somewhere to live between one boot and the
next, and there is no such place yet. It also brings the native assembler
nearer the host one, which has searched -I directories since before there was a
machine to run this on.

The reader's per-file state grew from 293 bytes to 301, because the name it
keeps is a path now and every block of a file is asked for by it. Six of those
would no longer fit the room set aside, so the include list moved up a page.
Both numbers are written down in two places on purpose and both were changed.

dir said cosmos.asm was 17,460 bytes. It is 82,996. The size came out of the
block count's LOW BYTE shifted up and the tail beneath it, which is sixteen
bits, so anything from 256 blocks upward came back as itself less 65,536 - a
plausible number, and wrong. Files that big say their size in blocks now.
Printing the true figure wants decimal printing twenty four bits wide, which is
a page of console.asm to say something nobody reads more precisely than "big".

The Assembler Manual's line about SBFS being flat was the last thing in the
repository still claiming it, and docs.sh now looks for that phrase and three
like it in all four documents. Not a section that is wrong - one clause inside
a paragraph that is otherwise right, which is the shape this kind of staleness
takes.

The duplicate puts are gone with the wildcard that caused them, so building the
disk is quiet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-08-25 10:04:12 -04:00
AnachronautandClaude Opus 5 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
2026-08-24 22:21:02 -04:00
AnachronautandClaude Opus 5 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
2026-08-21 16:29:21 -04:00
AnachronautandClaude Opus 5 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
2026-08-21 14:44:11 -04:00
AnachronautandClaude Opus 5 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
2026-08-21 14:35:18 -04:00
AnachronautandClaude Opus 5 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
2026-08-21 14:16:54 -04:00
AnachronautandClaude Opus 5 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
2026-08-21 14:11:37 -04:00
AnachronautandClaude Opus 5 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
2026-08-20 22:13:15 -04:00
AnachronautandClaude Opus 5 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>
2026-08-19 18:19:15 -04:00
Anachronaut e3100b4718 Fixed assembler bug that caused crash on IR array resize. Added line editor app. 2026-08-17 23:26:21 -04:00
Anachronaut 1d1a14318c Programs can now list and share vectors. 2026-08-17 21:39:27 -04:00
Anachronaut 9e3425d34b Programs can now pin specific routines to specific vectors in SplitBit assembly. Added snake game. 2026-08-17 19:12:42 -04:00
Anachronaut 08624925fe Interrupt on keypress mode 2026-08-17 16:02:44 -04:00
Anachronaut 91c9d49d1b CosmOS pre-alpha and launchable application versions of old programs. 2026-08-17 15:31:49 -04:00
Anachronaut eff6902bcf Block device peripheral and SBFS file system implemented. 2026-08-16 14:03:29 -04:00