Commit Graph
40 Commits
Author SHA1 Message Date
Anachronaut 612bd1b97c B1: a boot area on the disk, reserved by arithmetic that was already there
The first rung of booting from disk. A boot area is blocks between the
superblock and the directory that the filesystem never allocates and never
sees, and NOTHING WAS ADDED TO RESERVE THEM: both implementations work out
the first usable block as directoryStart + directoryBlocks, and
directoryStart has always been a field rather than a constant. Formatting
with the directory moved up reserves everything below it. Neither allocator
changed, on either side.

Two new superblock fields in bytes that were reserved: bootBlocks at 14,
per slot, and bootSlot at 16. A disk made before this has zero in both,
which reads as "no boot area" - true, and the same shape as the version two
parent field, where the value an older disk already held was the right
answer without conversion.

TWO SLOTS, ALWAYS. A boot slot is raw blocks with no entry to rename, so
the write-a-temporary-and-rename ordering that protects every file cannot
protect it, and a machine interrupted while updating its only slot would
not boot at all - the one failure on this disk with no way back. Writing
the slot that is not live and then moving one byte makes that a machine
that boots what it had before.

bootBlocks and directoryStart say the same thing from two sides, so a disk
where they disagree is refused rather than guessed at, as is one naming a
slot that does not exist.

Checked where it matters: the HOST formats a disk with a boot area and the
MACHINE fills it, then the reserved blocks are compared against zero. The
machine's allocator is the one that had no idea any of this was happening,
which is what makes that the check worth having. Six host checks besides,
including both halves of the superblock disagreeing.
2026-08-26 22:58:45 -04:00
Anachronaut 0a2965bc63 Name the rules, say when there is nothing to say, and hold a baseline
Four things SplitLint wanted, and they build on each other.

EVERY WARNING NAMES ITS RULE, in brackets at the end the way a compiler
names the flag that produced it. Twelve rules, listed by --help. That makes
the other three possible: suppressions can name one rule and leave the line
honest about the others, the harness can assert on a rule's identity rather
than on the wording of its message, and --machine can print one tab
separated line per warning - file, line, rule, message, help - so nothing
downstream reads prose. This file's own output was parsed with regular
expressions three times in one day before it had a shape to rely on.

A CLEAN RUN SAYS SO:

  No style warnings: 121 files checked against 12 rules.

It used to exit in silence, which does not tell you it found nothing - it
tells you nothing at all, and from outside the two are identical.

A MARKER THAT SILENCES NOTHING IS ITSELF REPORTED, as dead-suppression. An
exception that outlived whatever made it necessary is the thing the
required reason exists to prevent, and naming the wrong rule now gets you
both the warning you meant to silence and a note that your suppression is
doing nothing.

AND THE CORPUS IS HELD TO A BASELINE. Sixty one warnings are left in it
deliberately and nothing stopped a sixty second. Tests/lint-baseline.txt
records how many of each rule each file should produce, so a new one fails
make test while the sixty one stay quiet; confirmed by adding an INIA 0d0
to Say.asm and watching it name the file, the rule and the count. It counts
per file and rule rather than recording line numbers, because line numbers
would churn the whole baseline whenever anything was inserted above a
warning - the same reason cycle counts are stripped from recorded output
here. ./Tests/lint.sh --bless records it again.

One thing to know for next time: the rule name was inserted before the line
number at all twenty one call sites, and the signature was changed to match
rather than the twenty one call sites being fixed. (path, rule, line) reads
no worse than (path, line, rule) and one edit has fewer ways to go wrong
than twenty one.
2026-08-26 21:00:21 -04:00
Anachronaut c146d98588 Stop the linter recommending a change that a faster helper would break
SplitLint knew that CALL restores A, B and Data Pointers 0 to 2, so a
pointer set before a call is still set after it. That is true, and it made
the tool give advice that was correct today and unsafe to take.

Of the 178 redundant SETDs it found across the corpus, 122 were redundant
ONLY because of that restore - the shape is everywhere, because it is how a
helper is given its arguments:

  SETD.0 SbfsBlock
  SETD.2 SbfsFileStart
  CALL sbfsSetWord
  SETD.0 SbfsBlock        <- flagged

Removing that last line is right until sbfsSetWord is reached with RCAL,
which restores nothing - and that is not hypothetical, it is what RCAL was
added to this machine for, measured at close to halving the assembler's
memory traffic. The failure would also be silent from the linter's side: it
forgets everything across an RCAL, so it would stop reporting while the
removals stayed removed.

