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
-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
Dispatching through a vector with nothing in it was the one fault this machine
could not hand over, because the thing that would hand it over is the thing that
has just found nothing to hand it to. It stopped the machine and no program
could do anything about it - so calling a service the system does not implement
was fatal, and that is an ordinary mistake to make.
Two new fault vectors: 5 when a software vector was empty, 6 when a device
interrupted and its hardware entry was. Separate, because they are separate
mistakes with separate fixes - one is a program calling something that is not
there, the other a program that asked to be interrupted and forgot the handler.
WHICH ENTRY WAS EMPTY ARRIVES IN Q, and it is the only thing on this machine a
handler is given in a register. Not a fault cause register by another route: the
vector still says what happened and Q says which of the 256 entries it happened
about, which is a parameter and not a cause. It costs no new state at all,
because the frame already saved the Q the interrupted program had and RETI puts
it back.
The escalation happens once. If vector 5 or 6 is itself empty the machine stops
the way it always did, having genuinely run out of places to go.
swiFaultTest is what guards that, and it was written long before any of this: it
installs nothing, so it must still get the old halt. Breaking the escalation
fails the two new tests and not that one; making the escalation unbounded fails
that one and not the two new ones. Each break fails exactly the half it belongs
to.
noDeviceTest is fed no input on purpose. The console raises its line once when
input ENDS as well as when a byte arrives - which exists so a program driven by
interrupts is told when nothing more is coming - so with no input at all, that
end is what turns up.
Groundwork for CosmOS's fault screen, which wanted to catch these two and could
not.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
Programs/makefile is gone and everything it did is here. The split was not
paying for itself: nothing at this level ever ran that file, so it rotted,
and all three bugs found in it this week share that one cause. It named two
source files that had been renamed months earlier and failed outright. The
disk did not depend on the tree it mirrors, so a new file silently was not
on it. And the disk was in no default target, so 'make clean' threw it away
and 'make' did not bring it back - which is what prompted this.
The platform and the system stay separate, as TARGETS rather than as files:
'make SplitBit Assembler' builds the machine and its tools, 'make cosmos'
and 'make disk' build the system, and somebody who wants to write their own
system can ignore the second group entirely. That boundary is now one make
enforces, which the directory boundary never did - Tests/makedisks.sh has
always reached across it to build its own fixtures.
'make' now builds a bootable disk as well as the tools, because a machine
with nothing in the drive does not do anything and the first thing anybody
wants after building this is to watch CosmOS come up. It costs half a
second: the whole system, twenty six apps and the native assembler assemble
in less time than the emulator links.
Two things the merge needed that the split did not. Assembling now takes an
order-only dependency on the assembler, which came free when you had already
built the tools before changing directory. And the mirror is told which
directory to walk: it was ".", meaning Programs/, and left alone it would
have meant the whole repository - the C sources, the tests and the manuals
mirrored onto a disk for an 8-bit machine.
The disk comes out at 188 files where it was 189. The one that went is
Programs/makefile, which was itself being mirrored onto it as
/Source/makefile.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
Three blocks move and nothing else changes. Branches take 0x60, subroutines
take 0x70, and the ALU moves up into the 0x10 block the two of them used to
share. Order within each block is preserved exactly - this relocates them,
it does not rethink them.
WHAT IT BUYS IS AN EMPTY 0x00 TO 0x0F. Program Memory that was never
written, or a load that stopped part way and left zeroes in its tail, used
to read as a long run of ADDs: the machine carried on through them, arrived
somewhere unpredictable, and whatever broke there was a long way from the
byte that caused it. Now it faults where it is met:
Fault: 0x00 at Program Address 0x0004 is not an instruction.
That is the address of the byte after the last real instruction, which is
the difference between a diagnosis and a search. Reserving the whole nibble
rather than just 0x00 means a run into blank memory faults wherever it
starts rather than only when it lands on the right byte. runOffTest records
it, and the block is left empty for whatever turns out to want it.
The other half is room: branches and subroutines had filled 0x10 to 0x1F
between them, so a service return that keeps Q and DP3 had nowhere to sit
next to its family. It has 0x76 waiting now.
Five places wrote an opcode down that the scripted remap did not reach, and
four of them were found by tests rather than by looking:
- secondPass.c lists which opcodes take an address, and firstPass.c knows
SWI by number. Missing those made XOR read as a branch.
- Asm.asm knows SWI by number too, being the other assembler. Missing it
made the native and host assemblers disagree byte for byte, which is
exactly the check that exists to catch a thing known in two places.
- loaderTest.asm carries a hand written payload, and its RETI was 0x19. To
the assembler those are numbers and to the program they are data, so
nothing but running it could notice. It says so in a comment now.
- The Assembler Manual prints the bytes hello.asm assembles to, and two of
them were branches.
The monitor's recorded disassembly moved by exactly the bytes it should:
18 became 72 wherever SWI appears, with SETD and INIB untouched and every
disassembled line still reading the same.
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
Includes two edits of Anachronaut's: dropping a "See the Assembler Manual"
from text that is now IN the Assembler Manual, and tightening a sentence
about when a vector number has to be agreed.
The first of those was mine to have caught. Moving "Loading A Program From
A Disk" into that manual turned a cross reference into a document telling
you to go and read itself. I checked for dangling references before the
move and found none, because I checked for references by SECTION NAME and
not for references to the manuals themselves.
Checked properly now, both directions: no other document refers to itself,
and nothing that moved says "above" or "below" about something that ended up
in a different file.
The three remaining directional references are gone with them - two that
were correct today and one, "See Numbers You Write Down below", pointing at
a section twenty four lines further down that nothing guaranteed would stay
there. A reference that carries a direction is a reference that goes wrong
the next time anything moves, and this week moved a great deal.
There are none left anywhere in the repository.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
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
"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
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
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
"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
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
> 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
> 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
> 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
'make run-cosmos' now boots a machine with Asm.sbx on the disk and two
source files to point it at, so the thing can be watched working rather
than only tested:
> load Asm.sbx
> run hello.asm
wrote hello.bin: program 17, data 14, labels 2
> run strings.asm
wrote strings.bin: program 62, data 88, labels 4
Both come out byte for byte identical to the C assembler's, and both run.
hello.asm and testPrograms/stringKeyword.asm are the two single-file
programs with no #Include, which is what the native assembler handles so
far; strings.asm is the second because it has a subroutine and a label
used before it is defined, so it exercises a forward reference that
hello.asm does not.
The assembler builds from its own directory rather than from Apps/,
because it is not one file. Its pieces are found beside it without being
told, since an include is looked for next to the file that asked for it
before anywhere else; only services.asm needs the include path.
ALSO A CORRECTION. Asm.asm's header, its usage message and the Assembler
Manual all said 'run Asm.sbx hello.asm'. That is the convention we talked
about wanting later, not the one this shell has: load and run are separate
commands, so the whole rest of the run line is the argument and that form
asks for a file called "Asm.sbx hello.asm". All three now say load first.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
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