54f5cfe8a40bbc49e7c6c4826b4cab0d77dff363
11
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
a7d3e09d94 |
Refuse a streamed file that commits more than it reserved
osFileStart sets an extent aside and osFileWrite refuses a block index outside it, so writing off the end was already barred. Committing a larger size was not, and reaches the same neighbour by simply claiming it: a directory entry is the only record of what a file owns, so an entry claiming a block it was never given owns it, and so does whatever owned it before. Both files then look perfectly well formed. The free count went backwards past zero on the same path. Found by ChatGPT's review of the streaming work, in NOTES.md. I had bounded the index because writing off the end was the obvious way to reach a neighbour, and had not noticed that the other end of the same reservation was open. THE SIZE IS COMPARED, NOT THE ROOM IT TAKES UP. One block and a tail occupies exactly what two whole blocks occupy, so bounding the blocks alone would let a file reserve the first, commit the second, claim no block it was not given, and still report two hundred and forty six bytes that were never written to it - whatever the disk had there before. Checked before anything is touched, which is why the temporary is found twice. The old file is deleted a few lines down and a refusal after that point would have destroyed the thing it was protecting. AND IT CAUGHT A REAL ONE IMMEDIATELY. The assembler reserves the file plus room for its vectors, and asked for four bytes per vector DECLARED - which looks like a safe bound and is not, because a device is declared during the SECOND pass, in the line that implements it. A program with a device installs a vector that was not counted when the room was measured. CosmOS reserved 14,163 bytes and committed 14,167, writing four bytes past what it had been given on every build since S2. It landed inside the last block it owned, and would not have if the boundary had fallen four bytes earlier. It reserves against the vector table's LIMIT now, which cannot go stale whenever things are counted. Claim.asm is the program that tries it: reserve one block and a tail of ten, write them, then tell osFileDone the file came to two whole blocks. The refusal and the honest commit that follows are both recorded. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
fb7b224bbb |
S2: the assembler writes the file as it makes it
The output image is gone. It was eighteen kilobytes and it is now one block of window, because the file was always produced in order and only ever needed to be written that way. Everything works in FILE OFFSETS now. A cursor is a two byte number counting from the front of the file, and since a block is two hundred and fifty six bytes, the block it lands in is the offset's high byte and the place within that block is its low one - so there is no division anywhere, and ImgWalk, ProgPut and DataPut needed no change but where they start. ONE WINDOW RATHER THAN THREE. The plan said three: one per segment, and a third for the block where the program ends and the data begins, which belongs to both. Fetching a block back instead makes all of that one case. The header is patched after every byte is out, the boundary block is written by both cursors, and both are simply revisits - a revisit is what fetching handles. osFileFetch is the service that allows it, and is the read side of the write. A run of bytes in one segment costs nothing extra; a switch between segments costs two block operations, and a source file has a few dozen switches and several thousand bytes. Two bugs, both a pointer meaning two things: putAt took the cursor to advance in DP2 and then wanted DP2 for the window's address. A call puts DP2 back the way it was AT THE CALL, so the step at the end moved whatever the last call had left there - the window walked off across memory while the cursor stood still. It goes in memory now, like the block did in S1, and for the same reason. The size the file is created at could not be right. How many vectors are actually installed is not known until the second pass has resolved their handlers, and by then the file must already exist to be written into - so Keys, which brings one vector, came out four bytes short. Teaching the first pass to count them meant teaching it about devices, and about a Boot line in a loadable program not being installed at all, which is two ways to disagree with the second pass about what a file contains. So osFileDone is told the size instead. A writer asks for as much as the file could possibly come to - the whole of it plus four bytes for every vector DECLARED, which no file can exceed - and says what it really came to at the end. The blocks it did not use go back to the free count. Asking for too much costs a moment; asking for too little writes off the end of a file. That is a better service for it, not a workaround. A writer that cannot know its size until the last byte is the ordinary case, and it is exactly the case this whole rung exists for. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
9f7dffdeca |
S1: the write side learns to stream
osFileStart, osFileWrite and osFileDone are the mirror of osFileInfo and osFileBlock. A program can now write a file it never holds: Pour writes twelve blocks and a tail while keeping 256 bytes of it at a time, and the host tool reads all 3,112 bytes back with every block where it was put. ONE WRITE IS OPEN AT A TIME AND COSMOS HOLDS IT. Reading needs no state - a name and an index are the whole question - but writing safely does, because the new file has to exist before the old one is thrown away and something has to remember which temporary belongs to which name. Keeping that here means the careful order is written once instead of in every program that streams. Nothing already on the disk is touched until osFileDone, so a disk without room says so while the old file is still there. That is stronger than osFileSave can manage, where the size is only known once the caller has every byte in hand. osFileSave stays: Edit and Files hand over whole documents and have no reason to learn any of this. osFileWrite refuses an index past the end of the file, and that refusal is not politeness. Files are contiguous, so block nine of a three block file is a real block belonging to something else, and writing it would put one file's bytes inside another with nothing anywhere saying so. Checked both ways: the tail block is allowed and the one past it is not. Three bugs, all of them the same shape - a register or pointer used for two things at once: DP3 carried the block count in and was popped high byte first, which is the wrong way round from every reader in the system and made the count two hundred and fifty six times too big. sbfsStreamStart took the name in DP0 and then wanted DP0 for something else before it had read it, so it walked whatever it last pointed at and reported that it could find no room. sbfsStreamWrite kept the caller's block in DP3 across a find - DP3 being the pointer a return does not put back, which is exactly why the find uses it too. What went to the disk was whatever the scan last looked at. It goes in memory now, and the file is correct because every block says which block it is; a check on the length alone would have passed all three of these. Writing no longer finds the file for each block either. Nothing moves a file once it is made, so where it starts is settled when the temporary is created. That was not even slow - a scan stops the moment it matches - but it was a walk of the directory per block for an answer that cannot change, and it is 28 per cent of the cost of writing forty blocks. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
588e02aff5 |
Double CosmOS's half of the machine, and check that it fits
The memory map gave CosmOS 0x0000 through 0x1FFF of Program Memory and applications 0x2000 and above. CosmOS is 8141 bytes at the previous commit, which is fifty one bytes short of the line, and the next thing added to it went over. GOING OVER DOES NOT FAIL WHERE IT HAPPENS. Nothing enforces the division: an application says where it goes with #Base and the loader puts it there, so a CosmOS that has grown past 0x1FFF simply has the next program loaded written over the end of it. What breaks is whichever part of the shell that program happened to cover, at whatever later moment somebody uses it. It turned up here as the monitor's assemble command answering "I do not know" to valid instructions, several commands into a session, on a machine that had booted perfectly well. Both halves are doubled: applications now start at 0x4000 in Program Memory and 0x2000 in Data Memory. That is 16K of code and 8K of data for the system, against the 8775 and 2948 it uses today. Both were on the same trajectory, and moving them together means the twenty files that say #Base are edited once rather than twice. The standalone loader's loadable.asm keeps its old base: it belongs to the loader CosmOS grew out of, not to CosmOS, and its addresses answer to a different program. The unbased-segment diagnostic keeps its old base too - it exists to produce an error message that names the address, and the message is what is recorded. Tests/docs.sh now reads the two limits out of the table in the README and measures both segments against them. It reads them rather than being told them because the table is the specification, and this is the second time in this project that the thing nobody checked is the thing that rotted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
4fd8bf7b3f |
Step a Data Pointer with INCD and DECD, not DPUP and DPDN by one
DPUP takes an immediate, so an offset of one is legal and does exactly the right thing. It is also three bytes where INCD is two, and reads as "offset the pointer up by one" where INCD reads as "step the pointer". 56 of them across 15 files: the system, the assembler, the editor, and eight test programs. CosmOS is 9,564 bytes to 9,537, the native assembler 11,648 to 11,635, and every program in the repository together 49 bytes lighter. The worst offender was numbers.asm, written this week, where every sixteen bit helper reaches the low byte and comes back the long way round. It is the file every other part of the assembler includes, so it is the first thing anybody reads when they go looking - and it was teaching them the long way. Pattern matched off sbfs.asm rather than off the instruction table I had just embedded in two programs. THIS IS NOT TWO WAYS TO DO ONE THING. DPUP takes an arbitrary number, so one is inevitably among them; INCD earns its place by making the common case a byte cheaper. The overlap is structural and the choice is a usage question, which is a linter's job rather than an ISA's - "DPUP.n 0d01: INCD.n does this in a byte less" is a mechanical rule with no judgement in it. Nothing needed re-recording, which was not a foregone conclusion: cosmosBreak prints the system addresses the registers happened to hold, and they did not move. Both assemblers still produce identical bytes and CosmOS still builds itself to a fixed point. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
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 |
||
|
|
fb335681d2 |
M4: SplitBit assembles SplitBit, and then assembles itself
> load Asm.sbx
> run cosmos.asm
wrote cosmos.bin: program 7036, data 2448, labels 475
> run Asm.asm
wrote Asm.sbx: program 7533, data 4099, labels 555
Both byte for byte identical to what the host assembler builds from the
same source. The machine now builds the operating system it is running on,
and builds the thing that built it.
THE CHECK THAT MATTERS MOST IS THE THIRD ONE. A binary that matches could
still have come from an assembler wrong in some way this particular source
happens not to exercise. So Tests/native.sh boots the CosmOS that CosmOS
built and has THAT assemble CosmOS again - and the second generation is
identical to the first, down to the cycle count. It is a fixed point: the
machinery has been through itself. After this the host is a convenience
rather than a necessity.
WHAT STOOD IN THE WAY was not the assembler. It loaded, faulted at 7,780
cycles, and the fault was in CosmOS: a loaded program is staged at 0x8000
before being blitted into place, so the whole FILE has to fit in the 32,768
bytes above it. The assembler's file was 33,983, and 22K of that was
zeroed scratch buffers - because #Reserve emits what it reserves.
None of that is initialised data. It is scratch, wanted only while the
assembler runs, and while it runs everything above its own data is free.
So the buffers are a MAP now rather than declarations - Assembler/scratch.asm
writes down six addresses and the file carries none of it. 33,983 bytes
became 11,648, and the assembler could load itself.
The map has a file of its own because the reader and the label table both
need addresses out of it while neither includes the other.
The sizes are cut to the largest thing it is asked to build, and that turns
out not to be the operating system: the assembler is 555 labels and 11,648
bytes of output against CosmOS's 475 and 9,564. The hardest thing this
assembles is itself.
Also: sizing it for CosmOS meant raising the label table, and raising the
label table is what pushed the file over the staging limit. The two facts
only met because the first one was tried.
Speed, measured rather than guessed: CosmOS takes 80,168,646 cycles, which
is eighty seconds of emulated time and under a second under --fast. Most of
it is a straight walk of 475 label names, several thousand times. Sorting
or bucketing that is easy and was deliberately not written before there was
something to measure.
make run-cosmos now puts every source file on the disk, so the whole thing
can be done rather than read about.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
|
||
|
|
20989c3439 |
M3: programs that bring their own vectors
> load Asm.sbx
> run Keys.asm
wrote Keys.sbx: program 558, data 85, labels 52
> load Keys.sbx
> run
keys, by interrupt. q stops.
ab
the console has been handed back
The machine assembles a program carrying an interrupt handler, the loader
installs its vector, the console interrupts into it, and the shell takes the
vector back at exit. Byte for byte identical to the C assembler's, and
Tests/native.sh now checks a boot image and four loadable programs on every
run.
WHAT IT TOOK:
A declaration and an implementation are the SAME ENTRY. services.asm says
a service is called osPrintString and has number 16; cosmos.asm says
osPrintString is handled by handlePrintString. The name is met twice and
the second time fills in the handler, which is what lets one shared file
serve both the caller and the implementer.
So the first pass declares and the second implements. That is forced: a
handler is an address, and no address is known until every label has been
placed.
Boot in a loadable program fills the entry field rather than being
installed - vector zero is where the whole machine starts, and a program
loaded into a running system has no business saying anything about that.
A boot image is the one thing that does, so there it is installed like any
other, behind a "VEC" marker in the SPBT file.
Device is named by the port, and Device with the five reserved names are
matched without regard to case, the way mnemonics are: they are part of
the language rather than names the programmer chose. Devices have no names
of their own, so they are given one nothing can type.
TWO BUGS, both of a kind worth naming.
The first: "is this a loadable program" was written out as an OR of the two
segment bases in seven places, and the sense wanted is the opposite in most
of them. One of the seven had it backwards and put a version ONE header on a
file carrying vectors, which a loader is right to refuse. It is one flag
now, settled once and tested the same way everywhere.
The second: finding the entry to write a handler into means calling vecFind,
which reads the entry's fields out - including the handler it does not have
yet. An address resolved into VecHandler before the find was overwritten
with zero by the find itself, and the file came out with a vector pointing
at address zero: a slot that looked installed and went nowhere. The
resolved address has a variable of its own now.
Keys.asm and console.asm go on the CosmOS disk, so the whole path can be
watched rather than only tested.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
|
||
|
|
c5e4ec3455 |
M2: the native assembler builds applications
> load Asm.sbx
> run Say.asm
wrote Say.sbx: program 46, data 93, labels 7
> load Say.sbx
> run built by the machine itself
it says: built by the machine itself
The machine assembles an application and then runs what it built. Say,
greet and Files all come out byte for byte identical to the C assembler's,
and Tests/native.sh checks all three on every run alongside the boot image
M1 already covered.
WHAT IT TOOK, and it was more than #Include and #Base:
#Include The reader is a stack of readers. The current file's whole
state goes aside - buffer and all, 292 bytes - the new one
opens, and the end of it pops the old one back. A file goes in
once; including it twice does nothing, which is what lets two
libraries depend on a third. The list is forgotten between the
passes, because the second has to walk the same tree.
#Base Cursors start there, so labels hold the addresses the program
will really have. A program that says where it goes gets the
SBEX header and a .sbx name; one that says nothing gets SPBT
and .bin. A program that bases one segment and leaves the
other unbased with content in it is refused.
#Reserve Runs of zeroes, moved over in the first pass and written in
#Align the second. How many an #Align comes to depends on where the
cursor has reached, which is why both passes keep a cursor.
#Vectors Names are read and numbered, pinned where the source pins
them, so SWI osPrintString resolves. Every application needs
this - a program that calls a service names a vector declared
in a file it includes.
THE TWO PASSES ARE NOW ONE LOOP, walked twice, with Emitting the only
difference. They have to agree about the length of every token, and the way
they stop agreeing is by being two pieces of code that drifted apart -
which is the exact shape of the bug this assembler found in the C one.
Sharing the body means there is nothing to drift. What is left is checked
anyway: the second pass compares its own totals against the first's and
refuses to write the file if they differ.
THE BUG WORTH RECORDING. The tokenizer holds one character of lookahead,
and at an #Include that character belongs to the file being put aside. It
was carried across and handed back on the way out, which is wrong: a file
runs out in the middle of whatever the tokenizer happens to be doing, so
the character arrives in the middle of a word. `start:` came back as `s`
and then `tart:` - and the result assembled into a perfectly plausible
file. The fix is to undo the read instead, so the character is simply still
there when the file is opened again and the question of when to hand it
back never arises.
Three smaller ones, all old friends: three places took the CONTENTS of a
buffer where they wanted its ADDRESS; vecTakeAuto returned its answer in A,
which a RET puts back; and pass two re-declared every vector because only
the label table was being skipped on the second walk.
sameText moved down into numbers.asm from the label table - four parts want
it now, and a reader test that includes neither labels nor tokens has to
build on its own.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
|
||
|
|
affe9d09ea |
Put the assembler and some source on the CosmOS disk
'make run-cosmos' now boots a machine with Asm.sbx on the disk and two
source files to point it at, so the thing can be watched working rather
than only tested:
> load Asm.sbx
> run hello.asm
wrote hello.bin: program 17, data 14, labels 2
> run strings.asm
wrote strings.bin: program 62, data 88, labels 4
Both come out byte for byte identical to the C assembler's, and both run.
hello.asm and testPrograms/stringKeyword.asm are the two single-file
programs with no #Include, which is what the native assembler handles so
far; strings.asm is the second because it has a subroutine and a label
used before it is defined, so it exercises a forward reference that
hello.asm does not.
The assembler builds from its own directory rather than from Apps/,
because it is not one file. Its pieces are found beside it without being
told, since an include is looked for next to the file that asked for it
before anywhere else; only services.asm needs the include path.
ALSO A CORRECTION. Asm.asm's header, its usage message and the Assembler
Manual all said 'run Asm.sbx hello.asm'. That is the convention we talked
about wanting later, not the one this shell has: load and run are separate
commands, so the whole rest of the run line is the argument and that form
asks for a file called "Asm.sbx hello.asm". All three now say load first.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
|
||
|
|
dcb331c151 |
SplitBit assembles SplitBit: M1, a single file with no includes
Programs/CosmOS/Assembler/ is an assembler written in SplitBit assembly. It
runs under CosmOS, reads source off a SplitBit disk, and writes a binary back
to it with no host involved anywhere:
> run Asm.sbx hello.asm
wrote hello.bin: program 17, data 14, labels 2
THE ACCEPTANCE TEST IS THE BYTES. Tests/native.sh assembles Programs/hello.asm
both ways and compares the two files byte for byte, then runs the one the
machine built. "It ran" and "the sizes look right" both pass for a binary with
a label one byte out, which is a program that jumps into the middle of an
instruction - so the only honest test is the one SplitDisk and sbfs.asm
already work under: two implementations of one written specification, each
checking the other. The files are identical and the result prints Hello,
World! in 70 cycles.
hello.asm is the target because it is the oldest program in the repository.
The first thing this machine ever ran is now the first thing it assembles for
itself.
TWO PASSES OVER STREAMED SOURCE. The C assembler reads every token of every
file into one array; that cannot port, because cosmos.asm alone is 56,047
bytes against 64K of Data Memory. The native one streams through a 256 byte
window, twice, and keeps only the label table between the passes. Two passes
suffice because every length is known without resolving anything - an
instruction's from its shape, a value's is one, a string's is its characters
and a zero - so the first pass fixes every address and the second never needs
a fixup list. A forward reference stops being a special case and becomes the
reason there are two passes at all.
The parts, each checked before anything was built on it:
source.asm characters out of a file of any size, with a line number
token.asm tokens out of characters, one character of lookahead
classify.asm what a token is, in the C assembler's order, which IS the
language: keyword, instruction, value, string, label
labels.asm names packed in an arena, four bytes of index each
numbers.asm sixteen bit arithmetic, since sbfs.asm's cannot be reached
table.asm the instruction set, generated by the same script the
monitor's copy is, and now BOTH are checked by docs.sh
readTest.asm and tokenTest.asm check the reader and the tokenizer on their
own, recorded as cosmosSource and cosmosTokens. A wrong classification does
not produce a wrong byte somewhere obvious; it produces a right looking
program of the wrong length, so it is worth catching where it happens.
WHAT IT REFUSES: #Include, #Base, #Align, #Reserve and #Vectors are refused
by name rather than ignored. Skipping a directive would produce a file that
looked right and was the wrong length, which is the worst thing an assembler
can do.
Two traps worth recording, both already known to this project and both hit
again: CALL restores A, B and DP0-DP2, so three routines returning an answer
in A had it undone by their own return; and numStep works on DP0, so three
sites that set DP1 left a pointer that never advanced.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
|