So a claim now ends at any call, for pointers and for registers, the way a
claim about carry already did. 257 warnings become 127, and the redundant
SETDs 178 become 54 - which is exactly the number an independent count of
"no CALL in between" had arrived at separately.

The fixture gained a SETD and an INIA repeated across a CALL, which must
stay quiet, and the harness fails with the old behaviour put back. Two
mistakes worth recording: the new expectations first pointed at the LABEL
above the repeats rather than the repeats, which passes for free because
nothing ever warns about a label; and the block landed in the middle of
another check's comment, leaving that comment describing the code below it
instead of its own.
2026-08-26 17:42:56 -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 2b079324ae Add SplitLint, and let it be told when something is deliberate
SplitLint reports valid assembly that has a shorter direct expression:
zero loads that could be RSTA or RSTB, Q moved through the stack where MVQA
would do, self-cancelling push and pop pairs, assignments overwritten
before use, unreachable fallthrough, one-byte pointer moves that could be
INCD or DECD, a branch to the label directly below it, a SETD reloading an
address the pointer already holds, and branches whose carry is known.

Its model is deliberately local and conservative: every label and every
directive forgets all known state, so a claim only ever lives inside a
straight-line region. It knows the calling convention - CALL forgets DP3
and keeps the rest, RCAL and SWI forget everything - and it shares
assembly.o with the assembler, so an added opcode cannot leave it holding a
private copy of the instruction table.

260 warnings across the corpus, of which three were wrong in the way that
matters: branchTest.asm and interruptFlagTest.asm exist to check that a
branch whose carry is known behaves correctly, so a diagnostic saying the
outcome is known is exactly right and exactly unwanted.

A line whose comment says "splitlint: <reason>" is now not reported on. THE
REASON IS REQUIRED and a bare marker is refused, because a suppression
nobody explained outlives whatever made it necessary. Suppressed warnings
are not counted, so --fatal-warnings does not fail on one, and the number
of them is printed at the end so the claim is visible rather than silent.

Tests/lint.sh checked a TOTAL: twenty three warnings expected, twenty three
found. That number stays right while the thing behind it goes wrong - a
rule that stopped firing while another fired twice would pass, and so would
a rule reporting at the wrong line. It now checks which warning came out
and at which line, that nothing else came out, and that the four lines
meant to stay quiet did. Confirmed by breaking one rule's message and
watching it name that rule: the old assertion passed the same sabotage,
because the warning still fired and the count never moved.

Written with the user while I was away; my part is the suppression
mechanism, the harness rewrite, and the three marks in the test programs.
2026-08-26 17:23:17 -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
AnachronautandClaude Opus 5 d4cba36c5e Devices that take time, and a filesystem that waits for one
The disk's status has always had a bit meaning "still going", and the header
beside it has always said to honour it. Nothing did, because nothing could: the
host finished the transfer inside the instruction that asked for it, so the bit
could never be seen up and asking about it was asking about something that
cannot happen.

--disk-cycles gives it a latency. The command is still checked at once, because
a refusal is not work - a block that is not there fails before any head moves -
but the transfer is remembered and done when the machine has run that far. Until
then the buffer holds the block BEFORE this one.

That last part is the point. A program that does not wait gets the wrong bytes
rather than an error, which is the failure the bit exists to prevent and the one
that would never have shown up. With a latency of two thousand, CosmOS could not
even mount: sbfsMount reads block zero and looks straight at the buffer.

deviceTick is the general shape rather than a disk feature. Called once per
instruction with the machine's clock, it lets anything whose moment has come
finish - which is what a display that refreshes, or a port that waits on the
host, would want in exactly the same way.

The filesystem watches the bit now, in one small routine reached with RCAL. That
is not decoration: what it hands back is the settled status in A, and CALL puts A
back the way it found it, so an ordinary call cannot carry the one thing this
exists to carry. Two bytes of Stack rather than ten, in a routine that runs on
every block the machine ever touches - the first place in the system where the
new call is the right one rather than merely a cheaper one.

The manifest takes a @N after a disk, the way it already takes :ro, so a test can
ask for a slow one. cosmosSlowDisk lists a directory at two thousand cycles a
block and gets the same listing as everything else, which is the whole assertion:
a filesystem that did not wait would print nonsense rather than fail.

Zero is the default and every other test runs at it. What waiting costs, on a
directory heavy run: 229k cycles at zero, 275k at five hundred, 415k at two
thousand, 1.16M at ten thousand.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-08-25 21:29:57 -04:00
AnachronautandClaude Opus 5 e0cf0a9a25 Charge the memory controller for the memory it moves
A blit cost ten cycles, which were the five port writes that set it up. The
quarter of a kilobyte that moved cost nothing, and no hardware moves a quarter
of a kilobyte for nothing.

