./SplitBit --disk system.img
stage two
CosmOS
>
No boot image named. The emulator shadows its built in stage one into
Program Memory - boot vector included - and the CPU then does exactly what
it has always done: reads the boot vector and starts where it points.
NOTHING ABOUT THE CPU CHANGED to make a machine that starts itself, which
is what picking shadowing over a mapped ROM bought.
The ROM is generated from Programs/Boot/stage1.asm by the makefile rather
than committed beside it, because a copy of a program kept next to the
program is a copy that goes stale. That makes the assembler a real
dependency of the emulator, which it always sort of was and now says so.
od and awk rather than xxd, which is not everywhere, or python, which the
README does not ask anybody to install in order to build this.
loadROM is loadFile given bytes instead of a path: both go through one
reader over an fmemopen stream, because a ROM is a boot image and there is
no reason for the machine to have two ways of understanding one.
Naming an image still works and is what every other test here does. That
path is not a shortcut to apologise for - placing memory from outside is a
real thing real machines allow, and it is a debugger. The help says so now.
No image and no disk is the one case with nothing to run, and it says that
rather than printing a usage message about a missing file.
run.sh gained a "rom" mode which hands the emulator a disk and nothing
else. The source column still names stage1.asm, because that is what is IN
the ROM: assembling it there says the thing the emulator carries is a thing
that still assembles.
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.
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
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
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
Found by a review, all three confirmed by trying them rather than by
reading.
1. THE STRICT BUILD CLAIM WAS FALSE. The README says the sources build clean
under -std=c11 -pedantic with -Wall -Wextra, and they did not: realpath is
an XSI interface, and _POSIX_C_SOURCE=200809L does not reach it, so the
assembler would not compile. The ordinary -Os build never saw it, because
without -std=c11 the compiler's own default declares realpath anyway.
_XOPEN_SOURCE=700 is POSIX.1-2008 plus XSI, and covers every file on its
own. Narrowed by compiling each source with each candidate macro rather
than by adding one and hoping.
And 'make strict' now checks it, as part of 'make test'. The README makes
a claim somebody may check by typing it, so the suite types it. Verified
the check bites by putting the old macro back.
2. THE COSMOS README HAD NOT CAUGHT UP WITH THIS WEEK. It said there were no
breakpoints and proposed writing a spare byte over an instruction - two
commits after SWI osBreak was built, which does it without overwriting
anything and is described correctly further down the same file. Its
Current Scope said there was no native assembler and that self-hosting
was the intended long-term milestone. And the streaming section spoke of
a machine "one day going to assemble itself".
All three now say what is true. Self-hosting is described as done, with
what is left of it named: a linker, and an editor that knows what
assembly is.
3. 'make run-hello' DID NOT WORK, and it is the one command the makefile's
own header advertises. It was a pattern rule against $(BUILD)/%.bin,
which worked while every program sat at the top of Programs/ and broke
the moment they were filed into Examples/ - which I did, four commits ago,
without trying it.
The name is looked up among the programs now, so it is the program's name
rather than its path, and an unknown one lists what there is instead of
saying "No rule to make target".
The same sweep was run across all four documents for other claims this week
invalidated. The remaining "not yet" phrases are about faults that genuinely
are not defined yet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
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