6fe898b5fc169fc359fc1bffdc26c0f959ee31b2
24
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
3449405b18 |
Give the system another page of each memory
CosmOS had 1,161 bytes of Program Memory left before the address applications load at, and Tab completion is not going to fit in that with anything to spare. So the wall moves up one page: the system keeps below 0x4FFF and 0x2FFF, and an application is based at 0x5000 and 0x3000. A PAGE IS A CHEAP THING TO GIVE IT AND AN EXPENSIVE THING TO RUN OUT OF. An application still has 44K of Program Memory before the vector table and the largest one here uses 7.5K, so what was taken from applications is space nothing has ever asked for - while what the system gained is the difference between building the next thing and counting bytes while building it. Not doubling, which was the version that would have cost application space worth minding. One page, and the same again when it is needed. Nothing in the machine knows where the wall is, so this is 34 #Base lines, one threshold in the fault handler, and the table in the CosmOS README that Tests/docs.sh reads its limits out of. The native assembler's scratch map had to move with it, and docs.sh said so before anything ran: its data reached 0x40D6 and its buffers began at 0x4000, so they were sitting on its variables. That file already carries a paragraph about the floor coming up and the map staying where it was. It has happened twice now, and been caught by a check the first time wrote. Twenty three recordings are the same runs a page higher. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
fd9c4c75f8 |
Tell the person where it hurts
A fault stopped the machine and printed a line to standard error. On a terminal that is a diagnosis. Behind a window it is a frozen picture and no reason at all, because the message went somewhere nobody was looking - the machine looked hung and was not. It had stopped, and said so invisibly. CosmOS catches all five faults now and says what happened on the screen, with the address, in red. A FAULT ENDS THE PROGRAM, NOT THE MACHINE. That is the answer to "carry on or start again", and it is not a compromise: a bare RETI from most of these meets the instruction that failed and fails again, so carrying on was never on offer. But the machine is almost never what is broken. Everything the shell puts back when a program exits - the Stack, its vectors, the drive, the working directory, the console, the screen - is exactly what wants putting back after one dies, so the handler sets a status and joins handleExit. You are back at the prompt, and the program is recorded as having STOPPED rather than finished, because saying "finished" under a red fault message would be the shell contradicting itself. A fault below where programs load is the system's own, and there is nothing to go back to. That one says so and stops. THE SCREEN GOES BACK TO A MODE TEXT CAN BE SEEN IN, and that is the part that matters rather than the part that is prettiest. A program that faulted in bitmap mode left the console with no text rows, so it draws nothing at all: the message would be perfectly correct and completely invisible, which is the one thing it must never be. Two palette entries go back for the same reason, since a program that wrote its own colours can leave every ink the same as every paper. Only the two the message needs, so the rest of what the program chose is left alone. Both halves are checked by looking at the PICTURE, because the serial line was never where the problem was. Crash blind ruins the palette and drops into bitmap mode before it faults; without the mode the screen comes back 320 by 200 with nothing on it, and without the palette it is the right size with the message present and unreadable. Each break loses the red on its own. Crash is also a program worth having: it breaks in whichever of the five ways you name, so a fault screen can be looked at without having written a bug first. Two things found on the way: The native assembler keeps its OWN copy of the reserved vector names, so it did not know NoHandler or NoDevice and built a cosmos.bin that differed from the host assembler's. Caught by native.sh, which is exactly the drift that test exists for. And cosmosMonitor had dead input. It assembles code into 0x8000 and runs it, and that code faults - which used to kill the machine, so everything after it in the file had never run. It runs now, and the recording grew by sixty lines of monitor session that had been unreachable since the day the fault was put there. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
4e3258e3f7 |
Put the libraries on the disk, and say which include is missing
Sieve-16.asm, Life.asm and Fib-16.asm could be read on the machine and not assembled on it. All three #Include print.asm, and print.asm was not on the disk. /LIB WAS STILL A HAND-WRITTEN LIST. Programs/Libraries is what an #Include means when it is not a CosmOS source - print, the integer helpers, the maths - and nobody had thought to name them, which is the exact failure mirroring the sources was meant to end. It is mirrored now, so the next one nobody thinks of is there anyway. AND THE ASSEMBLER SAID NOTHING USEFUL. A missing include returned a failure and printed no message, so the catch-all at the top said "nothing was written", which is true and useless - finding out why meant reading the assembler rather than the message. It names the file now, and where it looked: cannot find print.asm, not beside the file that asked and not as /Lib/print.asm Both halves are worth having, because a name that is in neither place is usually one or the other spelt wrong. The same shape as More's "error 2": a program that knows exactly what happened, reporting a number or nothing at all. The disk also depends on the recipe that lays it out now. It did not, so adding the libraries left an image built without them and the next run reported the same thing missing - which sends you looking at the change you just made rather than at the stale thing in front of you. It cost twenty minutes to notice. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
c8c9f0b363 |
SRET: a handler answers the way a subroutine does
CALL saves A, B and Data Pointers 0 to 2 and nothing else, which is exactly why Q and DP3 are how a subroutine hands something back. An interrupt saves all of it, so a service with an answer had to reach into its own frame and un-save two fields by hand: MVSD.2 DPUP.2 0d02 ; the saved Q, by an offset it had to know STA.2 RETI Thirty places in CosmOS did that. Every one knew the frame's layout by heart, and all thirty would have gone quietly wrong the day the frame gained a field - the same duplicated fact this project keeps being bitten by, except duplicated into thirty places AND into the CPU. SRET is 0x76, in the seat the block split left for it. It is RETI's frame with RET's rule applied: A, B and DP0 to DP2 come back, the saved Q and DP3 are dropped, and the Interrupt Flag is restored from the frame - only that bit, so carry survives a service the way it survives a call, and there is one rule rather than two. RETI stays exactly as it was: a hardware handler has nothing to say and must leave no trace. CosmOS is 10,969 bytes against 11,122, and no handler knows a frame offset. TWO MISTAKES WORTH RECORDING, both mine, both caught by tests. The first conversion matched STA.2 with a regular expression that did not allow a trailing comment, so it ran past the end of one handler and into the next. The second understood the pattern and still got it wrong: the old frame write carried the answer from A into the saved Q slot, so simply deleting the write left Q holding whatever it happened to hold. Services that answer by calling something were fine - Q already had it - and services that set A directly silently reported success for every failure. cosmosCwd is what noticed, by saying "cannot go there" about a directory that was there. Sixteen handlers move the answer into Q now. Seven MVQA went with it. They copied Q into A so the frame write could carry it; SRET puts A back, so they moved a value nobody would ever read. |
||
|
|
cd5f548736 |
Move the opcode map: nothing in 0x0X, and room for a return variant
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. |
||
|
|
9c144469b4 |
Take the SplitLint findings that are one operation, leave the rest
Twenty four more sites, and the interesting part is which ones were left alone. A rule emerged while reading them and it held all the way through: apply where the repetition is INSIDE one operation, skip where the author's own structure says it is a new thought, and never where two equal values mean different things. Taken: - Five registers reassigned to a value they already held, where both are the same quantity: two masks in one expression in Snake, two spaces printed by the monitor, both halves of block zero in waitTest, and a RSTA in Pour that the very next instruction overwrote. - Eighteen SETDs that reload a pointer inside one operation - a store back into the variable just read, or an INCD stepping to the second byte of a two byte value. Those read correctly without the reload. - sbfsNext, which branched to the label on the line below it. Left, with reasons that are the useful part of this: - Eight registers where the same number means two different things. CosmOS and the loader set A to 1 for a blit command and then to 1 again for a bank number; Asm compares a type against 3 and then a status against 3. Removing those couples one quantity to another that is equal by accident and would part company silently. - Ten RSTAs that open the RSTA/RSTB/CCF/ADD "return zero" block. The redundancy is what makes that idiom self contained; taking it out makes the return value depend on the line above. - Eleven SETDs that begin an arm of a comparison chain. Each arm loads, compares and branches, and they get reordered - the repetition is the reason a new arm can be dropped in anywhere. - Twenty five SETDs separated from their pointer by a blank line or a comment, which is the author saying a new thought starts here. - Two CCFs before arithmetic, which this codebase writes unconditionally. - Three redundant branches in test programs whose recorded output includes addresses, where three fewer bytes moves what the test demonstrates. Nine recorded outputs moved and every one is a size in a listing or, for Life, five more generations inside the same cycle budget. Behaviour is unchanged everywhere: cosmosSnake and cosmosEdit pass byte for byte while Snake loses eight bytes and Edit twelve. CosmOS is 10,902 bytes of program against 10,937, and the native assembler 12,173 against 12,183. The CosmOS README's size for Edit moved twice in one sitting, and this morning's check caught it both times - which it could not have done before that claim was reworded to name what it was about. |
||
|
|
e1273337c4 |
Two mechanical fixes SplitLint found: MVQA, and RSTA for zero
Twenty four places moved Q into A or B by pushing it and popping it back. That is four bus cycles and two bytes to do what MVQA does in one of each, and several of them are inside loops - Life, the calculator, int8. Nineteen more loaded zero with INIA 0d0 where RSTA says the same thing in one byte. Both are equivalent at the CPU rather than by assertion: RSTA and INIA both leave Status alone, and PSHQ followed by POPA nets to A = Q with the Stack Pointer where it started. The one difference is that the pair leaves a copy of Q in memory just below the Stack Pointer and MVQA does not, which nothing here reads. Five recorded outputs moved and every one of them says the change worked: - 16x16Life fits five more generations into the same cycle budget, the first 457 lines identical, because the loop got cheaper. - Life.sbx is 1409 bytes rather than 1411, in three tests that list it. - Edit.sbx is 1995 rather than 1996. That last one broke a check I added this morning, and the hole is worth recording: the CosmOS README's claim about Edit's size did not have the word "Edit" on the same line as the number, because the subject was in the sentence before, so the check that measures quoted sizes skipped it silently. The sentence now names what it is talking about, which makes it both checkable and clearer, and the check fails on a wrong number there. Comments on either half of a replaced pair are carried onto the instruction that replaces them, so nothing anybody wrote was lost. |
||
|
|
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. |
||
|
|
6b41354f8f |
Delete a comment describing a design that was removed, and check the rest
Two comments in the native assembler survived the changes that made them false, and both are the kind that misleads rather than merely ages. Asm.asm still explained an eighteen kilobyte buffer that the whole output had to fit in "because a file is written in one call and there is nowhere to put half of one" - which stopped being true when the assembler learned to stream, and the variable it described, ImgRoom, does not exist any more. It was sitting in front of the comment that replaced it, so the paragraph a reader met first described the design that had been taken out. Replaced by what is actually there: no limit but the disk, one block at a time through a window in scratch. scratch.asm said the system keeps below 0x1000 twelve lines above the paragraph explaining that the system's half of Data Memory was doubled to 0x1FFF. A stale number next to its own correction is worse than a stale number alone, because which one a reader believes is a coin toss. docs.sh now checks both of the facts scratch.asm asserts about the machine around it: the floor it claims the system keeps below, against the CosmOS README, and the address its buffers start at, against where the assembler's own data actually ends. Neither is enforced by a line of code anywhere - the map is a comment, deliberately, because reserving the buffers would put 22K of zeroes in the file and the assembler could not load itself - so a check is the only thing that can hold them. Both fail when broken; the second reports the exact overlap. |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
7cd5e34347 |
Two ceilings a hundred bytes apart look like one ceiling
The sixteen kilobytes taken back a moment ago all went to the output image, because that was the wall: 13,245 bytes of cosmos.bin against 13,312. Lifting it moved the machine straight into the next one, a hundred and eleven bytes away - the label names, at 8,081 of 8,192 - and the index was a hundred and eighteen entries from the same place. So the room is shared out rather than given to the obvious one. Names and index both double, and the output takes what is left, which is still four and a half thousand bytes more than CosmOS needs. LabLimit and LabRoom in labels.asm have to agree with the map in scratch.asm and are now said to. Worth recording how this was found, because it is the good case. The assembler STOPPED and said "no room left for label names: Mode, at line 3598" - a limit it checks, names, and points at. Every other ceiling this project has hit went unnoticed until something downstream broke: a program loaded over the shell, a path silently cut short, a file reported as itself less 65,536. A limit that announces itself is worth the handful of instructions it costs. The output's eighteen kilobytes are temporary. They exist because the assembler holds a whole finished file in memory before writing it, and the file is produced in order, so it could be written as it is made. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
ec56439d9a |
Take back the sixteen kilobytes the scratch map left behind
The assembler's buffers start at 0x8000, and the reason written beside them is that everything above its own data is free. That was true when it was written. The system kept below 0x1000 then; its data now reaches 0x1FFF, and the assembler's own moved from 0x1000 to 0x2000 with it when both halves of the memory map were doubled. The floor came up and the map stayed where it was, leaving sixteen kilobytes between the two that nothing touched at all. Starting at 0x4000 takes that back, with nearly four kilobytes of slack still in front of it and room for the assembler's own data to double before the two would meet. All of it went to the output image, because that is the buffer that is actually full. cosmos.bin is 13,245 bytes against the 13,312 it had, which is sixty seven bytes - the next thing added to the system would have made the machine unable to build it, and it would have shown up as a test going red rather than as anything explicable at the machine. The label table has room yet, so this is not the moment to share the space out evenly. This is the third time a limit here has gone unnoticed until something hit it, and the second where the cause was a number that stopped being true rather than one that was ever wrong. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
06bdbf7728 |
D5: move in, and give the assembler somewhere to look
The demo disk is three directories instead of thirty nine names in one list with cosmos.asm sitting between fileStream.asm and sbfs.asm: /Apps what you run /Source what you name to the assembler /Lib what those include The split is by ROLE rather than by which directory the host keeps a file in. Everything in /Lib is named by an #Include somewhere and by nothing else, which is what makes it a library rather than a source. THAT LAYOUT WAS NOT POSSIBLE UNTIL NOW, and finding out why is what this rung actually cost. An include on the machine was a bare name resolved where you stood, so every source that calls a service had to sit in the same directory as services.asm - which is every source worth having. The first arrangement of this disk put the examples in a directory of their own and none of them would assemble. So the native assembler has a search path: beside you, then /Lib. The same rule the shell already uses for a program it does not recognise, applied to the thing that reads source, and the same reasoning for it being two fixed places rather than a list - a list needs somewhere to live between one boot and the next, and there is no such place yet. It also brings the native assembler nearer the host one, which has searched -I directories since before there was a machine to run this on. The reader's per-file state grew from 293 bytes to 301, because the name it keeps is a path now and every block of a file is asked for by it. Six of those would no longer fit the room set aside, so the include list moved up a page. Both numbers are written down in two places on purpose and both were changed. dir said cosmos.asm was 17,460 bytes. It is 82,996. The size came out of the block count's LOW BYTE shifted up and the tail beneath it, which is sixteen bits, so anything from 256 blocks upward came back as itself less 65,536 - a plausible number, and wrong. Files that big say their size in blocks now. Printing the true figure wants decimal printing twenty four bits wide, which is a page of console.asm to say something nobody reads more precisely than "big". The Assembler Manual's line about SBFS being flat was the last thing in the repository still claiming it, and docs.sh now looks for that phrase and three like it in all four documents. Not a section that is wrong - one clause inside a paragraph that is otherwise right, which is the shape this kind of staleness takes. The duplicate puts are gone with the wildcard that caused them, so building the disk is quiet. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
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
|