b1dde7908c7c6e432e6834f8f7c3b3f90dd79eb9
77
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
b1dde7908c |
Lunar Porter flies on a controller
A held thruster burns every tick it is held for, which is the whole reason the pad exists: the console can only say a key went down, so a thruster driven by it could be pumped and never leaned on. The burn happens on the same tick gravity does, and for the same reason - a sixteenth of a pixel is the smallest step this arithmetic takes, and applied sixty times a second it is an enormous acceleration. On the tick, thrust and gravity are two numbers whose RATIO is the whole feel of the thing. Position still moves every frame; only the acceleration is stepped, and nothing can see that. Two against gravity's one, so climbing and falling are the same speed. Three was the first try and it left the moon after about a second of holding. If there is a pad the console's arrows are ignored, because under a window the same keypress reaches both - the pad as a level, the console as a byte - and a thruster that fired twice for one press would be a mystery to anybody tuning it. q still quits, since a pad has no letter for it. With no pad the arrows still burn once a press, which is the most that can be done down a wire. And break.sh now rebuilds the disk images as well as the binaries. Half the things worth breaking here are SplitBit assembly rather than C, and those live on the fixture disks - so an edit to a .asm file changed nothing the suite could see, and the tool reported that nothing caught the break. That is the exact lie it was written to prevent, turning up in a new place. With the disks rebuilt it catches this one: the lander falls between the two captures instead of climbing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
d6c81fa32c |
Lunar Porter, rung one: it flies
A lander over a moon that wraps. Landing, crashing, fuel, cargo and bases are not here - this rung exists to answer whether it FEELS right, because everything after it is bookkeeping and none of it is worth building on a lander that is no fun to fly. The moon comes for free. The map's column origin is a ring in hardware, so 128 cells is 1024 pixels of surface with no edge and no seam to cross. Position and velocity are sixteen bit in SIXTEENTHS OF A PIXEL, and the unit is the design: gravity is a small number added to a velocity and a velocity is a number added to a position, with no multiply or divide anywhere. 1024 pixels is 16,384 sixteenths, which is 2^14 - so going all the way round is an AND with 0x3FFF rather than a comparison, and it is never wrong at the seam. The lander never moves sideways. The world scrolls under it and it sits at the middle of the screen, which is a byte a frame instead of two and is also what makes the wrap invisible: there is no moment where it jumps. One key is one burn. The console says which key went down and there is no such thing as a key coming up, so a thruster cannot be held - a press adds to the velocity once. That is a property of the machine rather than a choice this program made, and it reads as pumping the engine. Four bugs found by running it, all worth keeping written down: B CANNOT BE A LOOP COUNTER here. Every comparison is an INIB, so the count was overwritten by whichever bound was last tested and the loop reset itself for ever. Counters that outlive arithmetic live in memory. A subroutine answers in Q, and the AND after it read A. The moon came out flat because it was testing the height against 1 instead of the random number. Row minus height, not height minus row: they are equal at the surface, equal does not borrow, and the surface row has to be ground. And the shift register has A as its HIGH half. Written the other way, the view scrolled by 256 cells for every one it should have. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
5222c85100 |
More fills the screen it is on, not the screen it was written for
Twenty two lines was right when there was one screen size. It still is on the forty column screen and wastes three fifths of the eighty column one, so More asks the rows register instead - which is readable for exactly this sort of reason. Rows minus three is twenty two on a twenty five row screen, so nothing changed underneath anyone already reading files this way. It fills a bigger screen and leaves a smaller one alone. A bitmap screen has no rows and says so with a nought, which through an eight bit subtraction would be 253 lines. Anything under five falls back. The existing test stopped testing paging the moment this worked: 32 lines fits in a 47 line page, so the file never paged and the recording lost the prompt entirely. The fixture is 60 lines now - the INPUT needed moving, not just the output, which is the failure this project keeps meeting. And cosmosMoreNarrow, which runs Mode first and pages the same file on the forty column screen. Two recordings of one file at 47 lines and at 22: a More that went back to a constant would make them the same length. Both were verified with break.sh, and the first attempt was a bad break rather than a bad test - it replaced one of two reads of the rows port and the other still fetched the real value. Which is a fair argument against reading a port twice, so it is read once and kept now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
fba1b553d2 |
Depth: a ball behind the near pillars and in front of the far ones
The demo for what V5 added. Four pillars at four distances, each ONE 8 by 8 tile stretched to its own width and height, and a ball walking past all of them at a distance between two. What it shows is the thing an ordering cannot. The ball is sprite NOUGHT and every pillar is numbered after it, so table order puts the ball in front of all four - and it is still hidden behind two of them, because the depth buffer is asked per column. Caught mid-straddle in the checks: the ball is 48 wide and the pillar 32, so it shows on both sides and nowhere across the middle. Writing it found the conceptual trap in the feature, which is now written down where somebody will hit it. The pillars first carried their own distance in their entries AND wrote that same distance into their columns, so each was asked whether it was in front of itself - and 20 is not nearer than 20, so all four vanished. THE BUFFER IS WHAT HAS BEEN DRAWN AND A SPRITE'S DEPTH IS A QUESTION ASKED OF IT. Scenery writes it; it does not ask. Also found that a scheme only gives a colour to index one. The default palette sets each scheme's paper and ink and nothing between them, so art drawn in index two comes out black until a program writes a palette. And the fixture disk's root directory was full: four blocks, 32 entries, all taken, so adding an app failed the whole disk build. Loudly, which is the right way round - but it is a wall that moves for free, so it is eight blocks and 64 entries now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
cb898450b5 |
Sprites that scale, and a depth buffer to hide them behind
A target size in PIXELS rather than a multiplier, which is the whole of why this is usable here. A billboard at distance d wants to be k/d pixels tall, and that is a number a program has anyway - out of a lookup table, most likely. A multiplier would have to be a fixed point fraction arrived at by dividing, and this CPU cannot divide. Zero on an axis means the natural size, so every sprite written before scaling existed still means what it meant. The two axes are independent, and that shape - one tile wide at its own size, stretched to whatever height a distance says - is a wall column in a pseudo-3D game. Measured: a DDA step costs 85 cycles, so 80 columns of ray casting is about 85,000 cycles, or 12fps. Drawing those walls from the CPU instead would be 256,000 writes, fifteen frames of cycles for one frame of screen. The device doing the pixels is what makes such a game possible at all here, not merely faster. And a depth buffer, one byte a screen column at 0xD000, written by the program. A sprite with a depth draws only in the columns it is in front of. PER COLUMN, and that is the point: a billboard is nearer than the wall at one end of itself and further at the other, and no ordering of the table can say that. Table order settles sprites against each other; the buffer settles them against the scenery. Zero means no test at both ends, so a program that never writes it behaves as it did before it existed. The entry grew from 8 bytes to 16 - now, while two programs use the table, rather than once a game is written on it. Bytes 0 to 7 kept their meanings, so Sprite.asm needed no change. The pass is rewritten to walk where a sprite is GOING rather than where it came from, which is what makes a stretch and a squash one operation. It also made flipping fall out: turning the source coordinate round mirrors the tile order and the pixels inside each tile in one step, where drawing tile by tile had to be told to do both. All 111 checks passed unchanged at natural size, which is what says the rewrite changed nothing it should not. Clipping moved out of the inner loop and had to: a target size is sixteen bits, so a sprite asked to be 60,000 pixels tall would have been sixty thousand turns of a loop that drew eight rows. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
9eed23120f |
Four pages of tiles, in bits that were already there
A tile number is a byte and a byte reaches 256, which is not many once a font has taken 135 of them and a game wants a character, a background and a wall. Bits 4 and 5 of the attribute now say which page of 256 the number is in - bits already written on every cell and every sprite, and reserved for this since the attribute was defined. Four pages of 16K is 64K, which is the whole atlas, so THE FOURTH PAGE IS THE MEMORY THE SPRITE TABLE AND THE PALETTE ARE IN. That is not a hole in the design; it is the answer shared video memory has always given, and it is checked rather than forbidden. The atlas is 1024 tiles, and what a program spends on sprites and colours comes out of them: no sprites means page 3 is art, and sprites means 768 tiles and a reason. The page is a property of the CELL and not a mode, so one screen shows tiles from all four at once and nothing has to decide which page it is in. Both places a tile is drawn from now ask one function where the art is. They would otherwise drift: the sprite pass was written days after the map pass and neither is where the other is looked at. Nothing in CosmOS changes. The shell draws from page 0, which the screen save covers; a tile left in another page is invisible unless a map cell names that page, and the map is given back or cleared. Both breaks were tried and both failed the checks - and the second had to be tried twice, because the constant it needed lives in video.h and the harness was only editing video.c. That is the same silent no-op as yesterday's uncompiled break, in a different disguise. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
a916103a7f |
Sprites: things that move without the screen moving
Everything drawn on this machine was in a cell. Something between two cells meant rewriting both; something moving a pixel at a time meant rewriting them sixty times a second, which is affordable for one thing and not for twenty. A sprite is put at a pixel and the device draws it over whatever is behind, so moving it costs two bytes. MADE OF TILES, which is the decision the rest follows from: m by n taken in reading order from one index, so there is no second pixel format, no second kind of memory, and nothing a sprite can show that the map cannot. A 16 by 16 character is four tiles and the background can name the same four. 256 entries of 8 bytes at 0xC000 in the atlas - eight so the entry address is a shift, the same no-multiply argument as the palette's four. Position is signed and sixteen bits, because 640 by 400 does not fit in a byte and a sprite has to be able to sit half off the left rather than appearing whole at the edge. A PIXEL OF ZERO IS NOT DRAWN, or every sprite is a rectangle. Tested before the attribute is added, so a hole belongs to the art and not to the colour scheme. The same rule the other way round is what "behind" means: drawn only where the background pixel was zero, so a thing walks behind a pillar and in front of the floor in one frame. All of them draw, every frame, so they cannot flicker. Real machines dropped them per scanline because they had a fixed number of shift registers; this has a loop. The limit is the size of the table, which is a constant rather than a property of what is on screen. And the system takes them down at exit. The sprite table sits in the gap the screen save walks around - to the end of the map, then the palette - and that is right, because nothing the shell draws is a sprite: there is nothing to give back, only something to take away. Otherwise a program that put a ball up and left would leave it over the prompt, in front of everything, with nothing able to type it away. Sprite.asm deliberately leaves its own, because a program that faulted could not have cleared it. Every check here was re-broken and failed: transparency, reading order, draw order, priority, and size. Size needed breaking twice - the first attempt did not compile, and a silent build failure had left the old binary passing. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
eee95ef0ce |
Flip says the true thing, and the checks that let it lie
Two bugs, both in what the demo claimed rather than in the device. The assembler has no string escapes, so the "\n" written in a literal printed as a backslash and an n. A newline is a byte; Say.asm has always written one as 0x0A 0x00 and this now writes it out of the console port. And the line whose whole job was to still be there afterwards was wiped out on the way back, because the program called osTakeScreen - which restores the screen AS IT WAS BEFORE, so the tidy-up erased the one thing the demo was pointing at. It did not need saving: nothing it touches is the shell's. A program that damages nothing should not ask, and asking anyway costs it the screen it was standing on. Which turned out to be untrue as written, and that is the third thing. Flip drew with a tile of its own, and the system copies the font back over every tile at exit - so the filled screen went blank the moment the program left, and the check that the system put the display back could not tell a restored screen from an abandoned one. It passed with the restore deleted. So did the check that a program can show the other screen at all: a blank screen counts as one colour just as well as a filled one does. Now it fills with 0x0A, which is an asterisk in one of the reversed colour schemes: paper is the colour and ink is black, so a whole screen is drawn with NO TILE REDEFINED and it survives leaving. Both checks ask for the commonest colour in the picture rather than counting colours or naming a pixel - the font's only blank glyph is the space, whose attribute nibble is nought, so a filled screen is always a pattern and which pixel lands on paper depends on the character. Both were re-broken afterwards and both failed this time. Also cosmosFlip, a transcript test, which is what would have caught the printed backslash in the first place. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
023362b05a |
A second screen, and one port to say which is shown
A screen drawn where it can be seen is seen half drawn. A program that moves forty things and rewrites the map underneath them is wrong for as long as it takes to put them all right, and at a megahertz that is long enough to look at. So the device brings a second screen bank, on port 0x3B, and port 0x3C says which of the two is displayed. Everything a program draws into the other one is invisible until one byte shows the whole of it at once. ONE REGISTER IS ENOUGH, where the hardware this imitates needed two. The other said which screen the CPU's window pointed at; there is no window here, because a program reaches a bank through the memory controller by its number. Writing to the screen that is not shown is a matter of naming its bank, and the device never has to be told. And a flip cannot tear: a frame is drawn from one bank in one go, so a flip either happened before that frame or happens before the next. There is nothing to race, where the real machines had to catch the few lines between frames to swap in. The console draws into whichever screen is displayed rather than one of its own, so a fault message lands where somebody can read it even if a game had flipped. And CosmOS puts the displayed screen back at exit, the way it already puts back the cursor and the ink: a program that faulted while flipped could not have, and a shell that only came out right for programs which remembered would come out wrong the day one crashed. Flip.asm is the worked example. It deliberately does NOT restore the display itself - that is the point of the paragraph above, and it is what makes the system's guarantee the thing under test rather than the program's good manners. Written the other way round first, where it passed with the guarantee deleted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
2abc8281df |
Loops, and the scripting language is a language
while and for. Both only mean anything in a script, because a loop goes back to the line that opened it and a prompt has no line to go back to - and both say so rather than doing something surprising. THE SCRIPT READER KEEPS THE POSITION OF EVERY LINE before reading it, which is what makes any of this possible: by the time a line has been read the reader is past it, and a line is not a fixed size to subtract. Three words per line, and the block is read again on the way back so the pointer into it means what it meant - the same thing nesting one script inside another already did, for a different reason. THE TWO LOOPS END DIFFERENTLY, and that is the design rather than an accident. A while is taken away at its end and its own line asks the question again, so nothing has to be remembered. A for is not: how many words it has used is kept in the block, and its line reads itself again and counts one more off the front. That is a byte in a block instead of a copy of the word list in every one of them. Blocks grew from a byte to a record of sixteen - state, kind, words used, and where the line that opened it was - and sixteen because A and B are a shift register, so four rotations turn a block number into its offset. The history and the variables are addressed the same way for the same reason. Nested loops, an if inside a loop, a loop inside a branch nobody takes, and a for with no words: the last two run no times rather than once, which is the case worth having a test for. Three things found by running it: textSame asks whether two WHOLE strings are the same, so "in red green blue" is not "in". The word has to be split off before it is compared. A for typed at a prompt complained about while, because both arrive at the same place. One message that names neither is better than one that names the wrong one. And docs.sh caught a naming convention nobody had written down: it recognises a packed name by its label ending in "Name", so ForName2 was silently not counted. It failed the right way round - saying the run was shorter than the count claims rather than passing - but the convention now lives where the names are and not only in the checker. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
16f8232a35 |
Lines that are only run sometimes
if, else, end, and same. IF TAKES A COMMAND, which is one rule rather than two and is why comparing values needs no syntax of its own: "same" is an ordinary command that fails when its two words differ, so "if same $a $b" falls out of the rule instead of being an exception to it. Anything else that can fail is a question too - "if load Snake.sbx" is a perfectly good one. The shell already had the other half. LineFailed exists because a script stops at the first line that did not work, so every command was already saying whether it had, for a different reason entirely. A BLOCK HAS TWO KINDS OF NOT-RUNNING. One where an else would turn it on, and one where it would not - which is what an if pushes when something above it is already being skipped. That is what makes nesting need no looking down the stack: the top of it says everything. A branch nobody is taking is not even looked at. The skipping happens BEFORE the names are filled in, so a variable mentioned in a branch that is not running is not an error - a line nobody runs must not be able to fail. AND LINES MAY BE INDENTED, which they could not be before there was anything to indent inside. Nobody writes an if inside an if without indenting what is in them, and a leading space used to make the first word empty and match nothing. Found by writing the test script the way anybody would write one. CALL commandFailed became BRI commandFailed in nine places. It never returns - it marks the line and branches to the prompt - so calling it was a lie that cost a Stack frame each time, and fourteen other sites already branched. THE LINT RULE FOUND THIS, three days after I wrote the rule and on my own code: two false positives that were really the linter being right about a CALL that is not one. It does not fix the leak on its own, since a failure inside any called routine still abandons that frame, but it removes the cause of the commonest case and makes the code true. The mechanical edit then left a BRI prompt stranded behind one of them, and the linter caught that too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
a8707f29f0 |
Names for things
"set apps /Apps", and then "$apps" anywhere on a later line stands for it. A name stops where a name stops - letters and digits - so it composes into a path without anything having to be quoted, which is the whole reason a script would want one. THE SUBSTITUTION HAPPENS ON EVERY LINE THE SHELL IS ABOUT TO RUN, typed or read out of a file, so the two behave the same and no command below has to know that variables exist. Same shape as the line editing: one place the whole system already flows through, rather than a decision made twenty times. A NAME NOTHING WAS SET TO DOES NOT RUN THE LINE. Every other shell expands it to nothing, and that is the wrong answer here: a mistyped name would quietly become an empty path, which is the class of silent wrong answer the rest of this system spends its effort refusing. It says so and the line counts as failed, which stops a script - and the test proves that by running one, where the line after it must not appear. Somebody who wants an empty value writes "set name" and gets one, so the escape hatch exists and has to be asked for. A NAME TOO LONG IS AN ERROR RATHER THAN A SHORTER NAME. Cutting it off at fifteen characters was the first version, and it is the same fault wearing a different coat: two names differing only after the fifteenth would be one variable, and the complaint about a missing one printed a word nobody typed. Eight slots of sixty four bytes - sixteen of name, forty eight of value - and sixty four rather than eighty because A and B are a sixteen bit shift register, so two rotations turn a slot number into its offset. The same trick the history uses, and the reason neither needs a multiply this machine has not got. TWO THINGS I GOT WRONG AND ONE I FOUND: doSetVar ended in RET. It is BRANCHED to from the dispatch, not called, so that RET went wherever the Stack happened to point - the same fault that formatted a disk last week, in a command written three days after the rule was named. The new lint rule does not catch this shape: it fires on falling INTO a subroutine, not on a branch target that ends like one. And a test of the expansion's answer, which is dead code: commandFailed does not return. It marks the line and branches to the prompt, the way every failure in this shell is reported, so the only way out of the expansion is the one where it worked. Which turned up a real leak, measured and not yet fixed: every failure that goes through commandFailed abandons the frames between the prompt and the call. SP goes from FFFD to FE6D over twenty of them, twenty bytes each. Its own commit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
3c76934a9a |
Tab reaches the disk
Paths and programs, which is the half that makes it worth having. The first word of a line is a command or a PROGRAM, offered under the name somebody would type - the extension taken off - and anything after it is a file, offered as it really is. A separator anywhere in the word says which directory to look in. A directory answers with a separator on the end instead of a space, which says what it is and lets the next part be typed straight away. The answer ending in one is also what stops a space being added, so that is one test rather than a flag. PROGRAMS ARE LOOKED FOR WHERE THE SHELL WOULD LOOK to run one: where you are, /Apps on the disk you are on, and /Apps on drive 0. Offering something the shell would not find would be finishing a word into a thing that then does not work. Drive 0's is skipped when that is already the drive, or every program in it would be offered twice and nothing would ever be the only match. Walking somebody else's directory means standing in it, which is the only way to walk one here, so where the person was and which drive they were on are put down first and restored whatever happens. Three bugs, all found by running it: THE DIRECTORY TEST WAS INVERTED. dir asks the same question the same way round four hundred lines further up, which is what made it obvious once looked at. THE /Apps WALK OVERWROTE THE TYPED PATH. The whole search runs a second time to list the matches, and by then TabDir said "/Apps" - so a word that had named nowhere went looking in the wrong place and listed nothing at all. Two ways into the walk now, and the typed path is never written over. AND LISTING ONLY KNEW ABOUT COMMANDS, because it was a second copy of the walk. It is the same walk with a flag now: finding the answer and showing the matches are the same question asked twice. Also cosmosMonitor, which had been RE-BLESSED INTO MEANINGLESSNESS by the wall move. It disassembles a loaded program, at an address the input names - and that address moved a page while the recording was simply re-recorded to whatever came out, which was a page of zeroes. It is pointed at 5000 again. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
bb065fe221 |
Tab finishes a word somebody started
The first word of a line, against the shell's fifteen commands. One match goes in with a space after it, because a word that can only be one thing is finished. Several are folded into their longest common prefix and that goes in, which is the most that can be said without guessing which was meant - and if that adds nothing, the matches are listed and the line put back underneath. THE LINE COMING BACK IS THE HALF I EXPECTED TO BE HARD and it was already solved. The prompt has been reprinted somewhere else entirely, so the editor's idea of where the line begins is wrong - but editAnchor works that out backwards from where printing ended, precisely so it survives the screen moving. Listing is a redraw it already knew how to do. editInsert became editPut, a routine, because completing a word puts in several characters and every one of them is that. Which cost a bug immediately: the old inline code left the insertion point in A, and a RET puts A back to what the caller had. Two more bugs worth naming, both mine and both the same shape - a pointer that had moved: THE CANDIDATE'S START HAS TO BE KEPT. The comparison walks DP3 through the name as it matches, so by the time a match is declared, DP3 points at the part AFTER what was typed - and that is what got copied. "he" completed to "he" because the answer taken was "lp". AND THE INSERTION STOPS AT OR PAST, not exactly equal. With the wrong answer the two counters passed each other and the loop ran off the end of the buffer, filling the line with whatever was next in memory. They cannot pass each other now, and the branch stays, because the cheaper failure is worth nothing. MY OWN TEST HAD A HOLE and breaking the code found it. The later-word case pressed Tab after a space, where there is nothing to finish anyway, so it passed whether or not the shell checked which word it was on. It types "echo he" now, which would become "echo help" if it did not. The assembler's label table went past 1024 and is doubled. A ceiling reached once will be reached again, and it is pointers into source already in memory. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
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 |
||
|
|
f97d15de08 |
The font comes from a chip, not from RAM that remembers
videoReset zeroed video memory and then wrote the font and the sixteen colour schemes into it, and the comment above that said out loud what was wrong with it: "everything here is ordinary video memory". RAM does not wake up with anything in it. That was the last piece of magic in this device, and it looked harmless until something wanted the font BACK - a program that redefines a glyph had destroyed the only copy there was. So the device has a character generator, the way the machines this one is pretending to be really did, and the copy into RAM is a thing it DOES rather than a state it mysteriously starts in. Command port 0x39: bit 0 for the font, bit 1 for the schemes. THE RAM IS STILL RAM. A program may overwrite every glyph and every colour and should be able to, which is what makes this a tile engine rather than a text display. What changed is that it is no longer a one way door. NEITHER COMMAND CLEARS WHAT IT DOES NOT OWN. The font used to clear the whole of tile memory before writing itself, which was harmless while it happened only at reset and is wrong the moment a program can ask: a program that defined a tile of its own and then wanted its text back would have paid for it with the tile. The reason it is a chip rather than a file on the disk, which was the other candidate: the boot chain prints before CosmOS exists. Stage one prints "?" when there is nothing to boot, and if the font came off the disk then the message about the disk having failed would be the one thing that could not be drawn. A system that wants its own font still loads one over the top - the ROM is the floor, not the policy. Two things that had been worked around now simply work. The shell asks for both whenever a program exits, so a program that redefined a letter no longer leaves it unable to spell, and Grid no longer needs to have saved the screen to avoid handing back green text on blue. And the fault screen asks for the glyphs first, because a message spelled in somebody's tile graphics is no message at all. Five video checks. Two runs each for the font and the schemes, since the map holds a tile NUMBER and the glyph is looked up when the frame is drawn - so restoring changes every cell using it, including ones drawn before, and what the two runs differ by is the command. The third guards the decision not to clear: tile 200 has to survive the font coming back. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
925388c2f2 |
Keep the personal disk three starts back
Copied before every start of the machine, and kept several deep rather than copied over one file. ONE BACKUP TAKEN AT EVERY START IS WORSE THAN NONE. The way a disk is lost is that something goes wrong - and the very next thing anybody does is start the machine again to see how bad it is, which is exactly when a single backup gets overwritten by the wreckage. Three deep means the damage has to happen and then be started past three times before the copy that would have helped is gone. AND IT IS NOT GUARDED BY A CHECK THAT THE DISK STILL LOOKS RIGHT, because no such check can be written. The disk that went missing this week was a perfectly valid and perfectly empty filesystem: the format had succeeded, and there is nothing to look at that says a disk has lost anything. That is the whole reason to keep the old ones rather than to judge the new one. The backups sit outside clean's reach like the disk itself. A backup a rebuild deletes is not one. The message the disk prints when it is first made said "nothing in this makefile will touch it again", which stopped being true the moment this was added. It now says written to, and says where the copies go. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
66be42d7bb |
A word the monitor does not know goes to the disk, not into the weeds
There was nothing at the end of the monitor's command list. An unrecognised word
fell off it and straight into sayPrompt - which is a ROUTINE, so its RET had
nothing of its own to return to and went wherever the Stack happened to be
pointing.
The user found it by typing a program's name at the monitor prompt, which is an
entirely reasonable thing to do: the monitor is a mode of the shell, so
everything the shell does is meant to work in it. What they got was a fault, and
before that a second prompt printed on top of the first - which is sayPrompt
doing exactly what it is for on its way past, and the tell that it had been
entered rather than called.
WHERE THAT RET WENT DECIDED HOW BAD IT WAS. Usually 0x0003, in the middle of
newLine, and the machine stopped on a byte that is not an instruction. Once it
was inside sbfsFormat, and the machine formatted the disk it had booted from -
the user's would not start again, and neither would mine, which is how I came to
have a reproduction before I had a diagnosis.
Pre-existing, and not recent: it is there at
|
||
|
|
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 |
||
|
|
4d976fc22a |
A program reading a line gets the editing too
osReadLine goes through the shell's editor now, so anything that asks the system for a line gets arrows, Home, End and Delete. The editor is a program, and a word typed with two letters the wrong way round can be put right without starting the line again. IT DOES NOT GET THE HISTORY, and that is the interesting half. Edit would otherwise fill the history with the text of somebody's document, and pressing Up in the middle of writing one would put "dir" into it. The history belongs to the thing whose lines are commands. Two entry points rather than a flag the caller sets first, so a caller cannot forget which it wanted. And the console is put back the way it was FOUND rather than the way the shell likes it. A program that had asked for key mode and then read a line through the system used to be handed back a console in line mode having asked for nothing of the sort. The status port reports all three things the control port can ask for, in the same order two bits along, so one shift turns what the console IS into what to write to make it that again. Which uncovered a real fault in the console. READING THE STATUS PORT WAS EATING A KEY: in line mode the poll consumed an arrow key and dropped it, so a program that looked and then asked for key mode - exactly what reading a line now does - found the first key it was reaching for already gone. A look must not consume what it cannot report, because the mode can change. It is held now and delivered as soon as something will take it. A blocking read still discards it, and must: that read IS the delivery, and a byte held there would be met again forever. Four recordings gained a program's echo, and cosmosEdit's went from "> : : : : > : : > 1: alpha" to a session you can read. VERIFIED THE SAME WAY AS BEFORE: with only the program side of the echo silenced, all 192 tests pass against the recordings as they were before this commit, so the echo is the whole of what changed. cosmosEditService is the new test and it checks both halves at once. Inside Edit, Left/Delete/Left puts "alpah" right. Up and Down do nothing there - were a program's line walking the shell's history, the next line would come out as the echo command from the top of the file instead of the word. And one press of Up back at the prompt finds the command typed before Edit was started, which is the proof that nothing the editor read went into the history at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
71f6e215f9 |
The shell remembers what was typed before it
Up walks back through the last eight lines and Down forward again. It exists only because the keys reach the system now: until A1 and A2 there was nothing to press Up at, and the line was assembled somewhere the shell could not see. A RING RATHER THAN A LIST. A ninth line pushes the oldest out by moving where the ring starts, not by moving any of the lines - so keeping a line costs a copy of that line and nothing else, however full the history is. Eight is a power of two, so which slot an entry lives in is an AND. The ISA had the awkward part already: A and B are a sixteen bit shift register, so one SHR with B empty turns a slot number into the offset of a 128 byte slot, high byte and low, ready for DPUW. A NINTH SLOT HOLDS WHAT WAS BEING TYPED when Up left it, and Down brings it back. Losing a half written line to a keypress is the sort of small rudeness that makes a thing unpleasant to use, and it costs one slot to avoid. An empty line is not kept, and neither is one the same as the line already at the top. The test proves the second by looking one further back: if a repeated command were kept twice, the line behind the newest would be the same line again. The redraw had to learn to rub out. One space was enough while the only thing that shortened a line was taking one character out of it; a recalled line replaces the whole of it, and a short line over a long one left the tail of the long one on screen looking like part of what you were typing. It now covers exactly what was lost - which turned out to be one space fewer than before in the cases that GREW, so two lines of cosmosEditKeys lost a trailing space that was never doing anything. Costs 1157 bytes of Data Memory, taking CosmOS to 6220 of the 8192 it has before a loaded program's data begins. Worth writing down: that is the budget, and this is the largest single thing in it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
81e544eb3d |
Load a program that has no data
A five instruction program that writes one port and exits has no Data Segment at all, and the loader stopped the machine dead on it. It asked the memory controller to move a segment of no bytes, and a length of zero asks for the whole 64K - which is the machine's rule, and a reasonable one, since two bytes cannot say 65536 and a transfer of nothing is not usually what anybody meant. It is exactly what was meant here. 64K did not fit, the controller refused, and the load stopped half done. ON A TERMINAL THAT PRINTS A FAULT WITH AN ADDRESS. Behind a window it is a frozen picture and no reason at all, which is how it was found and is a separate problem from this one. The header says how long each segment is, so the loader knows before it asks. Both bytes are already in hand, so the test costs one OR. Nothing is lost by skipping the transfer: a blit leaves the controller's addresses past whatever it touched, and a blit of nothing would have left them where they already are, which is where the vectors are read from next. Guarded for the code segment too. A program with no code is equally assemblable and would have stopped in exactly the same place. Mode.sbx is the fix's test and a program worth having on its own: forty columns or eighty, whichever the screen is not in, which is what a person wanting Snake drawn twice the size actually needs. Ten instructions and no data, deliberately - it prints its two digits a register at a time rather than from a string, so it stays the smallest shape a loadable program can take. Nothing else on that disk had ever been that shape, which is why nothing had ever tried it. Reported by the user, who wrote the program. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
736037462e |
The shell edits the line it is given
Three different things used to do this job, and which one you got depended on where the machine was running. On a terminal the host held the line and did the echoing and the backspacing; behind a window the console's own gatherer did it; from a file nothing did it at all. One job, three implementations, none of them in the system - which is why there was no way to move about in a line and nowhere for a history to live. So editLine does it. Key mode while a line is being read and line mode straight after, so nothing else in the system and no program calling osReadLine notices anything changed. Left and Right, Home and End, Backspace for the character before the cursor and Delete for the one under it, and anything typed goes in where the cursor is with the rest of the line moving along. Ctrl-D means the end of input again, on an empty line, because that was a thing the terminal did while it was holding the line and it is not holding it now. Same trade as the echoing. MOST KEYSTROKES DRAW NOTHING BUT THEMSELVES. A character typed at the end of a line needs no cursor moved: printing it is the whole change, and a backspace there is three ordinary bytes. That matters beyond speed - moving the cursor by hand is what a terminal is TOLD about, in an escape sequence, so redrawing on every keypress would fill every recorded transcript in this suite with them. The line is only reprinted when something happened in the middle of it. Where the line STARTS is worked out backwards from where printing ended, rather than trusted from what was remembered. That is what makes it survive the screen scrolling: a line printed on the bottom row moves everything up by one, and a remembered row would be one too low from then on. The command line holds 127 characters, up from 63. The limit started to be felt the moment a line could be moved about in. 58 recordings changed, and every one of them by the echo. THE PROOF IS NOT A HEURISTIC: a CosmOS built with the echo silenced reproduces 187 of the 188 recordings byte for byte. The one exception is cosmosTyped, the backspace test, where the rub-out marks now come from the shell instead of from the console's gatherer - same marks, different author. cosmosEditKeys is the new test, and every line in it is typed wrong and then corrected with a different key. Its last line is eighty six characters at a prompt in column two on an eighty column screen, so the line runs onto the row below and the shell has to find the start of something it can no longer see; breaking either half of that arithmetic fails it. Also: agree.sh looked for "> the same", anchored to a prompt that no longer precedes what a command prints. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
2a29cebc6b |
Make the screen come back on the machine people actually run
Found by running it: Grid exits and the shell prints its prompt into the grid, with the view up to seven pixels out of alignment. Three faults, and the first is the one that made the other two visible. MAKE RUN-VOYAGER HAD NO SCRATCH DRIVE. It gives drive 1 to Disks/personal.img, which is a file and not volatile, so there was nowhere to keep a screen - osTakeScreen answered no and the whole feature silently did not happen. It was tested with --ram-disk and shipped without one, which is as good a description of testing the wrong machine as I can write. There is now a RAM disk in drive 2, after the personal disk so that drive 1 stays the one that is yours. A PROGRAM TOLD NO MUST COPE. A refusal is not a fault, it means doing what the program did before there was anywhere to save a screen. Grid deleted its own tidying up when it started asking, so being refused left the grid on screen with a prompt written into it. It clears up again when refused, and only then. AND THE SYSTEM ALWAYS LEAVES THE SCREEN USABLE. The fine scroll registers go back to zero at every program exit, whether or not the picture could be saved: the console draws in whole cells, so a view three pixels into one puts every character three pixels out for ever. That is not part of saving a screen and should never have depended on it. Both paths are checked now. With a scratch drive the screen comes back cell for cell; without one, no grid is left behind. Breaking either fails one of them and not the other. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
ab72443b99 |
Give the screen back: osTakeScreen, and the RAM disk earns its keep
A program that takes the whole screen leaves the shell a blank one, and
whatever was on it is gone. There was nowhere to put 48K of video memory on
a machine with 64K of Data Memory that CosmOS already lives in.
A DRIVE MADE OF MEMORY IS SOMEWHERE. The screen goes to a file on the
scratch drive - the first volatile drive found at boot - like any other
file, and comes back from handleExit alongside the vectors and console mode
already put back there. The filesystem does the allocating, so this
invented nothing: it is 196 pages of tiles, map and palette, with a block
on the front holding the cursor, the four scroll registers and the mode.
NOT AUTOMATIC, and that is the whole design. Saving on every program start
would be cheap enough; restoring on every exit would be wrong, because dir
and Files and Say print and stop and their output is the reason you ran
them. A program says it took the screen, and one that says nothing behaves
exactly as every program did before this existed.
It deleted thirty lines of Grid, and they were all wrong anyway: four
scroll registers put back by hand, the map filled with spaces, the cursor
sent home, palette bank 0 written out - and the other fifteen banks kept
Grid's colours, because there was nowhere to have kept the real ones. Grid
is 64 bytes smaller and gives back what was actually there.
The check compares the screen before against the screen after, CELL BY
CELL, and allows only the rows around the cursor to differ - found from
where the text ends rather than guessed at, because the first version
assumed the cursor was near the bottom of the screen and let three real
differences through.
Two things cost time and neither was the feature:
- An edit adding "SWI osTakeScreen" to Grid was in the same script as a
failing s.index, so the file was never written - and the COMMENT
describing the call did land, from a later edit. Grid documented a call
it did not make, and read as though it should have worked.
- docs.sh caught osTakeScreen having no row in the services table, which
is the check the service layer added for exactly this and the second
time it has earned itself.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
|
||
|
|
04f1ffabd4 |
A disk made of memory, brought up by whoever owns it
THE MACHINE SUPPLIES BLOCKS AND SAYS WHAT A DRIVE IS. It says nothing about filesystems, which is what leaves room for a system that would rather have its own - and is why the volatile bit is a fact about the hardware rather than a promise about SBFS. 0x26 what the selected drive is: bit 0, contents do not survive 0x27, 0x28 how many blocks it has --ram-disk N a drive of N blocks with memory behind it A drive of memory selects, reads, writes and has a size like any other, and a program cannot tell the difference except by how fast it was. The one thing it cannot work out for itself is that the contents are volatile, because an empty disk and a volatile disk look identical from outside. THAT BIT IS THE DIFFERENCE BETWEEN A DRIVE A SYSTEM MAY FORMAT ON SIGHT AND ONE IT MUST NOT. CosmOS formats a volatile drive it cannot read, because there was never anything on it to lose, and leaves every other unreadable drive alone - an unformatted floppy is not an invitation, it is a blank floppy. Removing that check formats somebody's blank disk, which is checked rather than asserted: cosmosBlankDisk boots with one and requires it to be refused. So CosmOS grew a format. The size comes from the drive rather than from a superblock, since a superblock states a size too and that is no use on a disk which has not got one yet. Sixteen directory blocks, 128 names, chosen rather than worked out: a scratch disk runs out of names long before room, and this machine cannot divide. The RAM disk is no faster on this emulator by default, and that is honest rather than disappointing: the emulated disk has no seek time unless asked for one. With --disk-cycles 10000 the same copy is 7.94M cycles against 8.70M, the difference being every write. run.sh takes "ram:2048" where an image name goes, which needs no removing between runs because there is nothing to remove. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
3e48e9690d |
Put a disk of your own in drive 1
make run-voyager and make run-cosmos now attach Disks/personal.img as the second drive. It is made the first time it is wanted and then left alone: a rule with no prerequisites, so make never looks at it again. NOT UNDER build/, and that is the whole point. Everything else in this repository is made from source and can be thrown away without losing anything, so 'clean' empties build/ without a thought - and a disk of your own that a clean deletes is not a disk of your own. It is the one place here where something MADE ON THE MACHINE can live, which starts to matter the moment there are tools on it that make things. Disks/ is in .gitignore for the same reason: what you make on the machine is yours and not the repository's. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
e4f4bae762 |
Work across two disks: copy between them, and run a program from one on
files from the other Two things anybody expects of a second disk, and each needed something different. COPYING NEEDED TWO THINGS TO REMEMBER A DRIVE. The write stream is the only thing here that lives across service calls, so it is the only thing whose drive can change underneath it: every osFileBlock names its source path again and goes back to the source drive, and then osFileWrite has to come home. It records the drive it was opened on and returns there. And the file lookup CACHE. It keeps the last path resolved so a reader walking a file does not re-walk the directory for every block - and skipping the walk skipped the drive the path named, so block one of a cross-drive copy read the source's block numbers off the DESTINATION disk. It only showed on files of more than one block, because a file of one is never looked up twice. One block worked and two did not, which is a suspicious enough shape to have suspected sooner. RUNNING A PROGRAM FROM ELSEWHERE NEEDED A THIRD PLACE TO LOOK, and two restorations. The shell tried where you are and /Apps on the disk you are on. It now tries /Apps on drive 0 as well, which is what makes the system's programs work from a disk of your own - one with your files on it and no system, which is most of the point of having a second disk. The drive goes back after the load, because by then the program is in memory and the blocks it came from mean nothing; and again when it exits, because a program that copies between disks moves the drive as its own paths need to and being left wherever it finished is not what was asked for. Copy 1:/a 0:/b now leaves you exactly where you were. The fixture disk grew an /Apps, because it kept its programs at the root and so could not exercise the third place at all. Two hours of the debugging above were spent on a stale disk image. The machine boots the system that is ON the image, so a rebuilt cosmos.bin means nothing until the image is rebuilt too - and the trace said my new code never ran, which was true. Third time this project has been misled by one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
4cc6393f5b |
Name a drive in a path
"1:/notes", or "1:" on its own for wherever that drive already was. Done in sbfsWalk, which is where every path in the system arrives - eight callers between the shell, the config reader and the filesystem - so it works for anything that takes a path rather than for whichever commands somebody remembered to change. NAMING A DRIVE GOES THERE AND STAYS THERE. Switching for the length of one command and switching back reads better and cannot work: a path resolves to a start block and a length, and those mean nothing without the drive they were read from. A load that resolved on drive 1 and then read its blocks on drive 0 would read the right blocks of the wrong disk. A name beginning with a digit is still a name. The colon is the whole of what tells them apart, and /2things is on the fixture disk to keep it that way. Two bugs, and the second is the interesting one. SUB sets carry on a BORROW, so a character below '0' leaves it set - and the test for "not a digit" branched on clear. Every prefix was ignored. Then the leading-separator test reads the first character through DP0, which sbfsPathDrive could not move because RET puts DP0 back the way it found it. It advanced SbfsPathAt and DP0 still pointed at the digit, so every prefixed path was judged relative and walked from the named drive's working directory. IT ONLY SHOWED WHEN THAT DRIVE WAS STANDING SOMEWHERE OTHER THAN ITS ROOT, because a relative walk from the root is an absolute one - so "cd 1:/2things" worked from a fresh boot and failed after "cd 1:/notes". The test does it in that order for that reason. Copying between two disks is still not one command: each path resolves on its own drive and the drive stays where the last path left it. That wants Copy to change drives between blocks. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
5644c24113 |
CosmOS knows about all four drives
A mounted disk is EIGHT BYTES - where its directory starts, how many
blocks it is, how big the disk is, and where you are on it. They now sit
together in the data segment, and changing drives is one copy out and one
copy in. The other three thousand lines of filesystem go on reading the
same four names they always have and never learn there is more than one
disk, which is the whole reason this was affordable.
The version is not in the record. It is checked at mount and thrown away,
because a version one disk's zero parent already reads as "in the root".
Every drive is mounted at boot: the controller says how many are plugged
in and each is tried in turn. One with nothing in it, or a disk this
cannot read, is left unmounted rather than stopping the others, so a
machine with a good disk in drive 0 and a blank in drive 1 starts.
'drive' says which one, 'drive 1' goes to another, and the working
directory goes with it - where you are on a disk is part of which disk you
are on. A drive the machine has not got is refused, and refused
differently from one that is there with nothing readable in it.
Three things the assembly caught me on, all the same misunderstanding of
what survives a call:
- OR reads A and B, and the bit came back from sbfsDriveBit in Q, which
RET does not disturb - but RET does put A back. The mounted mask never
got set and drive 0 was reported unmountable.
- MVQA then RSTA throws away the copy it just made, so doubling a bit
doubled nothing. SHL does it in one instruction, because A and B are
one register to it.
- There is no move from A to B. INB reads a port straight into B, which
is what the drive count comparison wanted.
run.sh takes more than one image now, separated by a plus, since the
machine has four drives and a test that could only name one could not
check any of this.
The buffer note is forgotten on a drive change and that is DELIBERATELY
kept although nothing can currently reach it: only the file read-ahead
consults it, a directory scan does not, and finding a file requires a
scan which overwrites the note on the way past. Two disks were built with
the same file at the same block to try to catch it and the answer was
right either way. Three instructions to hold an invariant rather than a
story about a bug - and the comment says so instead of claiming a fix.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
|
||
|
|
3b650cabcd |
Grid took the disk's bank number, and gave the screen back untidy
Found by playing with it: after running Grid, the shell could not start anything by name and dir said the disk was empty. Several commands after the program that did it had exited, and nothing had said a word. BANK NUMBERS ARE ONE NAMESPACE FOR THE WHOLE MACHINE. Grid registered video memory as bank 3, which is the number CosmOS gives the disk's buffer when it mounts - and that does not fail, it succeeds. Every read the filesystem made afterwards came out of video memory. Grid uses 4 now, and the CosmOS README has a table of who owns what, because the one place this was written down was a line in a service description about sbfsMount. Nothing hands bank numbers out and nothing refuses one that is taken. If programs start wanting banks routinely, a service that allocates them is what should exist rather than a longer table - noted there rather than built, since one program wanting one bank is not yet a system. Also puts the cursor home on the way out. The map was emptied and the console was not told, so the shell carried on writing from wherever the cursor had been standing when Grid started - twelve rows down a screen with nothing on it. Clearing is what homes a cursor and it costs one write. The regression test runs a program by name, then Grid, then the same program again; the second one is the check. Putting Grid back on bank 3 fails it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
1aa45fcfc4 |
Grid: fill the map, not the window
Scrolling sideways walked off the end of what the program had filled, so the grid went blank for six seconds and then came round again. A map row holds 128 cells - 256 bytes at two a cell, whatever mode the screen is in - and an eighty column screen shows eighty of them, so 48 were empty. This is the third thing this loop has counted and the first right one. It said forty, which filled half the screen. Then it asked the screen how wide it was, which fixed what could be seen and was still wrong. ASKING THE SCREEN IS RIGHT FOR FILLING A SCREEN AND WRONG FOR FILLING A MAP: a program writing one screenful wants the window, and a program that scrolls wants everything the window can be moved over. There is no register for that because it is a property of video memory rather than of the display. The check that should have caught it did not, and that is the more useful half. periodic.py looked at 32 pixels - four cells at the left edge - so it could not see a gap that was on the right, and at the cycle count it samples the origin had moved to column 22 and the gap was off in the middle distance. It reads the whole scanline now and says which column the picture stops repeating at, which is how the two failures above were told apart. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
bcd42e75ca |
Scroll the screen sideways, and by less than a cell
The screen could move one way, a cell at a time. Three registers were missing and this adds them: a column origin so the map can be wider than the screen as well as taller, and a pixel remainder for each axis so the step can be one pixel rather than eight. 0x36 Scroll column, in cells, wrapping at 128 0x37 Fine X, 0 to 7 pixels 0x38 Fine Y, 0 to 7 pixels FINE DOES NOT CARRY INTO COARSE. Writing 8 to a fine register writes 0, because only its low three bits mean anything. The alternative was for a write of 8 to step the coarse register, and it was rejected for one reason: a program that scrolls has to know where it has got to, and if the hardware carries then the only way to find out is to read the register back. Keeping them apart means the program already knows, because it did the arithmetic itself. It is also what the machines this one is pretending to be did. The renderer now draws one more row and one more column than fit and clips them, because with a fine offset the screen no longer begins on a cell boundary and the cells at two edges are partly off it. videoPutCell follows the column origin as it has always followed the row - a caller means a cell of the SCREEN, and the screen is a window onto the map. The fine offsets are deliberately not applied there: they move the finished picture by less than a cell, and there is no such thing as less than a cell to write into. So a program may scroll to any pixel without the console's idea of where row three, column five is moving underneath it. Grid now scrolls diagonally, a pixel a frame, in four port writes and two carries. It moved eight pixels every fourth frame before, which reads as the picture jumping rather than travelling. Seven checks, each one the same program with one register changed, so what is compared is where the picture stopped. Breaking fine X, fine Y, the column origin, the three-bit mask, or the console's use of the origin each fails exactly one of them. Grid's own two checks had to be rewritten, and the reason is worth keeping: they asked whether pixel 4 was a grid line, which was really a check that the scroll happened to be at a cell boundary. A picture that moves a pixel a frame can only be asked things that are true at every offset - that it repeats every eight pixels, and that one band of eight rows holds different colours from the next. Also repairs docs.sh, which found the minimal CosmOS application by taking the first asm block in the README. Documenting a program with an example above it made that a different block, and the check complained that the minimal application had no #Base about something that never claimed to be one. It looks under System Services now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
1a8a5efe03 |
Grid: the first program to use the screen as a screen
Everything drawn on this machine so far has been text or a bitmap. The tile
engine has been there since the screen was built and only the console had
touched it, and only ever to put a letter in a cell - the one thing it can
do that a plain character display could do too.
Grid redefines a tile, fills all 128 map rows with it, and scrolls by
writing ONE BYTE A FRAME. Nothing moves. The rows above and below the
screen are already drawn, so a screenful of movement costs one OUTA and the
rows that leave the top are still there.
Its tile goes at 200 because the machine wakes with the font in tile memory
- glyph n at tile n, for 135 of the 256 - so a program starting at zero
paints over the alphabet and the shell it is about to hand the machine back
to. Its sixteen colour bands are one tile and not sixteen: the attribute
nibble is added to every index in a cell, so the same 64 bytes come out in
sixteen colourings.
Three things it cost, all of them the same lesson about this machine:
- "SETD.0 X" then "STD.0.1" stores through DP1, which had not been set
yet. It assembles, and the blit then reads its 64 bytes from wherever
DP1 was last left, so the tile came out as noise.
- The palette entry for scheme n is at 0xFC00 + 64n, which reaches
0xFFC0 - four pages, not one. And doubling A by adding B needs B to
hold A, which RSTB is the opposite of. Both went away by writing all
256 entries in order and letting the controller step the address, so
nothing computes an address at all.
- The screen it hands back had the right cells and the wrong colours,
because restoring the map is not restoring the palette.
That last one is a gap in the machine rather than in this program, and is
written up in the CosmOS README. The console's colours live at exactly the
entries the attribute nibble lands on, so any program using the nibble
overwrites them and has nowhere else to write. Grid puts bank 0 back - grey
on black - and leaves the other fifteen. The real answer is a command to
the screen meaning "give me back what you woke up with", the way the
console has one for clearing. There is not one, and this is the first
program that ever wanted it.
Two checks in video.sh, which boots the whole system and reads the pixels
the renderer produced rather than trusting what the program believed.
Breaking the tile fails one and breaking the attribute fails the other.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
|
||
|
|
553882d28d |
Start CosmOS with a script, and let a script hold its tongue
Three things scripts wanted, and they are one thing: a machine that can have a face. /System/Boot/startup.sh runs before anybody can type. Every way of reaching the prompt for the first time goes through it, including the one where there is no disk - in which case there is nothing to find and nothing is said. A MISSING one is ordinary and silent, because a clean install has none and a machine that complained every boot about a file nobody wrote would be teaching its owner to ignore it. One that is THERE and does not begin with #! is the other case entirely: somebody meant that to run. #quiet stops each line being echoed, #loud puts it back. The prompt and the echo go together, because together they are what makes a script look like typing, so a quiet script gets neither and what it prints is all that appears. A nested script inherits quiet - a build that asked for it meant its helpers too - and gets its own setting back when the helper returns. Anything else beginning with # is handed to the shell, which does not know it and stops the script, because a script that asked for something this shell cannot do should not carry on as though it had been given it. clear empties the screen, which the console has been able to do since before there was a screen to do it on. THE PROMPT IS NOW SAID BY WHOEVER SUPPLIES THE LINE. It used to be said at the top of the loop, which is a decision made before the line is read and an answer not known until after - and it was wrong at both ends. #quiet is itself a line, so its prompt went out before anything knew to stay silent; and the line after a quiet script's last one comes from the console, having already been denied one. Off by exactly one line in opposite directions. A first attempt at this remembered whether the prompt had been skipped, which worked and was a flag standing in for a structure. The monitor's assembler prints a prompt of its own, so it reads through shellReadRaw, which is the same source without one. One admission. Handing the console its prompt back when a quiet script ended was a real fix when I wrote it and stopped being one an hour later, because the restructure above means the console's own path prompts whatever the flag holds. The comment claimed it fixed something. Breaking it on purpose changed nothing, which is how that was found, and it is now a comment saying so instead of a line pretending to work. The startup fixture ends QUIET on purpose: nothing puts the flag back when the outermost script finishes, so a script ending #loud would have tested the easy half. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
c28826df77 |
Let a script run a script, four deep
A build script calling a setup script is the first thing anybody tries. What is saved when one script starts another is A POSITION AND NOT A BUFFER: the name, which block comes next, how many are left, and where in the block it had got to. Seventy bytes, and they sit next to each other in the data segment on purpose so that saving them is one copy. The block itself is read again on the way back, which costs one disk read per return and saves 257 bytes a level - the inner script reads its own block into the single buffer there is, so coming back means fetching the outer one's block again and landing on the byte it left. The slot is reached by stepping rather than by multiplying, because this machine has no multiply and the depth is never more than three steps. Four levels. Deep enough for a script calling a script that calls a helper, shallow enough that a script running itself says so rather than filling memory. A line that fails now stops every level and not just the innermost, because a build whose helper failed should not carry on in its caller. The caller's place is saved BEFORE the new file is looked at, and put back on every way out that is not success. Opening writes the name into the live state in order to ask the disk about it, so by the time "there is no such file" is known, the caller's place has already been overwritten - a failed 'do' inside a script would otherwise leave the script that ran it reading from a name it never chose. The test resumes in the outer script's SECOND block, which is the case the whole design turns on and the one an ordinary nesting test would miss. Breaking the re-read, the save, or the limit each fails it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
a12d61fb80 |
Give the shell an echo, so a script can say what it is doing
Say.sbx has printed words since long before there were scripts, and is the wrong shape for one. It is a program: it has to be found on the disk, loaded and started, it prefixes what it was told with "it says:", and the system prints "finished" after it. Three lines of noise around one line of narration, and a load off the disk to produce them. echo is a command, so it costs a comparison. With nothing after it, a blank line - which is what anybody expects and is worth having to space a long script out. Not called "say", although that is the better word. Built-in commands are tried before the disk and always win, so a built-in say would shadow Say.sbx and quietly change what every existing script and test meant by it. Also puts "do" in the help, which the commit that added it forgot, and splits the help text: the new lines pushed it over the assembler's 255 character limit for one string. That failure was hidden for a few minutes by a 'make' whose errors were going to /dev/null - the build kept the disk it already had, and the machine cheerfully reported "I do not know: echo" from a system assembled before echo existed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
2466d79d9c |
Let the shell run a file of commands
'do <file>' runs the lines in a file as though they had been typed. The
only thing a script changes is where the next line comes from: everything
below shellReadLine - splitting the line, matching it, loading a program -
cannot tell the difference and does not have to.
What makes a file a script is '#!' on the front of it, not its name and not
a flag in its entry. The rule this filesystem keeps is that an entry holds
only what the content cannot say about itself, and a script can say what it
is; the loader already refuses anything that is not SBEX, so the two kinds
of runnable file turn each other away without either knowing about the
other. It is also the deferred half of the file-typing design, which said
to wait for a second kind of runnable thing before building any of it. This
is that second kind.
'#' is a directive and ';' is a comment, as in SplitBit assembly - one rule
across the machine rather than two dialects. Not Unix's convention: there
'#!' really is a comment that only the kernel reads, while here the shell
requires it and refuses the file without it, so calling it a comment would
be a lie about what it does.
A script stops at the first line that does not work, which is what the
LineFailed groundwork was for. Comments and blank lines are dropped by the
reader rather than by the dispatch, so they are not echoed either. A script
running out hands back to the console rather than ending the shell, because
running out of file and running out of typing are not the same thing. The
interactive assembler reads through the same path, so a script can contain
a block of assembly.
Three things this cost that were not obvious:
- RET puts A and B back, so a routine cannot answer in them. scriptByte
returning the character in A assembled, ran, and handed the caller its
own A back every time. It answers in memory now.
- A last line with no newline is still a line. Text files do not reliably
end with one and an editor eating it is a bad way to find out a command
did not run.
- Not LastStatus. See the commit before this one.
Six checks in three tests, two of which are about byte positions rather
than behaviour - a command lying across the boundary between two blocks,
and that missing newline - so their fixtures are generated rather than
committed, where an editor cannot helpfully repair them.
Nesting is not in yet: a script cannot run a script. That wants a stack of
positions rather than the one the reader keeps.
Also derives native.sh's self-hosting source list from cosmos.asm's own
#Include lines. It was a hand written list and went stale the moment
script.asm existed - the fourth time a list beside a thing has drifted from
the thing - so it now asks the thing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
|
||
|
|
e52d8d84f8 |
Collapse the two makefiles into one
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 |
||
|
|
3da020898c |
Write down what the machine costs, before it has to be worked out again
Three measurements that would otherwise be re-derived from scratch in three months, put where a reader will meet them rather than where somebody has to know to look. The README's cost model section says what a nominal 1 MHz actually means: about 270,000 instructions a second, since the average instruction costs 3.72 cycles. The same program therefore takes 3.72 times the wall clock it did when a cycle was an instruction - nothing got slower, the number got honest, and the number it replaced described no machine anybody could build. Beside it, the two figures that put that in proportion: the emulator runs 195 million cycles a second in --fast, and CosmOS assembling CosmOS is 654 million of them, which is eleven minutes at a megahertz, 3.3 seconds at --fast, and six and a half seconds at a hypothetical 100 MHz. That last is pessimistic rather than hopeful: it assumes hardware overlaps nothing, which is the same conservative reading the controller's cost model takes. The CosmOS README gets what that means for its assembler, which is superlinear in what it reads: 1,383 cycles a byte for a four kilobyte source, 6,290 for a hundred kilobyte one. IT IS NOT THE DISK, and the number that says so is worth keeping - 654 million on a disk carrying the whole source tree against 653 million on a flat one with a sixth as many files. The suspected cause is walking a table of some nine hundred labels once per reference, suspected rather than measured. Nothing is being done about it, and the reason is written down too: development happens with the host assembler. But faster hardware buys a constant factor and does not change the shape of the curve, so the program that forces this is not CosmOS - it is the first one twice its size. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
7073b972e6 |
Say what went wrong, and give the file tools room for a path
The makefile on the disk was fine. "Makefile" is not "makefile", and SBFS names are case sensitive - but neither tool said so, and both failed in ways that pointed somewhere else. MORE PRINTED A NUMBER THAT MEANT NOTHING. "cannot find the file, error 2" invents a vocabulary the system does not have: the filesystem library documents its answer as zero or not zero, never as a code, so 2 could not be looked up anywhere. It says "there is no file by that name" now, which is the only way opening fails that a person can do anything about, and is nearly always a name typed slightly wrong. EDIT SAID "0 LINES", which is also what an empty file that IS on the disk says. A name typed slightly wrong therefore looked exactly like the document you meant to open, right up until you saved it somewhere new. It says "new file" instead. Two bugs came out of writing that, and both are worth more than the feature. The first is mine and the label lied to me: loadNothing is not where a load FAILS, it is where every load FINISHES, reached at the end of splitLast on files that opened perfectly well. A flag set there on the strength of the name was set on everything. It is called loadDone now, and the failure has its own name. The second is older and general: a program is loaded once and may be run many times, so "load Edit.sbx" then "run" twice is two sessions over one copy of the Data Segment. Anything a session changes has to be put back by the session. A zero written in the Data Segment is the state a program starts in the first time and never again - and cosmosEdit runs Edit twice from one load, which is why it caught it immediately. AND THE FILE TOOLS COULD NOT ADDRESS THE TREE THEY NOW HAVE. Edit took 23 characters of name and More and Type took 29, which were right when everything lived in the root. With the sources mirrored onto the disk, "/Source/CosmOS/Assembler/classify.asm" is an ordinary thing to type - thirty-seven characters, cut down to a name meaning something else, or nothing. All of them take sixty-three now, which is what the shell reads of a command line, so nothing longer can arrive. Wander with it, since a directory is a path too. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
0852666e73 |
Mirror the source tree onto the system disk
A list of files in a makefile goes stale the moment somebody adds a program and forgets to name it, and what they forgot is invisible until they go looking for it on the machine. So SplitDisk gained a mirror command and the disk rule is one line: putting a file where the others live is now the whole of putting it on the disk. EVERY FILE GOES THROUGH put AND EVERY DIRECTORY THROUGH mkdir. That is the point of it - mirror adds a walk and no filesystem code at all, so anything the format refuses here it refuses everywhere, in the same words. What is new is the walk, and the walk is what the six checks in Tests/disk.sh are about: that it goes all the way down, that it leaves dotfiles and named directories behind, and that a name too long stops it. REFUSED RATHER THAN SKIPPED, because a disk quietly missing a file is the exact failure a mirror exists to prevent. Which meant four sources had to be renamed - a directory entry holds 22 characters and they were 23, 23, 24 and 29: 16bitSegmentedSieve.asm -> 16bitSieve.asm 16bitSegmentedSieveModern.asm -> 16bitSieveModern.asm consoleInterruptTest.asm -> consoleInterrupt.asm controllerWriteTest.asm -> controllerWrite.asm The test names in the manifest are unchanged, since those are identifiers and every recorded result is filed under them. Only where the source lives has moved. The entries are sorted before anything is written. readdir hands them back in whatever order the host filesystem feels like, and a disk image that comes out different from one run to the next is an image no test could compare against another. The disk grew from one megabyte to four and from 192 directory entries to 1,024. The sources are 2,850 blocks and the mirror filled the old directory on its first run, which is a thing that should not need thinking about again. The Tests fixture disk is deliberately NOT mirrored. It is a controlled fixture with known contents, and the shipped disk is the one meant to be useful; they want different things. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
761c11a66b |
Add run-voyager, and make running depend on the disk
The jitter was a stale disk. An image built before the escape sequences came out still had the old Snake on it, which sends ESC [ H every frame: the console swallows the escape, because it is below the font's first character, and then draws "[H" as two ordinary letters. So every frame began two characters to the right and one line further down than the last, and the board walked down the screen. Not timing at all. WHAT IS ON A DISK IS WHATEVER WAS BUILT WHEN THE DISK WAS MADE, and a machine whose console has changed will start that image quite happily. That is the compatibility break we chose when the parser came out, and it is fine - but it should not be a puzzle, so both run targets depend on the disk rather than merely using it, and both READMEs say why. run-voyager boots the same disk on the machine with a screen. It existed only as EMU=../Voyager in front of run-cosmos, which is not a thing anybody should have to know. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
79727044b7 |
Reboot, and the machine device that makes it possible
Until now the only way to restart was to stop the emulator and run it again, which meant the one thing the machine could not do was the thing Once was written for. The loop now closes without leaving it: > Once /System/Boot/bare.bin next start: /System/Boot/bare.bin, once > Reboot starting again stage two just this once: /System/Boot/bare.bin bare metal: no system, just this Writing 1 to port 0x13 asks the machine to start over. A PORT RATHER THAN A SERVICE, because a reset has to work when the system does not: something only askable through SWI would be unavailable in exactly the case that wants it most, and a program that owns the whole machine has no system to ask. It is device class 0x04, in the range kept for the machine rather than among the peripherals, because it is not one - it is not attached to anything and cannot be unplugged. WHAT A RESET REPEATS IS HOW THE MACHINE STARTED. Named an image, the emulator places it again; named none, the ROM is shadowed again and reads the disk. Anything else would mean a reset changed what the machine IS, which is the one thing a reset must not do. Both are tested. Taken between instructions, because a device cannot restart the machine from inside the instruction that asked: the CPU is part way through a step and its state is not yet anything a reset could leave behind consistently. The disk stays attached and keeps everything written to it - that is what warm means. The vector table is cleared, which is the one deliberate departure from leaving memory alone: a vector points into whatever installed it, and after a reset that program is not running, so a handler left behind would aim an interrupt at an address belonging to something gone. It is the argument CosmOS already makes at exit, applied to the machine. Reboot is 45 bytes, most of them the word it prints. |
||
|
|
7b28f48f52 |
Once: start something else on the next start, and only that one
A program that owns the whole machine had nowhere to run. It cannot be started from the shell, because starting it means there is no shell, and pointing boot.cfg at it means a machine that keeps starting it - which is a poor place to find a mistake in something written five minutes ago. Once writes /System/Boot/once.cfg, in the same format as boot.cfg and read with the same routines, because a second format for one setting would be a second format. The loader reads it before boot.cfg and DELETES IT BEFORE IT JUMPS, which is the only moment there is: after the jump the loader does not exist. Consumed by being read rather than by working, so a one shot that hangs cannot hang twice - the request is gone before the image ran, and the next start reads boot.cfg like any other. THE BOOT STATE IS NOT TOUCHED, and the first version got that wrong. It marked the start the way any other start is marked, and then every successful bare metal boot reported that it had never arrived - because a program with the whole machine has no filesystem to clear a mark with and is doing nothing wrong by not having one. Found by running it: the image printed its line and the next start still said the last one did not. Three disks, each a start further along, so none of the tests depends on another having run. The loop is closed on the machine now: write it in Edit, assemble it with Asm, ask for it with Once, restart, watch it own the machine, and the system comes back without being asked. |
||
|
|
89c667848b |
Edit read a file into a buffer it never checked the size of
Opening hello.asm showed a thirty one line file as three, one of them cut short. Opening it again hung the machine: the emulator kept running and nothing ever answered. Entry is the buffer a line is read into, and it is followed in memory by TextHead and ArenaFree - the head of the document, and the pointer its line allocator hands out. The loop that splits a file into lines copied characters in WITH NO BOUND AT ALL, so a 94 character line wrote thirteen bytes over both of them. The list head then pointed into the middle of the text and the allocator handed out an address inside the file, which is why the second open walked a list that led back into itself for ever. Typing was always safe. osReadLine is told how much room there is, so a new document behaved perfectly and a source file did not - which is exactly how the user found it, and why it looked like a mystery rather than a bug. The bound is there now, and the buffer is 128 characters: what a line is everywhere else on this machine, the same number configuration files use, rather than a second answer to a question already answered. hello.asm fits. A file with a longer line is REFUSED rather than shortened. This is an editor - a line cut on the way in would be written back cut, and the file damaged by having been looked at. It says so and exits with a status of one, which it can do since this afternoon; the file is byte identical afterwards, and the test checks that. Opened twice in the test, because once is not enough to see it: the first open does the damage and the second is what never returns. This is the third time this shape has turned up: a buffer written past its end into the variables that happened to follow it. The prompt walked off CwdText into the shell's own command names; the assembler's output ran into its label table. Every one was found by a person using the machine. |
||
|
|
87d819847e |
A program can say how it went
SWI osExit takes a status in A, and the shell keeps it. Fifty eight exits across twenty three programs now say deliberately whether they worked: 25 did what they were asked, 24 did not, 9 were asked wrongly. Compare is the exception and says so - one there means the files differ, which is a result rather than a failure, the way diff has always had it. IN A RATHER THAN Q, which is not a departure from the rule that a service answers in Q. This one takes an ARGUMENT, the way osPrintNumber takes A and B, and it never returns to answer anything. A is free precisely because a return would have put it back - and Q is the ALU's output, so a small number costs four instructions there against one in A. The shell does not print it. A program that failed has already said so in words and a number beside that is noise, so osLastStatus hands it back and Status is the program that shows it. That indirection is the point: the number exists for the thing that cannot read words. MARKING THE EXITS FOUND A DEFECT ON THE FIRST RUN. Type and More printed why they had failed and then fell through into the success exit, reporting that all was well. Nobody had noticed, because while the only reader was a person, the person could see both the complaint and the claim. Two smaller things. Snake sets the console to line mode and then exits with zero, and the linter flagged the second RSTA as redundant - an exit status and a console mode, equal by accident, which is the class that must never be collapsed. And the README still taught answering by writing into the frame, three months of habit that SRET replaced yesterday; that section is gone and the one describing SRET stands in its place. |
||
|
|
b4206673a6 |
The clean install disk is one the machine can start itself from
make run-cosmos handed the emulator a boot image AND a disk, so it took the direct path every time: memory placed from outside, nothing on the disk consulted about it. The whole boot chain was built and tested and then not used by the one command that runs the system. The disk is formatted with a boot area now, stage two goes into slot zero as raw bytes, and the system is an ordinary file at /System/Boot/cosmos.bin - which is what boot.cfg would choose between if there were one. There is not: stage two falls back to that name when the file is missing, and a clean install with nothing to configure is the right default. run-cosmos names no image, so the machine starts itself. The old behaviour is run-cosmos-direct, which is worth keeping and worth naming honestly: it is what a debugger does, and it is what to use when the thing being debugged is the boot chain, since it skips the boot chain. /Source gains stage1.asm and stage2.asm, so the machine can rebuild what starts it. Everything stage two includes is already in /Lib, so Asm stage2.asm on the machine produces the bytes a boot slot takes. Stage one is the exception and always will be: it is the ROM, and the one part of this that a disk cannot replace. Second slot left empty, which is what makes replacing the first survivable. |
||
|
|
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. |
||
|
|
ce2a2cd7e6 |
Settle is a program, and a machine with no fallback still starts
The boot state opened a loop that could not be closed from inside: the machine said "settle it to try again" and gave you no way to do so. Settle closes it, in 349 bytes. A PROGRAM RATHER THAN A SHELL WORD. The shell is for the things that cannot be done without it, and this is not one - it reaches the system through SWI like anything else, which means it can be replaced, left off a disk, or called by whatever comes to call programs in turn. That last one is the point: a shell word is not callable by anything. Two services for it. osBootState answers in Q, and a machine with no disk answers settled, because there is nothing there to be unsettled about. osBootSettle puts it back. SETTLING IS THE ONLY WRITE A PROGRAM GETS - marking a start as trying or fallen back is the loader's business, and a service that let a program claim either would let it lie about something the loader has no way to check. And a hole the tests walked into, which was mine rather than theirs. With no fallback configured, a failed start left the machine unable to start at all: the mark said do not use the system, and there was nothing else to use. That turns "the last start failed" into "no start is permitted", which is worse than the problem the mark was added to solve. With nothing to fall back to it now tries the configuration again and says so - a failure that was passing recovers, and one that is not leaves the machine exactly where it would have been without any of this, which is the most that can be promised when there is only one thing to start. docs.sh caught both new services having no row in the services table before anything else did. |