BANKS ARE SEPARATE MEMORIES, AND THAT IS WHAT SETS THE RATE. A move between two
of them can overlap its read and its write - fetch the next byte while the last
one is stored - so it settles at a byte a cycle. A move within one bank cannot,
and costs two. A fill has nothing to read and costs one whatever the banks are.
The odd cycle on each is the pipeline filling.

That is not a modelling choice so much as a reading of the structure the machine
already has: a Program to Data blit is inherently twice the rate of a Data to
Data one, and it is legible why.

Measured: 256 bytes is 297 cycles across banks and 518 within one, both
including the instructions that ask for it.

WHAT IT TAUGHT, which was not what I expected. Charging for movement costs the
native assembler 0.4 per cent and costs directory work 13.4. The assembler reads
a block and then thinks about it for a long time, so the move is amortised into
nothing; the filesystem reads a block in order to look at it and does nothing
else in between.

So the case for a blitter that runs alongside the CPU is weaker than it sounds.
Concurrency pays when there is other work to do during the transfer, and the
place that spends its time moving memory is exactly the place with nothing else
to do - it blits a block precisely so that it can read it. What that workload
wants is a FASTER controller, not a concurrent one: a wider data path halves the
wait, and the machine is waiting either way.

Video is the case that would still want concurrency, since a frame can be moved
while the next one is worked out. That is an argument about software nobody has
written yet, and it is now an argument with numbers on the other side of it.

The byte at a time port is charged too, for the byte it moves beyond reaching
the port. Nothing polls CTRL_STATUS, so the transfer stalls whoever asked for
it, which is the conservative reading and the one the software already assumes.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-08-25 21:11:47 -04:00
AnachronautandClaude Opus 5 f1e5cc46f6 A cycle is an access to memory, not an instruction
cycleCount used to tick once per instruction, so RSTA cost what SETD cost and a
CALL moving ten bytes of Stack cost what a branch cost. No machine anybody could
build works that way, and the emulator's job is to be the thing the hardware is
designed against.

Every touch of memory now goes through one of four accessors that charge for it:
fetching an opcode, fetching the bytes after it, reading or writing Data Memory,
and reaching a device port. One access, one cycle, nothing overlapped. The
accessors exist so the cost is counted where the access happens rather than in a
table of per instruction costs kept somewhere else - a table like that is a
second copy of what the code does, and the two drift.

The run loop spends a budget of cycles instead of running a count of
instructions, so the emulated rate means something: an instruction costs what it
touches, and a batch ends when the cycles are gone.

What the numbers say now: RSTA 1 and SETD 4, being one byte and four. LDA 3,
DPUA 2. CALL and RET together 24, RCAL and RRET together 8, because the first
pair moves twenty bytes of Stack and the second moves four. The average SplitBit
instruction costs 3.72 of these, measured over the native assembler assembling a
program.

And the measurement that prompted all of this: converting the filesystem's
hottest leaf routine to RCAL is 3.1 per cent cheaper on a directory heavy
workload. The old model said 0.0, which is what a model that cannot see memory
traffic must say about a change that is nothing else.

Three tests moved. settle() strips the cycle count from recorded output, so
nothing should have churned - but it was anchored to the start of a line and
replCalculator's last output has no newline on it, which leaves the halt message
mid line where the pattern never reached. Not anchored any more.

The two Life programs are bounded by a cycle count because they never end, and
that number was rescaled from 3,000,000 to 11,200,000 - the same amount of work
at 3.72 cycles to the instruction. Nothing about either program changed. No limit
reproduces the old output exactly, because the cut now lands elsewhere in a
frame, so they are recorded again rather than tuned to match.

Whether hardware overlaps a fetch with the end of the previous instruction is
left open on purpose. This is the conservative model; pipelining is a decision to
make while drawing the hardware, not one to inherit from an emulator.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-08-25 20:44:38 -04:00
AnachronautandClaude Opus 5 19ab36a201 The assembler can say where everything ended up
-S writes every label and the address it was given, in address order.

Nothing else knows that. A program on the disk is bytes; the monitor can
disassemble it but has no idea what any of it is called. So counting which
addresses a program calls says a great deal and names nothing - the answer
arrives as a column of numbers and somebody works out by hand which routine
each one is inside.

It was deferred when the native assembler was planned, as a listing and symbol
dump nobody needed yet. Finding out where the assembler spends its time is what
needed it: the top six call targets were addresses until this existed and are
numStep, numCompare, tokGet, srcNext, numAddByte and clsSameName with it.

