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
This commit is contained in:
Anachronaut
2026-09-02 11:08:09 -04:00
co-authored by Claude Opus 5
parent 023362b05a
commit eee95ef0ce
15 changed files with 143 additions and 72 deletions
+43 -40
View File
@@ -28,18 +28,11 @@
#Base 0x5000 #Base 0x5000
start: start:
; ---- The two banks this touches ---- ; ---- The one bank this touches ----
; ;
; Four is the atlas, where the tiles are. Six is the other screen: three is the disk's and ; Six: three is the disk's and four and five are the ones the system registers to save a
; four and five are the ones the system registers to save a screen with, so six is the ; screen with, so six is the first number free. Nothing hands these out - see the table in
; first number free. Nothing hands these out - see the table in the CosmOS README. ; the CosmOS README.
INIA 0d4
OUTA 0xE3
INIA 0x30
OUTA 0xE2
INIA 0x03
OUTA 0xE8
INIA 0d6 INIA 0d6
OUTA 0xE3 OUTA 0xE3
INIA 0x3B INIA 0x3B
@@ -47,50 +40,57 @@ start:
INIA 0x03 INIA 0x03
OUTA 0xE8 OUTA 0xE8
; The screen back afterwards, because the tile overwritten below is one the shell spells ; ---- And it does NOT take the screen ----
; with. Refused is survivable and is not checked: there is nothing this could do about it, ;
; and the system puts the font back at exit whether or not it saved anything. ; osTakeScreen saves what is on the screen and puts it back at exit, and this program
SWI osTakeScreen ; called it at first. That was wrong in a way worth keeping written down: it saves the
; screen AS IT WAS BEFORE, so the line printed below - the line whose whole job is to still
; be there afterwards - was wiped out by the restore that was meant to be tidy.
;
; It was not needed either. NOTHING THIS TOUCHES IS THE SHELL'S: the only memory it writes
; is the map of the screen nobody is looking at, and the only register it leaves changed is
; which screen that is - which the system puts back itself.
;
; A program that wants a screen saved should ask. A program that damages nothing should
; not, and asking anyway is not free - it costs the screen it was standing on.
SETD.0 Message SETD.0 Message
SWI osPrintString SWI osPrintString
INIA 0x0A
OUTA 0x00 ; The assembler has no escapes; a newline is a byte.
; Key mode, so a key arrives when it is pressed rather than when Return is. ; Key mode, so a key arrives when it is pressed rather than when Return is.
INIA 0x01 INIA 0x01
OUTA 0x02 OUTA 0x02
; ---- Tile one, made solid ---- ; ---- Every cell of the other screen, in one command ----
; ;
; Sixty four pixels of index one, FILLED rather than blitted, which is why this program ; TEN, and the reason is worth the paragraph. Fill writes one byte, and a cell is two, so
; carries no picture of its own. ; whatever is filled with is BOTH the tile and the attribute - there is no filling a map
INIA 0d4 ; with a tile and a colour that are different numbers.
OUTA 0xE3
RSTA
OUTA 0xE4
INIA 0x40
OUTA 0xE5 ; Tile one begins at sixty four.
INIA 0x01
OUTA 0xE2 ; Fill takes the byte it writes from SourceLow.
RSTA
OUTA 0xE6
INIA 0x40
OUTA 0xE7 ; Sixty four bytes of it.
INIA 0x02
OUTA 0xE8
; ---- And every cell of the other screen ----
; ;
; One byte fills both halves of a cell, so this is tile one and attribute one everywhere: ; Ten as a tile is an asterisk, because the font begins at the space and glyph n is
; the tile is index one at every pixel and the attribute adds sixteen to all of them, which ; character n plus thirty two. Ten as an attribute is one of the eight REVERSED schemes,
; comes out as a screen solidly in scheme one's ink. ; whose paper is a colour and whose ink is black. So the screen comes out green, covered in
; black asterisks, without a single tile being redefined.
;
; The high nibble stays at nought, which the manual asks for: it is reserved. That is the
; real constraint on which byte this can be - it has to be under sixteen to leave the
; nibble alone, and eight or over to land on a reversed scheme whose PAPER is the colour.
;
; Which means THIS PROGRAM WRITES NOTHING BUT THE SCREEN NOBODY IS LOOKING AT. It was
; drawn with a tile of its own first, and that tile was one the system copies the font back
; over at every exit - so the screen it had filled turned blank the moment it left, and a
; check that it had been put back could not tell a restored screen from an abandoned one.
; A picture that depends on the atlas does not survive leaving.
INIA 0d6 INIA 0d6
OUTA 0xE3 OUTA 0xE3
INIA 0x40 INIA 0x40
OUTA 0xE4 OUTA 0xE4
RSTA RSTA
OUTA 0xE5 OUTA 0xE5
INIA 0x01 INIA 0x0A
OUTA 0xE2 OUTA 0xE2 ; Fill takes the byte it writes from SourceLow.
INIA 0x80 INIA 0x80
OUTA 0xE6 OUTA 0xE6
RSTA RSTA
@@ -141,5 +141,8 @@ waitKey:
#Base 0x3000 #Base 0x3000
; One line and no newline in it. A string literal is terminated where it ends, so a second
; one after a 0x0A byte would never be reached - printing stops at the first terminator. The
; newline is written to the console directly instead.
Message: Message:
"A whole screen is about to be drawn where you cannot see it - this line will still\nbe here when it is done. Press a key to show it, and another to come back.\n" "A screen is drawn where you cannot see it. A key shows it, another comes back."
+1 -1
View File
@@ -725,7 +725,7 @@ from every assembly file in it. Several are old programs written for the bare ma
| Files | Writes a file, reads it back, renames it and deletes it, in 675 bytes, including nothing but the service names. It is what says a program does not need a filesystem inside it. | | Files | Writes a file, reads it back, renames it and deletes it, in 675 bytes, including nothing but the service names. It is what says a program does not need a filesystem inside it. |
| Break | Stops itself twice with SWI osBreak, so that the registers can be seen changing between one stop and the next. | | Break | Stops itself twice with SWI osBreak, so that the registers can be seen changing between one stop and the next. |
| Grid | The first program to use the screen as a screen. Redefines a tile above the font, fills all 128 map rows, and scrolls it diagonally a pixel at a time. | | Grid | The first program to use the screen as a screen. Redefines a tile above the font, fills all 128 map rows, and scrolls it diagonally a pixel at a time. |
| Flip | Draws a whole screen into the bank that is not being shown, waits, and then shows it in one byte out of one port. It deliberately does not put the displayed screen back, because that is the system's to restore - a program that faulted while flipped could not have. | | Flip | Draws a whole screen into the bank that is not being shown, waits, and then shows it in one byte out of one port. It writes nothing else at all - not a tile, not a colour - so it does not ask for the screen to be saved, and the line it printed is still there when it comes back. It deliberately does not put the displayed screen back either, because that is the system's to restore: a program that faulted while flipped could not have. |
| Edit | A line editor. | | Edit | A line editor. |
| Stream | Reads an 84,000 byte file through a buffer of 256, which is what says a file bigger than Data Memory can be read at all. | | Stream | Reads an 84,000 byte file through a buffer of 256, which is what says a file bigger than Data Memory can be read at all. |
| Type | Prints a named text file a block at a time, including one too large to fit in Data Memory. | | Type | Prints a named text file a block at a time, including one too large to fit in Data Memory. |
+1 -1
View File
@@ -79,7 +79,7 @@ from `make`, not from here.
### 1. Recorded output ### 1. Recorded output
`Tests/run.sh` assembles each program named in `Tests/manifest`, runs it, and compares `Tests/run.sh` assembles each program named in `Tests/manifest`, runs it, and compares
everything it printed against a file in `Tests/expected`. 203 tests, of which 141 run, 35 everything it printed against a file in `Tests/expected`. 204 tests, of which 142 run, 35
only assemble, 16 are expected to fail to assemble, and 11 boot from ROM with no image only assemble, 16 are expected to fail to assemble, and 11 boot from ROM with no image
given at all. given at all.
+1 -1
View File
@@ -22,7 +22,7 @@ Break.sbx 149
Grid.sbx 571 Grid.sbx 571
Press.sbx 872 Press.sbx 872
Mode.sbx 48 Mode.sbx 48
Flip.sbx 291 Flip.sbx 173
Crash.sbx 632 Crash.sbx 632
vars.script 50 vars.script 50
blocks.script 343 blocks.script 343
+1 -1
View File
@@ -12,7 +12,7 @@ Break.sbx 149
Grid.sbx 571 Grid.sbx 571
Press.sbx 872 Press.sbx 872
Mode.sbx 48 Mode.sbx 48
Flip.sbx 291 Flip.sbx 173
Crash.sbx 632 Crash.sbx 632
vars.script 50 vars.script 50
blocks.script 343 blocks.script 343
+1 -1
View File
@@ -29,7 +29,7 @@ Break.sbx 149
Grid.sbx 571 Grid.sbx 571
Press.sbx 872 Press.sbx 872
Mode.sbx 48 Mode.sbx 48
Flip.sbx 291 Flip.sbx 173
Crash.sbx 632 Crash.sbx 632
vars.script 50 vars.script 50
blocks.script 343 blocks.script 343
+42
View File
@@ -0,0 +1,42 @@
CosmOS
> Say before Flip
it says: before Flip
finished
> Flip
A screen is drawn where you cannot see it. A key shows it, another comes back.
finished
> Say after Flip
it says: after Flip
finished
> dir
greet.sbx 211
hello.sbx 53
Life.sbx 1396
Snake.sbx 2164
Keys.sbx 664
Say.sbx 156
Break.sbx 149
Grid.sbx 571
Press.sbx 872
Mode.sbx 48
Flip.sbx 173
Crash.sbx 632
vars.script 50
blocks.script 343
loops.script 272
tune.sbx 306
notes.txt 21
Apps <dir>
hi.script 121
bad.script 45
plain.script 24
cross.script 280
nonl.script 38
outer.script 376
inner.script 44
loop.script 35
25 files, 1 directory
> exit
halted
Execution halted.
[exit 0]
+1 -1
View File
@@ -19,7 +19,7 @@ Break.sbx 149
Grid.sbx 571 Grid.sbx 571
Press.sbx 872 Press.sbx 872
Mode.sbx 48 Mode.sbx 48
Flip.sbx 291 Flip.sbx 173
Crash.sbx 632 Crash.sbx 632
vars.script 50 vars.script 50
blocks.script 343 blocks.script 343
+1 -1
View File
@@ -105,7 +105,7 @@ Break.sbx 149
Grid.sbx 571 Grid.sbx 571
Press.sbx 872 Press.sbx 872
Mode.sbx 48 Mode.sbx 48
Flip.sbx 291 Flip.sbx 173
Crash.sbx 632 Crash.sbx 632
vars.script 50 vars.script 50
blocks.script 343 blocks.script 343
+1 -1
View File
@@ -24,7 +24,7 @@ Break.sbx 149
Grid.sbx 571 Grid.sbx 571
Press.sbx 872 Press.sbx 872
Mode.sbx 48 Mode.sbx 48
Flip.sbx 291 Flip.sbx 173
Crash.sbx 632 Crash.sbx 632
vars.script 50 vars.script 50
blocks.script 343 blocks.script 343
+1 -1
View File
@@ -12,7 +12,7 @@ Break.sbx 149
Grid.sbx 571 Grid.sbx 571
Press.sbx 872 Press.sbx 872
Mode.sbx 48 Mode.sbx 48
Flip.sbx 291 Flip.sbx 173
Crash.sbx 632 Crash.sbx 632
vars.script 50 vars.script 50
blocks.script 343 blocks.script 343
+1 -1
View File
@@ -10,7 +10,7 @@ Break.sbx 149
Grid.sbx 571 Grid.sbx 571
Press.sbx 872 Press.sbx 872
Mode.sbx 48 Mode.sbx 48
Flip.sbx 291 Flip.sbx 173
Crash.sbx 632 Crash.sbx 632
vars.script 50 vars.script 50
blocks.script 343 blocks.script 343
+5
View File
@@ -0,0 +1,5 @@
Say before Flip
Flip
abSay after Flip
dir
exit
+9
View File
@@ -968,6 +968,15 @@ cosmosStartupBad | CosmOS/Source/cosmos.asm | run | cosmosSta
# So this runs a program by name, then Grid, then the same program again. The second one is # So this runs a program by name, then Grid, then the same program again. The second one is
# the check. dir at the end says the disk is still there to be read. # the check. dir at the end says the disk is still there to be read.
cosmosGrid | CosmOS/Source/cosmos.asm | run | cosmosGrid.in | 60000000 | disks/cosmos.img cosmosGrid | CosmOS/Source/cosmos.asm | run | cosmosGrid.in | 60000000 | disks/cosmos.img
# ---- The screen nobody is looking at ----
#
# Flip draws a whole screen into the bank that is not shown. What is checked here is not the
# picture - video.sh has that - but the LINE IT PRINTS, which is the thing the program claims
# will still be there when it comes back. It was not, twice over: the assembler has no string
# escapes, so a "\n" written in a literal printed as two characters; and the program called
# osTakeScreen, which restores the screen as it was BEFORE the line was printed and so wiped
# out the one thing it was pointing at. A program that damages nothing should not ask.
cosmosFlip | CosmOS/Source/cosmos.asm | run | cosmosFlip.in | 60000000 | disks/cosmos.img
# Which disk the registers mean. Several disks are one controller with a drive register # Which disk the registers mean. Several disks are one controller with a drive register
# rather than several devices, because a port is an immediate byte inside the instruction # rather than several devices, because a port is an immediate byte inside the instruction
# that names it and a program cannot compute one. Run with a single disk, so drive 1 is a # that names it and a program cannot compute one. Run with a single disk, so drive 1 is a
+34 -22
View File
@@ -85,6 +85,21 @@ ASM
# bitmap pixel 5 are both address 0x0005. So every write below names the memory it is for, # bitmap pixel 5 are both address 0x0005. So every write below names the memory it is for,
# and there is deliberately no bare poke that picks by address - a helper that guessed would # and there is deliberately no bare poke that picks by address - a helper that guessed would
# be right for the tiles and wrong for a picture, silently. # be right for the tiles and wrong for a picture, silently.
# The colour most of a picture is made of. Asked this way rather than by naming a pixel,
# because a filled screen is a PATTERN - the font has one blank glyph and it is the space,
# whose attribute nibble is nought - and which pixel lands on paper depends on the shape of
# whichever character was filled with.
commonest() {
# commonest <ppm>
python3 -c "
import collections, sys
d = open(sys.argv[1], 'rb').read()
px = d[d.index(b'255\n') + 4:]
counts = collections.Counter(px[o:o + 3] for o in range(0, len(px), 3))
print(counts.most_common(1)[0][0].hex())
" "$1" 2>/dev/null || echo none
}
pokeTo() { pokeTo() {
# pokeTo <bank> <address> <byte> # pokeTo <bank> <address> <byte>
printf ' INIA 0d%d\n OUTA 0xE3\n INIA 0x%02X\n OUTA 0xE4\n INIA 0x%02X\n OUTA 0xE5\n INIA 0x%02X\n OUTA 0xE9\n' \ printf ' INIA 0d%d\n OUTA 0xE3\n INIA 0x%02X\n OUTA 0xE4\n INIA 0x%02X\n OUTA 0xE5\n INIA 0x%02X\n OUTA 0xE9\n' \
@@ -1014,33 +1029,30 @@ python3 -c "open('$BUILD/flip.keys','wb').write(b'Flip\n' + b'\x00'*3000 + b' '
timeout 30 "$EMU" --fast --cycles 200000000 --keyboard "$BUILD/flip.keys" \ timeout 30 "$EMU" --fast --cycles 200000000 --keyboard "$BUILD/flip.keys" \
--screen "$BUILD/flip.ppm" --disk "$ROOT/Tests/build/disks/cosmos.img" \ --screen "$BUILD/flip.ppm" --disk "$ROOT/Tests/build/disks/cosmos.img" \
--ram-disk 2048 "$BUILD/cosmos.bin" > "$BUILD/flip.out" 2>&1 || true --ram-disk 2048 "$BUILD/cosmos.bin" > "$BUILD/flip.out" 2>&1 || true
FLIPPED="$(python3 -c " FLIPPED="$(commonest "$BUILD/flip.ppm")"
d = open('$BUILD/flip.ppm', 'rb').read() [ "$FLIPPED" = "50c050" ] \
px = d[d.index(b'255\n') + 4:] && result ok "a program shows the other screen" "green, which is the paper it filled with" \
print(len({px[o:o + 3] for o in range(0, len(px), 3)})) || result no "a program shows the other screen" "commonest colour $FLIPPED, not the fill"
" 2>/dev/null || echo 0)"
[ "$FLIPPED" = "1" ] \
&& result ok "a program shows the other screen" "the whole picture is the one it filled" \
|| result no "a program shows the other screen" "$FLIPPED colours, not a filled screen"
# ---- And the system takes it back ---- # ---- And the system takes it back ----
# #
# The shell's scrollback, its prompt and every line the person typed are in screen NOUGHT. # The shell's scrollback, its prompt and every line the person typed are in screen NOUGHT.
# A program that exited while showing screen one would hand back a shell drawing perfectly # A program that exited while showing screen one would hand back a shell drawing perfectly
# onto a screen nobody had ever written to - blank, and looking for all the world like the # onto a screen nobody had ever written to, and Flip does exit while flipped - deliberately,
# machine had lost everything. Compared cell by cell against the same session without Flip # because a program that FAULTED while flipped could not put it back either.
# in it, which is the same way Grid's restore is checked. #
python3 -c "open('$BUILD/flipbefore.keys','wb').write(b'dir\n' + b'Say a line to come back to\n' + b'\x00'*200)" # What says so is a corner of the screen with nothing on it. The map Flip filled is one tile
python3 -c "open('$BUILD/flipafter.keys','wb').write(b'dir\n' + b'Say a line to come back to\n' + b'Flip\n' + b'\x00'*3000 + b' ' + b'\x00'*3000 + b' ' + b'\x00'*3000)" # and one attribute in every cell, so a screen still showing it is that colour EVERYWHERE; a
for phase in flipbefore flipafter; do # screen nought that came back is black where nobody has printed. Checking a corner rather
timeout 30 "$EMU" --fast --cycles 200000000 --keyboard "$BUILD/$phase.keys" \ # than comparing whole pictures, because Flip's own line is meant to survive on this one and
--screen "$BUILD/$phase.ppm" --disk "$ROOT/Tests/build/disks/cosmos.img" \ # an equality check would call that a difference.
--ram-disk 2048 "$BUILD/cosmos.bin" > "$BUILD/$phase.out" 2>&1 || true python3 -c "open('$BUILD/flipafter.keys','wb').write(b'Say a line to come back to\n' + b'Flip\n' + b'\x00'*3000 + b' ' + b'\x00'*3000 + b' ' + b'\x00'*3000)"
done timeout 30 "$EMU" --fast --cycles 200000000 --keyboard "$BUILD/flipafter.keys" \
BACK="$(python3 "$ROOT/Tests/samescreen.py" "$BUILD/flipbefore.ppm" "$BUILD/flipafter.ppm")" --screen "$BUILD/flipafter.ppm" --disk "$ROOT/Tests/build/disks/cosmos.img" \
[ "$BACK" = "yes" ] \ --ram-disk 2048 "$BUILD/cosmos.bin" > "$BUILD/flipafter.out" 2>&1 || true
&& result ok "and the system puts the screen back" "screen nought, with what was on it" \ [ "$(commonest "$BUILD/flipafter.ppm")" = "000000" ] \
|| result no "and the system puts the screen back" "$BACK" && result ok "and the system puts the screen back" "black again, and not the filled screen" \
|| result no "and the system puts the screen back" "commonest colour $(commonest "$BUILD/flipafter.ppm")"
# ---- Clearing puts the cursor back at the top ---- # ---- Clearing puts the cursor back at the top ----
# #