eee95ef0ce7c125894357dfcb32ed33bcea1a27f
9
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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 |
||
|
|
66e7b84272 |
The screen is two banks: an atlas and a screen
Tiles and colours are written when a program loads; the map is written whenever anything moves. Sharing one 64K bank made them compete for room neither needed all of, and had a worse consequence than being cramped: a bitmap covers the whole bank, so entering bitmap mode destroyed the font. A program could not draw a picture and then say anything about it. Split, each gets a whole bank. The atlas holds the tiles and the palette, the screen holds the map or a bitmap, and a picture now costs the map and nothing else. It also leaves 48K free in the atlas, which is where the sprite table and a second page of tiles are going. No new mechanism was needed. A bank is registered by naming the port that owns it, so a device with two banks needs two ports that own memory: the base port keeps the atlas, since tiles have been at 0x0000 since there was a screen at all, and 0x3A owns the screen. The registry now answers honestly about which ports in the block bring memory, where it used to say all sixteen did. CosmOS never addresses video memory except in one place - the screen save, which walks 196 pages of it. The page number already says which bank a page is in, so screenBankFor works it out rather than keeping a second list beside screenPageFor. Grid and picture.asm register both banks; colours.asm only touches the palette and needed none of it. Tests/video.sh names the memory every write is for, because an address cannot: tile 5 and bitmap pixel 5 are both 0x0005, and a helper that guessed would be right for the tiles and silently wrong for a picture. And picture.asm gained a check, because this change broke it and nothing noticed - registering the second bank leaves DestBank pointing at it, so the palette went into the wrong one and the picture came out black. It was the only thing here found by looking rather than by a test. 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 |
||
|
|
b04e4b7d1c |
Finish making the tune a program
The user's conversion, which I reverted while I was working out whether it was half done or broken. It was half done: an sbx application wants its #Include above #Program, because services.asm ends in a #Vectors block and a #Base written after that has no segment to be the base of. So the include moves up, the bases move a page with everything else, and the test starts it from the shell instead of booting it. It plays for 9,469,987 cycles, 9,423,527 of them waiting, which is the same 567 frames of music it played as a boot image. BEING A PROGRAM MEANS ITS VECTOR IS THE SYSTEM'S TO INSTALL. It brings the screen's, so that it has a beat to play to, and CosmOS puts it in when the tune starts and takes it back out when it stops - a thing a boot image never had to have right, and the second program here to exercise the version two format at all. 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 |
||
|
|
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
|