Sorted by address rather than by name, because the question asked of it is
always "what is at this address".

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-08-25 18:15:50 -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 da91a36d92 D4: the machine makes directories too
mkdir and rmdir are the machine's own now, and a file goes where its path says
rather than always in the root. A disk can be organised without the host tool
touching it.

Everything below the surface works in terms of a directory and a name rather
than a path. sbfsWalkParent splits the last name off, walks the rest, and hands
back the two - and the separator stays on the end of the head, which is what
makes one rule cover every kind of path: "/x" leaves "/", which is the root;
"x" leaves nothing, which is where the machine already is; and "A/x" leaves
"A/", which is neither and needs no special case to say so.

Saving works in those two as well, and had to. The careful order a save uses -
make a temporary, write it, delete the original, rename the temporary - only
works if the temporary is made in the SAME directory as the file, because the
rename at the end changes a name and does not move anything. Renaming to a path
naming a different directory is refused for that reason, rather than quietly
being a lie the disk goes along with.

Three things this cost, all found by running it:

mkdir Apps/Deep made /Apps/Apps. The leaf was worked out into SbfsWanted and
then the head was walked - and walking goes through sbfsPathNext, which puts
every name it meets into SbfsWanted on the way past. The head's last name
landed exactly where the leaf was. It has somewhere of its own now.

rmdir took a directory with something still in it, which is the one failure the
whole design is arranged to prevent. Looking for children clobbered DP2 and
rebuilt it from the buffer and the entry count with the subtraction the wrong
way round, so the pointer walked off the end of the block and found nothing.
The comparison goes through a CALL now, like the two beside it, and DP2 comes
back on the entry because a RET puts it there. SplitDisk's "in use but not
reachable from the root" line is what caught it.

Refusing a name longer than twenty two used to read the twenty third character
of a shorter one, which is somebody else's string. It is measured now.

Tests/agree.sh is new and is the gate this rung was for: the same disk built
twice, once with SplitDisk and once with CosmOS, compared byte for byte. The
two share no code and only a written specification, and every field one writes
and the other only reads is checked there and nowhere else - which entry a
thing lands in, which block, what a directory's unused fields hold, the
version, the free count. It caught a wrong parent immediately when that was
broken on purpose.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
2026-08-25 09:18:29 -04:00
AnachronautandClaude Opus 5 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
2026-08-24 18:41:11 -04:00
AnachronautandClaude Opus 5 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
2026-08-21 17:06:46 -04:00
AnachronautandClaude Opus 5 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
2026-08-21 16:51:03 -04:00
AnachronautandClaude Opus 5 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
2026-08-21 16:47:13 -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 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
2026-08-21 14:54:51 -04:00
AnachronautandClaude Opus 5 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
2026-08-21 14:50:03 -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 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
2026-08-21 13:46:59 -04:00
AnachronautandClaude Opus 5 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
2026-08-21 13:38:47 -04:00
AnachronautandClaude Opus 5 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
2026-08-21 12:03:31 -04:00
AnachronautandClaude Opus 5 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
2026-08-21 11:50:52 -04:00
AnachronautandClaude Opus 5 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
2026-08-20 22:50:39 -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 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
2026-08-20 21:19:36 -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 eff6902bcf Block device peripheral and SBFS file system implemented. 2026-08-16 14:03:29 -04:00
Anachronaut 04dfcd707b Memory controller implemented. 2026-08-15 20:43:53 -04:00
Anachronaut 6d1966d500 Interrupt system implemented, some new programs. 2026-08-15 00:44:13 -04:00
Anachronaut 638b68b25c Long standing assembler bugs fixed, new path system. Make compatibility update. 2026-08-14 16:53:28 -04:00
Anachronaut c2440ae5fa Various bug fixes to assembler, added more data pointers. 2026-08-13 23:41:22 -04:00
Anachronaut 3809aed5eb Update README.md 2024-10-26 15:39:31 -04:00
Anachronaut 81b364394c Update README.md 2024-10-26 15:38:20 -04:00
Anachronaut dfa5ad2638 Assembler Completed
Added the Assembler.
Added instructions for the assembler to README.md
Added Assembler Manual
Modified Makefile to build the Assembler.
2024-10-26 15:31:24 -04:00
Anachronaut dae900eaf4 Update README.md 2024-10-16 23:21:10 -04:00
Anachronaut be90f8e500 Update README.md
Fixed typo.
2024-10-16 23:16:19 -04:00
Anachronaut f611177a66 Create README.md 2024-10-16 23:11:08 -04:00