fee2b1ef1007e3863ae3bc39761d0ec6b416aca8
337
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
ee77d79780 |
A pad is sampled once a frame, and the recorder writes what was sampled
The live state is written by whatever watches real hardware, on ITS clock: a window polls its keyboard once a HOST frame, which is not a machine frame. Read straight through, that made a pad whose value could change in the middle of a machine frame - breaking the one promise the device makes, that asking twice in a frame gives the same answer both times. The manual said it could not happen and the code allowed it. It also made recordings that were not of the flight. The recorder sampled on a frame boundary and the program read whenever it read, so the two saw different bytes. A replay of that is a DIFFERENT FLIGHT, faithfully reproduced: it flew a lander off the top of the screen that had never gone there, and every check said the replay was deterministic and re-recorded as itself, because it was. Both were true and neither was the point. So the live state is latched once a frame. What the machine reads and what the recorder writes are now the same thing by construction rather than by two clocks happening to agree. Real hardware latches a controller once a frame for the same reason. WHAT IS STILL NOT COVERED: the latch itself. Every recorded pad already changes only on a frame, so the tests cannot tell a latched live pad from an unlatched one - the case that went wrong is the one with a real hand on a real controller, which is the case a headless suite has none of. Said here rather than left to look tested. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
de1857f5f7 |
Record every pad, not the one that happened to be first
The first recording ever made with this came back 1,766 frames of nothing. It recorded pad NOUGHT and the controller was somewhere else - which pad one lands on is an accident of the host, the same accident that made Lunar Porter read all four in the first place - and a flight flown for the purpose was lost to it. So every pad is or-ed into the byte. A demo is a record of what somebody DID, and on a machine one person is playing the number it arrived on is not part of that. It plays back on pad nought, where --pad puts the first file given, and any program that reads more than one pad reads them or-ed anyway for exactly the same reason. --record-pad takes one file now rather than filling pads in turn, because there is nothing left for the second one to mean. The check for it plays a recording on pad ONE with nought holding nothing and requires the bytes back. That is the case that was missing: the round trip was tested and passed, on pad nought, which is the only pad it could not have gone wrong on. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
eb695a3f3b |
--keyboard was a flag that did nothing behind a window
Voyager installed its own input hook after machineStart, which had already installed the keyboard file's - so naming a keyboard file and running the window silently got the window, and the flag said nothing about being ignored. Both together is the combination a demo wants. Recording a flight needs the typing that STARTS it to be the same every time, because a human reaching the shell a moment later shifts every frame of the recording after it - while the flying itself has to come from whatever is actually in somebody's hands. So the window only takes the keyboard when no file was named. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
a163c670d0 |
A demo recorder: --record-pad writes what --pad reads
One byte a frame, in exactly the format the player takes, so a recording needs no conversion and there is no second format to keep in step. That symmetry is the feature, and it makes the strongest form of the claim testable: a recording is made OF a playback, and the bytes coming out have to be the bytes that went in. It exists because some inputs cannot sensibly be written by hand. Flying a lander from one base to another is a few hundred frames of steering that has to arrive somewhere eight cells wide, and several attempts at authoring one got within two columns and no closer. That is a piloting exercise rather than a test. Playing it once and keeping what happened is the answer. A BYTE FOR EVERY FRAME, written inside the loop that advances the recordings rather than after it, so a machine that jumped several frames at once still writes one for each. A recording is a timeline: one that skipped the frames nobody looked at would play back faster than it was flown. What is recorded is what the DEVICE WOULD REPORT, not the live state - a recording of a playback that wrote the live state would be a file of noughts. And it is flushed as it goes, because a recording is usually stopped by whoever is playing rather than by the program ending, and a demo lost to a buffer is a demo flown twice. Tests/replay.sh is where this and whatever follows it are checked. Twelve scripts now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
caf5e1f99d |
A base speaks in the window, not into the world
Two bugs with one cause. The console draws into the map, so a message printed while flying was a message the lander then flew over - and printing scrolls, so every one of them moved the whole world up a row. The window is at a screen position and forty cells wide, and neither is true of it. So the window is two rows now: the gauge, and whatever there is to say. The letters are ordinary tiles, because the character generator starts at the space and glyph n is character n less thirty two. The rest of the row is blanked after every message, or a short one would leave the tail of a long one behind it. Opening the throttle wipes the line, because a message that outlived the moment would be read as describing this one. The crash still goes to the console, deliberately: it is the last thing the program says and it should survive the program. A or Start continues from a message as readily as a key does. Somebody flying on a controller should not have to reach for the keyboard to say they have read something. AND TWO TESTS WENT WITH IT, which is the interesting part. cosmosLanderSoft and cosmosLanderPadOne asserted on lines in a transcript, and the lines moved off the console - so both went on passing while checking nothing at all. A test that asserts a side effect rather than the thing itself is always one refactor from being decorative. What they were for is now checked in the picture, where the message actually is. The lander check moved earlier too. The window grew to two rows, so by 1.5 million cycles the lander had climbed behind the status bar - the window doing exactly what it should, and leaving the check counting six pixels of a forty pixel lander. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
bfb515e23b |
Cargo: bases with names, and a landing that is not an ending
Four bases, told apart by the scheme their pad is drawn in, so "the cyan one" is a thing a person can say and a thing the machine already knows. Yellow is missing on purpose: it is the lander, and a base the same colour as the thing landing on it would be a poor joke. Land empty at a base and it loads cargo for the base ACROSS THE MOON, two along - so the pairs are cyan with red and green with blue, and the wrapping surface is a route rather than scenery. Land carrying at the right one and it takes the cargo and pays eighty units of fuel. Land at the wrong one and nothing happens, which is why the destination will want to be on the screen. A LANDING NO LONGER ENDS THE RUN. The lander rests where it is, exactly on the surface with both speeds zeroed, until the throttle opens again - which is the only way to stop being landed. Gravity does not pull on something already sitting down, and a base does not hand out cargo sixty times a second to a lander parked on it. The pad array holds the base's number plus one rather than a flag. Nought still means no pad, so it is still one lookup, and a flag would have to be followed by "and which of the four" - the same walk done twice for an answer already in hand. Pads are eight columns rather than four. Four was 32 pixels in a moon 1024 round, which is a target somebody flying by feel misses over and over. WHAT IS NOT COVERED, and why: the delivery and wrong-base paths need a lander flown from one base to another, and hand-authoring a recorded pad input that hits an eight column pad across a 128 column moon is a piloting exercise rather than a correctness one. Several attempts got within two columns. Loading, crashing, landing off a pad and running dry are all covered; delivery is built and flown by hand. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
7257ad369c |
Landing pads, carved rather than looked for
A random walk does not leave flat ground and a lander wants some. Four pads are cut into the moon after it is generated, each four columns levelled to whatever height its first column happened to have - so they sit in the landscape rather than on a shelf above it. The moon decides where they are; this only decides that they are flat. Searching for flat spots was the alternative and it can fail, which means a fallback that carves anyway - the carving, plus a search nobody needed. They are marked by an ATTRIBUTE and not a tile of their own, which costs no art at all: a nibble is added to every index in a tile, so one solid block is grey moon or a cyan pad depending on the byte beside it. Which columns are pads is an array, because asking has to be one lookup. Four comparisons per column per row is 12,800 of them for one screen, and the landing verdict asks the same question again. THE LANDER STARTS ABOVE ONE, because that is where a porter's day begins. Starting in the middle of nowhere meant a straight descent landed in the middle of nowhere, which is a fine thing to be able to do and a poor thing to have to. That change cost the crash test its teeth, and the way it did is worth keeping. It held nothing at all and let the lander fall - and a short drop onto the high ground of the base you started above is survivable, which is correct, and left the test saying nothing. It holds Right now: lateral speed has no limit and nothing slows it, so a slide always ends badly however the rest is tuned. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
6073086584 |
The system takes a leftover window down
Lunar Porter put a fuel gauge up and never took it away, so the shell came back with FUEL across the top and the cursor underneath. Clearing did not help: a window is a layer at a SCREEN position that does not scroll, which is exactly what makes one left behind unpleasant - it sits over whatever comes next and cannot be scrolled off, cleared away or typed past. Taken away rather than given back, like the sprite table and for the same reason: nothing the shell draws is a window, so there is nothing to restore. And a program that FAULTED while one was up could not have taken it down itself, which is why this belongs to the system rather than to whichever programs remember. The check for it needed writing twice, and the first version was the familiar kind of wrong. It counted the gauge BAR's colour - and the bar disappears on its own whatever happens, because it is drawn with a tile the screen save puts back, so the check passed with the teardown deleted. What actually survives is the LABEL, in font tiles the shell needs anyway. So the screen is cleared afterwards and read cell by cell. With the window down a cleared row is "> " and a cursor; with it up the same row is F, U, E, L. Cells one and three being empty is the whole difference, and it is 29 and 22 pixels of it rather than a threshold somebody has to believe. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
2274be4b68 |
Lunar Porter, rung three and a half: fuel
Every thruster costs a unit every tick it fires, so holding two at once costs two - the honest price, and it makes a drift you corrected expensive in a way a drift you avoided is not. AN EMPTY TANK IS NOT AN ENDING. There is no message and nothing stops: a lander with no fuel is still flying, it just cannot do anything about where. What happens next is gravity, and gravity is patient. The test for it holds the thruster from the first frame to the last and crashes anyway, which is what says the fuel is real - a lander that could hold Up for ever would land every time, and the economy this is the first half of would have nothing to buy. The gauge is in the window, which is what the window was built for two commits ago: a bar at a SCREEN position, so the moon turning underneath does not carry it off. Thirty five cells after a label, redrawn whole every frame because seventy bytes out of one port is cheaper than working out which of them changed. A byte of fuel, and a byte is enough. Over eight it is a bar of up to thirty one cells - a shift, because there is no divide - and at a unit a thruster a tick it is about forty seconds of holding the engine open. Sixteen bits would be more arithmetic for a number nobody reads to the unit. Cargo and the bases are the other half. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
17c8da111f |
A window: a layer that does not scroll
The map moves and this does not, which the map alone cannot express. The scroll registers move ALL of it, so a score printed into the map slides away, and one printed into whichever rows the view happens to be showing jumps a pixel at a time as the fine offset changes. Port 0x3D is how many rows tall and 0x3E is which row it starts at. Nought tall is no window, so a cleared screen has none and every program written before this means what it meant. A start row is a register because a status bar along the bottom is as common as one along the top. IT HAS ITS OWN MEMORY, and that is the argument for it. The cheaper design draws the top rows of the MAP without the scroll applied - no new memory, one register - and makes those rows part of the playfield's ring, so a game that scrolls vertically has to route its world around its own scoreboard for ever. The point of a status bar is that it is not somewhere in the level. Lunar Porter does not scroll vertically today and will the moment an orbit is a thing you can reach. 0xC000 in the screen bank, which the map does not reach: it ends at 0xBFFF. Same cells, same tiles, same pages, same schemes. Being in the screen bank makes it per screen, so flipping the buffer flips the status bar with it - what a double buffered game wants, and surprising the other way round. Drawn over everything, sprites included. A sprite that could cover the fuel gauge would be a bug in every game that had both. Tile modes only. In bitmap mode the picture is using that memory, so a bitmap program pins things to the screen with sprites, which are in screen coordinates for the same reason. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
e3eccd17a8 |
Settle the view before saying how the landing went
The console draws into the map and the map is what is being scrolled, so a message printed while the view was three pixels into a cell came out three pixels off the top, with as much of its first row missing as the cell above it had lost. The flying is over by then, so the fractional part of the view has no more work to do. Putting it back is what makes the whole message visible. This is not the general problem. A status bar that has to stay readable WHILE the map moves is a different thing entirely, and nothing here solves it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
a074a831f0 |
The shell scrolls, and the moon was drawn where nobody was looking
Lunar Porter never put the row origin back. The map is a ring 128 rows tall that the screen shows 25 of, and the shell leaves that origin wherever its last command finished - so a moon drawn into rows nought to 24 while the screen is reading from row forty is a moon nobody can see. It came out as terrain missing, or half there, depending on how far down the prompt had got. Running Pad first was enough; so was holding Return. Nothing here is tidiness. It is the difference between the rows a program WRITES and the rows the screen READS, and only one of those is under the program's control. Grid has always known this; Lander did not. The check for it needed writing twice. Forty returns caught nothing, because the shell runs an eighty column screen which is FIFTY rows tall - forty returns fill it and never scroll it, so the origin was still nought and the test passed against a build with the fix taken out. The screenful that matters is the one the shell is using, not the one the program is about to ask for. At eighty it is 28,608 pixels of moon with the fix and none at all without it. break.sh is what said so. The first version of this check looked exactly like a passing test. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
f7be843ed9 |
Lunar Porter takes any controller, not the first one
A controller does not always arrive on pad nought. The front end hands out the numbers the host gave it, so a game that reads only the first one works on the machine it was written on and silently does nothing on the next - which is the shape of "the pad is detected, Pad shows it, and the game ignores it". Four reads and three ORs. One person flies this and which socket they plugged into is not a thing they should have to know. Presence is any of the four bits rather than the low one, for the same reason. The manifest's pad column takes several fixtures now, comma separated, and they fill the pads in turn. So cosmosLanderPadOne holds nothing on pad nought and flies the whole landing on pad one - a test that fails on the version of this program that shipped an hour ago. Also confirmed while looking: raylib 6 does refresh which gamepads are ready every frame in PollInputEvents, so a hot-plugged pad should be seen. Whatever is stopping that is above us and worth a separate look. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
6def343e97 |
Find Raylib's link flags instead of guessing one set
Raylib 6 calls X11 directly from GetClipboardImage, so a static libraylib needs -lX11 on a Linux desktop where 5.6 did not - and because it is an archive, one function nobody calls drags in the whole object and every X symbol with it. THE OLD SHAPE FAILED IN THE WORST AVAILABLE WAY. One hardcoded guess was test-linked, and when the guess went short the probe reported that Raylib was not installed - so make quietly stopped building Voyager and said so in the words it keeps for a machine with no graphics library at all. The answer looked like an absence and was a missing flag. So the candidates are tried in order and the first that links is the answer. The probe and the flags are the same thing now rather than two facts that can disagree, which is the property that was actually missing: a probe that tests something other than what gets built can only ever be a coincidence. Verified against 6.1-dev: Voyager builds and all 210 tests pass under it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
df50c2f0f8 |
The pad was working; the game was told there was not one
0x64 counted only the RECORDED pads. So a controller plugged into Voyager reported its buttons perfectly, and every game asking whether there was a controller was told no - which is exactly what Lunar Porter asked, once, at startup, before falling back to the console for the rest of the run. The cause is worth naming: a front end calls padSet every frame for every pad, so "held nothing" is the commonest thing it says and cannot also mean "there is no pad here". Connected is said separately now. Pad nought is always there behind a window, because the keyboard is behind it - which is the useful answer rather than the literal one. And Pad.asm, which is what should have existed before any of that guessing began. It prints a line whenever a pad changes, and tells apart the three states that look identical from inside a game that will not respond: one nobody noticed, one mapped to nothing, and a mapping that is wrong. WHY A PROGRAM AND NOT A PRINT IN THE FRONT END: because the question is what the MACHINE can see. A front end reporting what it thinks it is sending answers a different question, and the gap between those two is the whole of this bug. It also found that osPrintNumber takes A as the HIGH half - the same way round as the shift register and every other pair here, and not what a byte in A wants. Every value came out 256 times too big. Gravity is one frame in ten rather than six. The ratio between thrust and gravity is the feel; how often the tick comes round is how fast that feel arrives, and one in six was still touchy. Same lander, more time to think. And the verdict waits for a key. It printed and left immediately, taking the screen with it - so the one thing worth seeing, the lander sitting on the ground it had just reached, was gone before it could be looked at. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
8eb4e4d67e |
Lunar Porter, rung two: it lands, or it does not
The terrain is an array in Data Memory rather than something read back out of the map, and that is the whole reason this is cheap: the ground under the lander is one index into 128 bytes, where asking the screen would be a transfer through the controller every frame. The column is the world position over eight, masked to the moon's 128. The surface is that column's row times eight - three turns left of the shift register, since a row is at most 24 and 192 fits in the low half. The feet are the lander's top plus its eight pixels. WHAT DECIDES IS THE SPEED AT THE MOMENT IT ARRIVES. Both of them, and both have to be gentle: three quarters of a pixel a frame downwards and half of one sideways. Sideways is the tighter on purpose, because a landing that was soft downwards and sliding is a lander on its side - which is the interesting half of the difficulty, and the half the drift bar was blind about until it existed. Two fixtures say it works, and they differ only in what was held: one holds nothing and falls the whole way, the other pulses the thruster six frames in sixteen and survives. Same terrain, same seed, same keys. Also: the gamepad did nothing, and the reason is that the four direction buttons are the D-PAD. A lot of controllers made this century have one nobody uses - the thumb goes on the stick, which reports as an axis rather than a button - so a pad that was plugged in and working correctly did nothing at all. The stick counts as held past halfway now. Untested here, because there is no controller in this environment and the suite runs headless; Voyager also says at startup which controllers it can see, so a pad that still does nothing can be told apart from one nothing noticed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
db0c26e13f |
A bar for the drift, and a lighter touch sideways
A moon has no air, so a sideways drift never stops by itself and stopping one means cancelling the velocity EXACTLY. That is not hard to do; it is hard to do blind, which is what it was - a number nothing on the screen said anything about. So sprite one is a bar whose width is the drift. It runs right from the middle of the screen for a rightward one and left for a leftward one, so which way is as plain as how fast, and stopped is the one state with nothing drawn at all. The whole of it is a target width written once a frame; the device stretches one tile into it and the program draws nothing. Sideways thrust is one a tick rather than two. At two, the smallest correction available was twice the size it needed to be and overshooting was the normal outcome. WHICH ZERO MEANS NOTHING TURNED OUT TO MATTER. A target width of nought is the NATURAL width, not an empty sprite - so a bar with no drift in it came out eight pixels wide, sitting at the middle of the screen, saying "stopped" in the same shape it says "drifting slightly". What draws nothing is a SIZE of nought, which is the other zero in the other byte. Both meanings are deliberate and documented and it still caught me out inside a week of writing them down. The check for it earned its place by failing on that before it was found, which is the best evidence a check can offer. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
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 |
||
|
|
fed1453e6e |
Controllers: four pads that say what is held
The console says WHICH KEY WENT DOWN, which is the right shape for typing and the wrong one for playing. A game wants to know what is being held, this frame, possibly several things at once, and a stream of presses cannot say that: a key that is down and staying down sends nothing at all. Lunar Porter's thrust is a burn per press for exactly that reason. So a pad is its own device on ports 0x60 to 0x6F, reporting a LEVEL. One read gives every button at once, holding is the natural thing to express, two directions together cost nothing, and reading does not consume it - a game may ask twice in a frame and be told the same thing both times. Four of them, because a party is four. They cost a port each and nothing at all when unused. The directions are the low nibble so "which way" is an AND with 0x0F; the buttons are the high nibble for the same reason. 0x64 says which are really there, so a game can ask for a controller rather than sitting silent while somebody presses things at it. They never interrupt: a game polls once a frame because that is when it draws. KEY-UP ON THE CONSOLE WAS THE OTHER WAY TO DO THIS AND WAS REJECTED. A terminal hands over characters and can never report a key coming up however it is asked, so it would have been a thing that worked behind a window and silently did not down a wire. A separate device can honestly say it is not there. Voyager drives pad nought from the keyboard as well as from any real controller, OR-ed rather than chosen between, so a game written for a pad is playable on a machine with none and unplugging one mid-game does not leave somebody holding nothing. And a recorded path, which is what makes any of it testable: --pad names a file of one byte a frame, and the manifest has an eighth column for it. A BYTE A FRAME AND NOT A BYTE A READ - a level asked twice in one frame has to answer the same both times, and a file that advanced per read would depend on how the program happened to be written. Voyager's own tests run headless with nobody holding anything, so without this the device would be exercised only by somebody playing: the state the console's line editing was in when it broke twice in two days. 0x50 is the timer, not free. The block this went in was chosen after looking rather than before. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
88ecb208f4 |
The coarse scroll register was being sent the wrong register
CALL eighth OUTA 0x36 RET puts A back the way it found it, so the column origin was written the high byte of the position that had been passed in, and the answer the subroutine had worked out went nowhere. The fine register was computed inline with OUTQ and was correct, which is exactly what it looked like from the outside: smooth scrolling within a cell that never advanced one. Q is the only register that crosses a RET. Every other answer in this program already came back in it; this one had been written as if A would do, and A very nearly does, which is what makes it worth a comment rather than a fix. Gravity was Jupiter's. A sixteenth of a pixel per frame per frame is the smallest step this arithmetic can take and it crossed the screen in a second, so it is applied one frame in six instead - which divides the pull by six and costs a byte and a compare. The alternative was a finer unit for velocity than for position, and that means a shift every time one is added to the other, twice a frame, for ever. And the check that catches all this now looks 1.5 million cycles in rather than twelve. The first number came from assuming a program that saves a whole screen takes a long time to start; it does not, and by twelve million the lander had flown seven hundred frames and left the picture. A capture near the beginning is worth more than a tuned one - there is less between it and the start that can move. 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 |
||
|
|
f8c3db5d56 |
A tool for breaking things, since doing it by hand went wrong twice
A check that passes proves nothing until it has been seen to fail. Doing that by hand failed twice in two days, and BOTH TIMES IT LOOKED LIKE A RESULT - the suite ran, went green, and read exactly like "this check does not catch that". Once the edit produced code that would not compile, make failed, the exit status was not looked at, and the previous binary ran the suite. Once the anchor was right and the filename was wrong, so nothing was edited at all. Neither had anything to do with header dependencies, which have always worked: DEPFLAGS is -MMD -MP and every .d is included. What was missing was a harness that refuses to report a result it did not earn. So Tests/break.sh checks every step of its own work and treats anything unexpected as a hard error rather than a green run. Not finding the break is the answer it exists to give, and it is worthless if it can also be the answer when the break never happened. It restores the file on the way out, including on an interrupt. It is not in the suite and docs.sh does not count it, for the reason makedisks.sh is not counted turned round - but being left out of the count is not being left out of the manual, and that gap is where a script goes undocumented for months. So docs.sh now requires both of them to be described, and caught this one being missing. Also: video.sh reads the fixture disks and does not build them, so after make sanitize clears the build directory it reported SEVEN product-looking failures for a missing file. It builds them now and says so. 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 |
||
|
|
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 |
||
|
|
4b109f704c |
The shell starts each line with the Stack where it left it
Every failure in this shell abandons a frame. commandFailed is reached with CALL and never returns: it marks the line and branches to the prompt, which is the idiom every command uses and is why a failure needs no unwinding anywhere. What it costs is the frame of that call and of everything between the prompt and it - twenty bytes for a name that was never set, more from somewhere deeper - and nothing ever gave them back. MEASURED BEFORE IT WAS FIXED. Twenty failed lines moved the Stack Pointer from FFFD to FE6D, and it only ever went one way. Nothing had noticed because it takes thousands of failures to reach anything and nobody types thousands of anything. A loop in a script would, which is why this is worth doing before there are loops rather than after. So the loop starts each turn from a known place. SystemStack is NOT that place: it is taken when a program starts, so that the shell's Stack can be given back when the program stops - which means it holds wherever the shell had got to at that moment, the value that needs correcting rather than the one to correct from. ShellStack is taken once, at boot, when nothing is happening. Second use of MVDS in the system, and it earns it for the same reason as the first: a Stack that is right by construction beats one that is right because everybody remembered. Break prints the registers, so the test is two dumps with eight failures between them and a requirement that they agree. 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 |
||
|
|
749fef8ce2 |
The shell's own words, as a table and not just a chain
The dispatch is a run of "is the line this name" comparisons. That is fine to execute and impossible to WALK, and completing a half typed command needs to walk them - so the names have to be data as well as code. They nearly were already: DirName through ExitName were fourteen zero terminated strings sitting back to back, which is a table by accident of layout. This makes it deliberate. MonitorName joins them, the run is labelled, and a count goes underneath because a run of strings does not say where it stops. WHAT MAKES IT A TABLE IS THE ZEROES. Each name ends in one, so the next begins after it: no pointers, no lengths, and adding a command costs a line. Tests/docs.sh reads both the dispatch and the run and compares them, because the two can disagree and every way they do is quiet. A command added to the dispatch and not to the run simply never completes, which nobody would think to check by hand. Something put BETWEEN the strings is worse: the walk ends there and takes every command after it, and the machine goes on working perfectly except that Tab knows about six things instead of fifteen. All three break that way and say something useful. Putting one byte in the middle of the run reports that it holds ten names against the fifteen claimed, which points at roughly where. Groundwork for Tab completion. Nothing uses it yet. 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 |
||
|
|
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 |
||
|
|
c1b3c4c156 |
A rule for the bug that formatted a disk
falls-into-subroutine. The code above a label ends without going anywhere and the label is one something CALLs, so execution walks into the subroutine, reaches its RET, and returns to whatever the Stack happens to hold - because nobody called, there is no caller, and it goes somewhere nobody named. It is worth a rule because the symptom is nowhere near the cause and changes with the Stack. In CosmOS's monitor it was usually a byte that does not decode, in the middle of newLine; once it was inside sbfsFormat, and the machine formatted the disk it had booted from. Two exemptions, and both had to exist or the rule would have reported well written code: A TAIL CALL IS THE SAME SHAPE AND IS FINE. Falling out of one subroutine into another means the RET returns to the outer caller, which is real. So it only fires when nothing since the last branch or return was a call target either - which is the linter's usual trade of precision for being worth reading. AND osExit NEVER RETURNS. It is how a loaded program gives the machine back, and every program here ends with it and then writes its helpers underneath. Without that, twelve well written programs were reported. It is the one name from the system this tool knows, and the comment says why it is there. Also SRET, which stopsFallthrough did not list. It returns from a handler exactly as RET returns from a call, and leaving it out is a gap in every rule that asks what reaches an instruction. Load bearing rather than tidy: without it cosmos.asm reports a handler ending in SRET as falling into the routine written under it. A first pass over the file collects call targets, because a subroutine is very often called from further down than it is written. The corpus reports none of it, which is the point rather than a disappointment, and the Test Manual now says so - a baseline entry that is absent is otherwise indistinguishable from a rule that never runs. Checked against the version of cosmos.asm from before the fix, where it names the line. 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 |
||
|
|
000a6d39cb |
Somewhere to send the fault about there being nowhere to send it
Dispatching through a vector with nothing in it was the one fault this machine could not hand over, because the thing that would hand it over is the thing that has just found nothing to hand it to. It stopped the machine and no program could do anything about it - so calling a service the system does not implement was fatal, and that is an ordinary mistake to make. Two new fault vectors: 5 when a software vector was empty, 6 when a device interrupted and its hardware entry was. Separate, because they are separate mistakes with separate fixes - one is a program calling something that is not there, the other a program that asked to be interrupted and forgot the handler. WHICH ENTRY WAS EMPTY ARRIVES IN Q, and it is the only thing on this machine a handler is given in a register. Not a fault cause register by another route: the vector still says what happened and Q says which of the 256 entries it happened about, which is a parameter and not a cause. It costs no new state at all, because the frame already saved the Q the interrupted program had and RETI puts it back. The escalation happens once. If vector 5 or 6 is itself empty the machine stops the way it always did, having genuinely run out of places to go. swiFaultTest is what guards that, and it was written long before any of this: it installs nothing, so it must still get the old halt. Breaking the escalation fails the two new tests and not that one; making the escalation unbounded fails that one and not the two new ones. Each break fails exactly the half it belongs to. noDeviceTest is fed no input on purpose. The console raises its line once when input ENDS as well as when a byte arrives - which exists so a program driven by interrupts is told when nothing more is coming - so with no input at all, that end is what turns up. Groundwork for CosmOS's fault screen, which wanted to catch these two and could not. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW |
||
|
|
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 |
||
|
|
373454ec00 |
A fresh disk for every test, fixtures included
A fixture built by makedisks.sh was handed to each test where it lay. Twenty four tests name disks/cosmos.img and several of them write to one, so a test could hand the next one a disk with its leavings on. romBoot is what found it. Its recorded output described a directory that selfBoot had made earlier in the same run, so it passed in a full run and failed on its own - which is the worst way round for a test to be wrong, because the form nobody runs is the one telling the truth. Its recording now says "made" like selfBoot's, which is what running the same input on the same disk should always have said. Fixed as a class rather than as an instance: run.sh copies a fixture before attaching it, the same way it already removed a scratch image. Then every one of the 138 run and rom tests was run on its own to see whether anything else was leaning on what ran before it. Nothing was, before or after. Also, cosmosEditKeys.in was written by Python's write_text, which encodes as UTF-8, so every key byte was 0xC2 and then the key. The test passed anyway, because the shell ignores a byte it has no use for - a fixture working for a reason it was not built on, which is exactly the thing that stops working without anybody touching it. Written as bytes now; the recording is unchanged, which is the proof the stray bytes were being ignored. docs.sh is what caught that, and it turns out to draw the line in the right place by construction: a deliberately binary fixture does not decode as UTF-8 and is skipped, while one that is accidentally UTF-8 decodes and is reported. 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 |