Replace the escape parser with cursor registers

The console had grown an ANSI parser, and that was the wrong shape. ANSI exists because a
screen used to be on the other end of a serial line and a byte stream was the only channel
there was. This screen is memory the program can already address, so reaching it by sending
characters for a state machine to take apart is a middleman for something the machine does
better - and it meant accepting an open protocol somebody else defines, in hardware, with no
natural end to it. Everything else on this machine is registers.

So the console gets three: cursor row at 0x03, cursor column at 0x04, and a command port at
0x05 where 1 clears the screen. Both cursor registers are READ as well as written, which is
the thing an escape cannot do without sending a query and parsing a reply - a routine that
wants to put the cursor back where it found it can now ask.

Clearing is one command against a thousand cells walked one at a time. Snake and Life are
smaller for it: 2,168 bytes to 2,163 and 1,410 to 1,396.

A HOST TERMINAL STILL SPEAKS ANSI, and bridging to the host is the emulator's job, the same
job it does reading standard input. So the escapes are now GENERATED, outbound, for the set
this device chooses, rather than parsed inbound as though the machine were a terminal. The
set cannot grow behind our backs because we are the ones saying it. The cursor is announced
lazily, at the next character rather than at the register write, so setting a row and a
column costs one sequence rather than two.

The console's block widens from three ports to six, which registryTest noticed: it had been
asking about port 0x05 precisely BECAUSE nothing was there, and the console had just moved
in. Re-blessing it would have left it checking nothing, so it asks about 0x80 instead -
clear of the console, the disk, the screen, the controller, and the sound device coming to
0x40.

Six checks in Tests/video.sh swapped from the sequences to the registers, including that the
cursor reads back and that one sent past the edge is clamped rather than refusing. Those
checks also stopped counting bytes from the ends of a file, which had quietly started
measuring an escape the moment the console began announcing the cursor.

SplitLint caught the one thing worth catching in the port: the clear command leaves A at 1
and key mode is also 1, so the second load looks redundant. Acting on it would tie a console
command to a console mode by coincidence, and break silently if either ever moved, so it is
suppressed with that reason rather than removed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01E2JrLzFvuFX9fgi1LDRjrW
This commit is contained in:
Anachronaut
2026-08-28 23:08:52 -04:00
co-authored by Claude Opus 5
parent bdb2d0d8e6
commit 43a05b3df1
17 changed files with 316 additions and 294 deletions
+5 -10
View File
@@ -36,8 +36,8 @@
start: start:
CALL seedGlider CALL seedGlider
SETD.0 ClearScreen INIA 0x01
CALL printString OUTA 0x05 ; Console command: clear the screen
SETD.3 GenerationsLeft SETD.3 GenerationsLeft
INIA 0xFF INIA 0xFF
STA.3 STA.3
@@ -126,8 +126,9 @@ seedGlider:
RET RET
renderBoard: renderBoard:
SETD.0 CursorHome RSTA
CALL printString OUTA 0x03
OUTA 0x04 ; Cursor to row 0, column 0
SETD.1 RowCount SETD.1 RowCount
SETD.2 ColCount SETD.2 ColCount
INIA 0d16 INIA 0d16
@@ -371,12 +372,6 @@ RanOutText:
StoppedText: StoppedText:
"stopped" "stopped"
ClearScreen:
0x1B
"[2J"
CursorHome:
0x1B
"[H"
; 18 by 18 cells with the current and next states interleaved, so 648 bytes. The ; 18 by 18 cells with the current and next states interleaved, so 648 bytes. The
; original leaves this implicit and leans on Data Memory being zero, which works but ; original leaves this implicit and leans on Data Memory being zero, which works but
+10 -11
View File
@@ -60,13 +60,17 @@ start:
CALL placeSnake CALL placeSnake
CALL placeFood CALL placeFood
SETD.0 ClearScreen INIA 0x01
CALL printString OUTA 0x05 ; Console command: clear the screen
; Key mode, so that one key is one byte and arrives when it is pressed. It is put back ; Key mode, so that one key is one byte and arrives when it is pressed. It is put back
; before this returns, and CosmOS puts it back too in case a program stops without ; before this returns, and CosmOS puts it back too in case a program stops without
; doing so. ; doing so.
INIA 0x01 ;
; A is already 1 from the clear above, and leaving this out would save a byte by tying a
; console COMMAND to a console MODE that happens to share a number. That is a coincidence
; rather than a saving, and it would break silently if either ever moved.
INIA 0x01 ; splitlint[redundant-assignment]: see above
OUTA 0x02 OUTA 0x02
gameLoop: gameLoop:
@@ -529,8 +533,9 @@ removeTail:
; The whole board, every frame, from the top left corner. Sixteen by sixteen is small ; The whole board, every frame, from the top left corner. Sixteen by sixteen is small
; enough that working out what changed would cost more than sending it all again. ; enough that working out what changed would cost more than sending it all again.
draw: draw:
SETD.0 CursorHome RSTA
CALL printString OUTA 0x03
OUTA 0x04 ; Cursor to row 0, column 0
SETD.0 BorderText SETD.0 BorderText
CALL printString CALL printString
CALL newLine CALL newLine
@@ -642,12 +647,6 @@ RandomSeed:
RandomState: RandomState:
0x00 0x00 0x00 0x00
ClearScreen:
0x1B
"[2J"
CursorHome:
0x1B
"[H"
BorderText: BorderText:
"+----------------+" "+----------------+"
+5 -10
View File
@@ -14,8 +14,8 @@
start: start:
CALL seedGlider CALL seedGlider
SETD ClearScreen INIA 0x01
CALL printString OUTA 0x05 ; Console command: clear the screen
generationLoop: generationLoop:
CALL renderBoard CALL renderBoard
@@ -48,8 +48,9 @@ seedGlider:
RET RET
renderBoard: renderBoard:
SETD CursorHome RSTA
CALL printString OUTA 0x03
OUTA 0x04 ; Cursor to row 0, column 0
SETD Board SETD Board
DPUP 0d38 DPUP 0d38
INIA 0d16 INIA 0d16
@@ -306,12 +307,6 @@ RowCount:
ColCount: ColCount:
0x00 0x00
ClearScreen:
0x1B
"[2J"
CursorHome:
0x1B
"[H"
; The emulator zero-fills the remainder of Data Memory. Board names the first ; The emulator zero-fills the remainder of Data Memory. Board names the first
; byte of a 648-byte logical allocation (18 * 18 * 2). ; byte of a 648-byte logical allocation (18 * 18 * 2).
@@ -10,8 +10,8 @@
start: start:
CALL seedGlider CALL seedGlider
SETD.0 ClearScreen INIA 0x01
CALL printString OUTA 0x05 ; Console command: clear the screen
generationLoop: generationLoop:
CALL renderBoard CALL renderBoard
@@ -38,8 +38,9 @@ seedGlider:
RET RET
renderBoard: renderBoard:
SETD.0 CursorHome RSTA
CALL printString OUTA 0x03
OUTA 0x04 ; Cursor to row 0, column 0
SETD.1 RowCount SETD.1 RowCount
SETD.2 ColCount SETD.2 ColCount
INIA 0d16 INIA 0d16
@@ -250,12 +251,6 @@ RowCount:
ColCount: ColCount:
0x00 0x00
ClearScreen:
0x1B
"[2J"
CursorHome:
0x1B
"[H"
; 18 by 18 cells with the current and next states interleaved, so 648 bytes. The ; 18 by 18 cells with the current and next states interleaved, so 648 bytes. The
; original leaves this implicit and leans on Data Memory being zero, which works but ; original leaves this implicit and leans on Data Memory being zero, which works but
+7 -2
View File
@@ -6,10 +6,15 @@
; about, which is the whole point: reading port 0x00 to find out what it is would take ; about, which is the whole point: reading port 0x00 to find out what it is would take
; a character off standard input and wait for one that never comes. ; a character off standard input and wait for one that never comes.
; ;
; Port 0x05 has nothing on it, and reads as class 0. That is the same answer a machine ; Port 0x80 has nothing on it, and reads as class 0. That is the same answer a machine
; with no registry at all would give, so software finds out whether it can enumerate by ; with no registry at all would give, so software finds out whether it can enumerate by
; enumerating. ; enumerating.
; ;
; It used to ask about 0x05, until the console grew cursor registers and took it. The empty
; port has to be one nothing is likely to want: 0x80 is clear of the console below it, the
; disk and the screen, the memory controller at the top, and the sound device that is coming
; to 0x40.
;
; Correct output is: ; Correct output is:
; 00 02 00 the console, class 2, no flags ; 00 02 00 the console, class 2, no flags
; 10 10 00 the test device, class 0x10, no flags ; 10 10 00 the test device, class 0x10, no flags
@@ -27,7 +32,7 @@ start:
CALL reportPort CALL reportPort
INIA 0xFF INIA 0xFF
CALL reportPort CALL reportPort
INIA 0x05 INIA 0x80
CALL reportPort CALL reportPort
HALT HALT
+65 -77
View File
@@ -232,23 +232,9 @@ static void consoleNewLine(void) {
} }
} }
// ---- The sequences this machine already speaks ---- // Clears the screen, or from the cursor to the end of it. Reached through the console's
// // Command port rather than through an escape sequence: this machine talks to its devices in
// Every program in the corpus that moves a cursor does it with ANSI escapes, because until // registers, and a screen it can address directly needs no protocol to reach it.
// now the thing on the other end was somebody's terminal. A display controller that did not
// understand them would draw "[2J" on the screen and leave the board underneath it, which is
// exactly what happened the first time Snake was run in a window.
//
// So the controller parses them, the way a video terminal did - that is what a VT100 was.
// The whole corpus uses two, ESC[2J and ESC[H, and the general shape is recognised so that
// anything else is SWALLOWED RATHER THAN DRAWN: a sequence nobody implemented should leave
// no marks, which is what a real terminal does with one it does not know.
#define SEQUENCE_PARAMS 2
static enum { DRAW_TEXT, DRAW_SAW_ESCAPE, DRAW_IN_SEQUENCE } drawState = DRAW_TEXT;
static int sequenceParam[SEQUENCE_PARAMS];
static int sequenceParams;
static void consoleClearScreen(int fromCursor) { static void consoleClearScreen(int fromCursor) {
const int rows = videoRows(); const int rows = videoRows();
const int columns = videoColumns(); const int columns = videoColumns();
@@ -260,72 +246,45 @@ static void consoleClearScreen(int fromCursor) {
} }
} }
// Returns 1 if the byte was part of a sequence and so is not a character to draw. // ---- Driving a terminal on the other end of the serial line ----
static int consoleSequence(uint8_t byte) { //
switch (drawState) { // The machine speaks registers. A HOST TERMINAL SPEAKS ANSI, and bridging to the host is the
case DRAW_TEXT: // emulator's job - the same job it does reading standard input. So the escapes are GENERATED
if (byte != 0x1B) { // here, outbound, for the set this device chooses, rather than parsed inbound as though the
return 0; // machine were a terminal itself.
//
// That is the whole difference in shape. Parsing means accepting an open protocol somebody
// else defines and putting a state machine in the hardware. Generating means one device
// knowing how to talk to one kind of host, in one direction, for exactly the things it can
// be asked to do - and the set cannot grow behind our backs, because we are the ones saying
// it.
static void consoleTellTerminal(const char *sequence) {
for (const char *at = sequence; *at != '\0'; at++) {
putchar(*at);
} }
drawState = DRAW_SAW_ESCAPE;
return 1;
case DRAW_SAW_ESCAPE:
// Only the bracket form. An escape followed by anything else is not a sequence
// this machine has ever sent, and swallowing the escape alone is enough.
drawState = DRAW_TEXT;
if (byte == '[') {
drawState = DRAW_IN_SEQUENCE;
sequenceParams = 0;
sequenceParam[0] = 0;
sequenceParam[1] = 0;
} }
return 1;
case DRAW_IN_SEQUENCE: // ---- Told once, and only when it matters ----
break; //
// A terminal cares where the cursor is at the moment something is about to be drawn there,
// not at the moment a register was written. Announcing on every register write sent two
// sequences for one move, because setting a row and a column is two writes. So a write only
// marks it, and the next character sends it.
static int cursorTold = 1;
static void consoleSayCursor(void) {
if (cursorTold) {
return;
} }
if (byte >= '0' && byte <= '9') { cursorTold = 1;
if (sequenceParams < SEQUENCE_PARAMS) { // Room for the widest this can be, and then some. The compiler cannot see that a cursor
sequenceParam[sequenceParams] = sequenceParam[sequenceParams] * 10 + (byte - '0'); // is bounded by the screen, and a warning about a buffer is not worth being clever over.
} char sequence[32];
return 1; snprintf(sequence, sizeof(sequence), "\033[%d;%dH", cursorRow + 1, cursorColumn + 1);
} consoleTellTerminal(sequence);
if (byte == ';') {
if (sequenceParams < SEQUENCE_PARAMS - 1) {
sequenceParams++;
}
return 1;
}
drawState = DRAW_TEXT;
switch (byte) {
case 'J':
// 2 is the whole screen, which is the one the corpus uses. Without a number it
// is from the cursor down, which is what the standard says and costs nothing.
consoleClearScreen(sequenceParam[0] != 2);
break;
case 'H': case 'f': {
// Row then column, one-based on the wire and zero-based here. Missing or zero
// means one, which is what makes a bare ESC[H the corner.
int row = sequenceParam[0];
int column = (sequenceParams >= 1) ? sequenceParam[1] : 0;
if (row < 1) row = 1;
if (column < 1) column = 1;
cursorRow = row - 1;
cursorColumn = column - 1;
if (cursorRow >= videoRows()) cursorRow = videoRows() - 1;
if (cursorColumn >= videoColumns()) cursorColumn = videoColumns() - 1;
}
break;
default:
// Recognised as a sequence and not implemented, so it leaves no marks.
break;
}
return 1;
} }
static void consoleDraw(uint8_t byte) { static void consoleDraw(uint8_t byte) {
if (consoleSequence(byte)) {
return;
}
switch (byte) { switch (byte) {
case '\n': case '\n':
consoleNewLine(); consoleNewLine();
@@ -868,10 +827,33 @@ uint8_t OutputHandler(uint8_t DataByte, uint8_t Address) {
case CONSOLE_DATA: case CONSOLE_DATA:
// Both, always. The screen because this machine has one, and standard output // Both, always. The screen because this machine has one, and standard output
// because the serial line is how everything that is not a person reads it. // because the serial line is how everything that is not a person reads it.
//
// The terminal is told where the cursor went first, if it has not been told
// since it was moved. Here rather than at the move, so setting a row and a
// column costs one sequence rather than two.
consoleSayCursor();
consoleDraw(DataByte); consoleDraw(DataByte);
putchar(DataByte); putchar(DataByte);
break; break;
case CONSOLE_CONTROL: consoleSetControl(DataByte); break; case CONSOLE_CONTROL: consoleSetControl(DataByte); break;
case CONSOLE_CURSOR_ROW:
// Clamped rather than refused. A cursor asked to go off the screen has an
// obvious place to be, and stopping the machine over one is a poor trade.
cursorRow = DataByte < videoRows() ? DataByte : videoRows() - 1;
cursorTold = 0;
break;
case CONSOLE_CURSOR_COLUMN:
cursorColumn = DataByte < videoColumns() ? DataByte : videoColumns() - 1;
cursorTold = 0;
break;
case CONSOLE_COMMAND:
if (DataByte == CONSOLE_COMMAND_CLEAR) {
consoleClearScreen(0);
consoleTellTerminal("\033[2J");
}
// Anything else does nothing. A command block reserved for later should be
// quiet rather than fatal, the same as the screen's spare registers.
break;
case CONSOLE_STATUS: case CONSOLE_STATUS:
// Read only. A device saying how it is does not take instructions through the // Read only. A device saying how it is does not take instructions through the
// same hole, so a write here is ignored rather than meaning something. // same hole, so a write here is ignored rather than meaning something.
@@ -937,6 +919,12 @@ uint8_t InputHandler(uint8_t Address) {
return consoleReadByte(); return consoleReadByte();
break; break;
case CONSOLE_STATUS: return consoleStatus(); case CONSOLE_STATUS: return consoleStatus();
case CONSOLE_CURSOR_ROW: return (uint8_t)cursorRow;
case CONSOLE_CURSOR_COLUMN: return (uint8_t)cursorColumn;
case CONSOLE_COMMAND:
// Write only. What it did is visible in the cursor and on the screen.
return 0;
break;
case CONSOLE_CONTROL: case CONSOLE_CONTROL:
// Write only. Reading it gives zero rather than what was last written, because // Write only. Reading it gives zero rather than what was last written, because
// everything it sets is reported by the status port and one fact wants one // everything it sets is reported by the status port and one fact wants one
+13 -1
View File
@@ -18,11 +18,23 @@
// does not change: writing sends a byte, reading takes one and waits for it. The other two // does not change: writing sends a byte, reading takes one and waits for it. The other two
// are additions, so a program written before they existed cannot notice them. // are additions, so a program written before they existed cannot notice them.
#define PORT_CONSOLE 0x00 #define PORT_CONSOLE 0x00
#define PORT_CONSOLE_TOP 0x02 #define PORT_CONSOLE_TOP 0x05
#define CONSOLE_DATA 0x00 #define CONSOLE_DATA 0x00
#define CONSOLE_STATUS 0x01 #define CONSOLE_STATUS 0x01
#define CONSOLE_CONTROL 0x02 #define CONSOLE_CONTROL 0x02
// ---- Where the cursor is, as registers ----
//
// Read as well as written, which is the thing an escape sequence cannot do without a query
// and a parse. A program that wants to put something back where it found it asks.
#define CONSOLE_CURSOR_ROW 0x03
#define CONSOLE_CURSOR_COLUMN 0x04
// Written, and it happens at once - the same shape as the memory controller's Command port
// rather than a bit in a register that otherwise holds state.
#define CONSOLE_COMMAND 0x05
#define CONSOLE_COMMAND_CLEAR 0x01
#define PORT_TEST 0x10 #define PORT_TEST 0x10
#define PORT_REFUSE 0x11 #define PORT_REFUSE 0x11
#define PORT_MEMORY 0x12 #define PORT_MEMORY 0x12
+15 -8
View File
@@ -506,7 +506,7 @@ If nothing is installed for the vector a device refused with, the machine stops
| Port | Device | Class | | Port | Device | Class |
| --- | --- | --- | | --- | --- | --- |
| 0x00 - 0x02 | The console. See The Console. Writing to 0x00 sends a byte to standard output, reading takes one from standard input. It interrupts on 0x00, its base port, when asked to. | 0x02 | | 0x00 - 0x05 | The console. See The Console. Writing to 0x00 sends a byte to standard output, reading takes one from standard input. It interrupts on 0x00, its base port, when asked to. | 0x02 |
| 0x10 | A test device. Writing anything to it puts its own line up, so that interrupt handling can be exercised without waiting on anything. The byte written is ignored. | 0x10 | | 0x10 | A test device. Writing anything to it puts its own line up, so that interrupt handling can be exercised without waiting on anything. The byte written is ignored. | 0x10 |
| 0x11 | A device that refuses everything, in both directions, so that refusal can be exercised without the memory controller. | 0x11 | | 0x11 | A device that refuses everything, in both directions, so that refusal can be exercised without the memory controller. | 0x11 |
| 0x20 - 0x23 | The disk. See Storage. It interrupts on 0x20, its base port. | 0x13 | | 0x20 - 0x23 | The disk. See Storage. It interrupts on 0x20, its base port. | 0x13 |
@@ -589,19 +589,26 @@ The font is in ASCII order, so a byte becomes a glyph by subtracting 32. Bytes b
Writing past the last column wraps to the next row, the same as a newline. Writing past the last column wraps to the next row, the same as a newline.
**And it understands the escape sequences this machine already sends.** Every program here that moves a cursor does it with ANSI escapes, because until there was a screen the thing on the other end was somebody's terminal. A controller that ignored them would draw `[2J` on the screen and leave the picture underneath it, so it parses them - which is what a video terminal did. ### Moving The Cursor:
| Sequence | Does | Three more registers, because that is how this machine talks to everything else.
| Port | Register |
| --- | --- | | --- | --- |
| `ESC [ 2 J` | Clears the whole screen. The cursor does not move. | | 0x03 | Cursor row. Read and write. |
| `ESC [ J` | Clears from the cursor to the end of the screen. | | 0x04 | Cursor column. Read and write. |
| `ESC [ H` | Puts the cursor in the corner. | | 0x05 | Command. Write 1 to clear the screen. |
| `ESC [ row ; column H` | Puts the cursor there, counting from one. |
Anything else in that shape - an escape, a bracket, some numbers, a letter - is recognised and **swallowed rather than drawn**. A sequence nobody implemented should leave no marks, which is what a terminal does with one it does not know, and drawing it would be worse than ignoring it. Both counted from zero, and both **readable**, which is the thing worth having: a routine that wants to put the cursor back where it found it asks where that was.
A cursor sent past the edge is clamped rather than refused. It has an obvious place to be, and stopping the machine over one would be a poor trade.
Clearing does not touch the scrollback. It clears what is on the screen, and what has already gone off the top is still in the map where the Scroll register can find it. Clearing does not touch the scrollback. It clears what is on the screen, and what has already gone off the top is still in the map where the Scroll register can find it.
**There is no escape sequence here, and there should not be.** ANSI exists because a screen used to be on the other end of a serial line and a byte stream was the only channel there was. This screen is memory the program can already address, and reaching it by sending characters for a parser to take apart is a middleman for something the machine does better - clearing by writing 1 to a port costs one command, against a thousand cells walked one at a time.
What a program on the other end of an actual serial line sees is a different question, and the answer is that the console sends it the escapes it needs. That is the emulator bridging to a host terminal, the same job it does reading standard input, and it is not part of this machine.
**Scrolling moves the video device's Scroll register and no memory at all.** The row that comes into view at the bottom is cleared, because the map is a ring and it is holding whatever was there 128 rows ago. The rows that go off the top are *not* cleared, and that is the point: a hundred rows of what has already been said are still in the map, so a machine has scrollback without anything having to keep it. **Scrolling moves the video device's Scroll register and no memory at all.** The row that comes into view at the bottom is cleared, because the map is a ring and it is holding whatever was there 128 rows ago. The rows that go off the top are *not* cleared, and that is the point: a hundred rows of what has already been said are still in the map, so a machine has scrollback without anything having to keep it.
**It is one screen.** A program that writes its own tiles and its own map has taken the screen, and a console still writing characters into it will scribble on what that program drew. This is not an oversight to be worked around - it is what one screen means, and it is why a program that wants the screen takes it. **It is one screen.** A program that writes its own tiles and its own map has taken the screen, and a console still writing characters into it will scribble on what that program drew. This is not an oversight to be worked around - it is what one screen means, and it is why a program that wants the screen takes it.
+29 -29
View File
@@ -1,4 +1,4 @@
 #  #
# #
### ###
@@ -14,7 +14,7 @@
 
# # # #
## ##
# #
@@ -30,7 +30,7 @@
 
# #
# # # #
## ##
@@ -46,7 +46,7 @@
 
# #
## ##
## ##
@@ -62,7 +62,7 @@
 
# #
# #
### ###
@@ -78,7 +78,7 @@
 
# # # #
## ##
@@ -94,7 +94,7 @@
 
# #
# # # #
@@ -110,7 +110,7 @@
 
# #
## ##
@@ -126,7 +126,7 @@
 
# #
# #
@@ -142,7 +142,7 @@
 
# # # #
@@ -158,7 +158,7 @@
 
# #
@@ -174,7 +174,7 @@
 
# #
@@ -190,7 +190,7 @@
 
# #
@@ -206,7 +206,7 @@
 
@@ -222,7 +222,7 @@
 
@@ -238,7 +238,7 @@
 
@@ -254,7 +254,7 @@
 
@@ -270,7 +270,7 @@
 
@@ -286,7 +286,7 @@
 
@@ -302,7 +302,7 @@
 
@@ -318,7 +318,7 @@
 
@@ -334,7 +334,7 @@
 
@@ -350,7 +350,7 @@
 
@@ -366,7 +366,7 @@
 
@@ -382,7 +382,7 @@
 
@@ -398,7 +398,7 @@
 
@@ -414,7 +414,7 @@
 
@@ -430,7 +430,7 @@
 
@@ -446,7 +446,7 @@
 
+30 -30
View File
@@ -1,4 +1,4 @@
 #  #
# #
### ###
@@ -14,7 +14,7 @@
 
# # # #
## ##
# #
@@ -30,7 +30,7 @@
 
# #
# # # #
## ##
@@ -46,7 +46,7 @@
 
# #
## ##
## ##
@@ -62,7 +62,7 @@
 
# #
# #
### ###
@@ -78,7 +78,7 @@
 
# # # #
## ##
@@ -94,7 +94,7 @@
 
# #
# # # #
@@ -110,7 +110,7 @@
 
# #
## ##
@@ -126,7 +126,7 @@
 
# #
# #
@@ -142,7 +142,7 @@
 
# # # #
@@ -158,7 +158,7 @@
 
# #
@@ -174,7 +174,7 @@
 
# #
@@ -190,7 +190,7 @@
 
# #
@@ -206,7 +206,7 @@
 
@@ -222,7 +222,7 @@
 
@@ -238,7 +238,7 @@
 
@@ -254,7 +254,7 @@
 
@@ -270,7 +270,7 @@
 
@@ -286,7 +286,7 @@
 
@@ -302,7 +302,7 @@
 
@@ -318,7 +318,7 @@
 
@@ -334,7 +334,7 @@
 
@@ -350,7 +350,7 @@
 
@@ -366,7 +366,7 @@
 
@@ -382,7 +382,7 @@
 
@@ -398,7 +398,7 @@
 
@@ -414,7 +414,7 @@
 
@@ -430,7 +430,7 @@
 
@@ -446,7 +446,7 @@
 
@@ -462,7 +462,7 @@
 
+54 -54
View File
@@ -1,6 +1,6 @@
CosmOS CosmOS
> loaded, starting at 4000 > loaded, starting at 4000
>  # >  #
# #
### ###
@@ -16,7 +16,7 @@ CosmOS
 
# # # #
## ##
# #
@@ -32,7 +32,7 @@ CosmOS
 
# #
# # # #
## ##
@@ -48,7 +48,7 @@ CosmOS
 
# #
## ##
## ##
@@ -64,7 +64,7 @@ CosmOS
 
# #
# #
### ###
@@ -80,7 +80,7 @@ CosmOS
 
# # # #
## ##
@@ -96,7 +96,7 @@ CosmOS
 
# #
# # # #
@@ -112,7 +112,7 @@ CosmOS
 
# #
## ##
@@ -128,7 +128,7 @@ CosmOS
 
# #
# #
@@ -144,7 +144,7 @@ CosmOS
 
# # # #
@@ -160,7 +160,7 @@ CosmOS
 
# #
@@ -176,7 +176,7 @@ CosmOS
 
# #
@@ -192,7 +192,7 @@ CosmOS
 
# #
@@ -208,7 +208,7 @@ CosmOS
 
@@ -224,7 +224,7 @@ CosmOS
 
@@ -240,7 +240,7 @@ CosmOS
 
@@ -256,7 +256,7 @@ CosmOS
 
@@ -272,7 +272,7 @@ CosmOS
 
@@ -288,7 +288,7 @@ CosmOS
 
@@ -304,7 +304,7 @@ CosmOS
 
@@ -320,7 +320,7 @@ CosmOS
 
@@ -336,7 +336,7 @@ CosmOS
 
@@ -352,7 +352,7 @@ CosmOS
 
@@ -368,7 +368,7 @@ CosmOS
 
@@ -384,7 +384,7 @@ CosmOS
 
@@ -400,7 +400,7 @@ CosmOS
 
@@ -416,7 +416,7 @@ CosmOS
 
@@ -432,7 +432,7 @@ CosmOS
 
@@ -448,7 +448,7 @@ CosmOS
 
@@ -464,7 +464,7 @@ CosmOS
 
@@ -480,7 +480,7 @@ CosmOS
 
@@ -496,7 +496,7 @@ CosmOS
 
@@ -512,7 +512,7 @@ CosmOS
 
@@ -528,7 +528,7 @@ CosmOS
 
@@ -544,7 +544,7 @@ CosmOS
 
@@ -560,7 +560,7 @@ CosmOS
 
@@ -576,7 +576,7 @@ CosmOS
 
@@ -592,7 +592,7 @@ CosmOS
 
@@ -608,7 +608,7 @@ CosmOS
 
@@ -624,7 +624,7 @@ CosmOS
 
@@ -640,7 +640,7 @@ CosmOS
 
@@ -656,7 +656,7 @@ CosmOS
 
@@ -672,7 +672,7 @@ CosmOS
# #
 
@@ -688,7 +688,7 @@ CosmOS
## ##
 
@@ -704,7 +704,7 @@ CosmOS
## ##
 
@@ -720,7 +720,7 @@ CosmOS
### ###
 
@@ -736,7 +736,7 @@ CosmOS
## ##
# #
 
@@ -752,7 +752,7 @@ CosmOS
# # # #
## ##
 
@@ -768,7 +768,7 @@ CosmOS
## ##
## ##
 
@@ -784,7 +784,7 @@ CosmOS
# #
### ###
 
@@ -800,7 +800,7 @@ CosmOS
# # # #
## ##
# #
 
@@ -816,7 +816,7 @@ CosmOS
# #
# # # #
## ##
 
@@ -832,7 +832,7 @@ CosmOS
# #
# #
## ##
 
@@ -848,7 +848,7 @@ CosmOS
# #
## ##
 
+1 -1
View File
@@ -1,6 +1,6 @@
CosmOS CosmOS
> loaded, starting at 4000 > loaded, starting at 4000
>  # >  #
# #
### ###
+2 -2
View File
@@ -2,8 +2,8 @@ CosmOS
> nothing is loaded > nothing is loaded
> greet.sbx 211 > greet.sbx 211
hello.sbx 53 hello.sbx 53
Life.sbx 1410 Life.sbx 1396
Snake.sbx 2168 Snake.sbx 2163
Keys.sbx 664 Keys.sbx 664
Say.sbx 156 Say.sbx 156
Break.sbx 149 Break.sbx 149
+2 -2
View File
@@ -1,8 +1,8 @@
CosmOS CosmOS
> greet.sbx 211 > greet.sbx 211
hello.sbx 53 hello.sbx 53
Life.sbx 1410 Life.sbx 1396
Snake.sbx 2168 Snake.sbx 2163
Keys.sbx 664 Keys.sbx 664
Say.sbx 156 Say.sbx 156
Break.sbx 149 Break.sbx 149
+15 -15
View File
@@ -1,6 +1,6 @@
CosmOS CosmOS
> loaded, starting at 4000 > loaded, starting at 4000
> +----------------+ > +----------------+
| | | |
| | | |
| | | |
@@ -19,7 +19,7 @@ CosmOS
| | | |
+----------------+ +----------------+
score 0 wasd steers, q stops score 0 wasd steers, q stops
+----------------+ +----------------+
| | | |
| | | |
| | | |
@@ -38,7 +38,7 @@ CosmOS
| | | |
+----------------+ +----------------+
score 0 wasd steers, q stops score 0 wasd steers, q stops
+----------------+ +----------------+
| | | |
| | | |
| | | |
@@ -57,7 +57,7 @@ CosmOS
| | | |
+----------------+ +----------------+
score 0 wasd steers, q stops score 0 wasd steers, q stops
+----------------+ +----------------+
| | | |
| | | |
| | | |
@@ -76,7 +76,7 @@ CosmOS
| | | |
+----------------+ +----------------+
score 0 wasd steers, q stops score 0 wasd steers, q stops
+----------------+ +----------------+
| | | |
| | | |
| | | |
@@ -95,7 +95,7 @@ CosmOS
| | | |
+----------------+ +----------------+
score 0 wasd steers, q stops score 0 wasd steers, q stops
+----------------+ +----------------+
| | | |
| | | |
| | | |
@@ -114,7 +114,7 @@ CosmOS
| | | |
+----------------+ +----------------+
score 0 wasd steers, q stops score 0 wasd steers, q stops
+----------------+ +----------------+
| | | |
| | | |
| | | |
@@ -133,7 +133,7 @@ CosmOS
| | | |
+----------------+ +----------------+
score 0 wasd steers, q stops score 0 wasd steers, q stops
+----------------+ +----------------+
| | | |
| | | |
| | | |
@@ -152,7 +152,7 @@ CosmOS
| | | |
+----------------+ +----------------+
score 0 wasd steers, q stops score 0 wasd steers, q stops
+----------------+ +----------------+
| | | |
| | | |
| | | |
@@ -171,7 +171,7 @@ CosmOS
| | | |
+----------------+ +----------------+
score 0 wasd steers, q stops score 0 wasd steers, q stops
+----------------+ +----------------+
| | | |
| | | |
| | | |
@@ -190,7 +190,7 @@ CosmOS
| | | |
+----------------+ +----------------+
score 0 wasd steers, q stops score 0 wasd steers, q stops
+----------------+ +----------------+
| | | |
| | | |
| | | |
@@ -209,7 +209,7 @@ CosmOS
| | | |
+----------------+ +----------------+
score 0 wasd steers, q stops score 0 wasd steers, q stops
+----------------+ +----------------+
| | | |
| | | |
| | | |
@@ -228,7 +228,7 @@ CosmOS
| | | |
+----------------+ +----------------+
score 1 wasd steers, q stops score 1 wasd steers, q stops
+----------------+ +----------------+
| | | |
| | | |
| | | |
@@ -247,7 +247,7 @@ CosmOS
| | | |
+----------------+ +----------------+
score 1 wasd steers, q stops score 1 wasd steers, q stops
+----------------+ +----------------+
| | | |
| | | |
| | | |
@@ -266,7 +266,7 @@ CosmOS
| | | |
+----------------+ +----------------+
score 1 wasd steers, q stops score 1 wasd steers, q stops
+----------------+ +----------------+
| | | |
| | | |
| | | |
+1 -1
View File
@@ -1,6 +1,6 @@
00 02 00 00 02 00
10 10 00 10 10 00
FF 01 00 FF 01 00
05 00 00 80 00 00
Execution halted. Execution halted.
[exit 0] [exit 0]
+56 -30
View File
@@ -131,6 +131,22 @@ size() {
head -c 20 "$BUILD/$1.ppm" | sed -n '2p' head -c 20 "$BUILD/$1.ppm" | sed -n '2p'
} }
# ---- What a program said, as numbers ----
#
# With two things taken out that are not the program's: the cursor sequences the console
# generates to drive a host terminal, and the emulator's own halt line. Counting bytes from
# either end of the raw file worked until the console started announcing the cursor, and
# then quietly measured an escape.
said() {
python3 - "$BUILD/$1.out" <<'PY'
import re, sys
data = open(sys.argv[1], "rb").read()
data = re.sub(rb"\x1b\[[0-9;]*[A-Za-z]", b"", data)
data = re.sub(rb"Execution [^\n]*\n$", b"", data)
print(" ".join(str(byte) for byte in data))
PY
}
echo "Checking what the video device draws." echo "Checking what the video device draws."
# ---- The machine wakes up able to show text ---- # ---- The machine wakes up able to show text ----
@@ -263,7 +279,7 @@ echo "Checking what the video device draws."
# The geometry is asked for rather than assumed, so a program can be written once and find # The geometry is asked for rather than assumed, so a program can be written once and find
# out what it is running on. # out what it is running on.
{ prologue; show 0x32; show 0x33; epilogue; } | run geometry || exit 1 { prologue; show 0x32; show 0x33; epilogue; } | run geometry || exit 1
GOT="$(head -c 2 "$BUILD/geometry.out" | od -An -tu1 | tr -s ' ' | sed 's/^ //;s/ $//')" GOT="$(said geometry)"
[ "$GOT" = "40 25" ] \ [ "$GOT" = "40 25" ] \
&& result ok "the ports say how big the screen is" "40 columns, 25 rows" \ && result ok "the ports say how big the screen is" "40 columns, 25 rows" \
|| result no "the ports say how big the screen is" "got \"$GOT\"" || result no "the ports say how big the screen is" "got \"$GOT\""
@@ -273,7 +289,7 @@ GOT="$(head -c 2 "$BUILD/geometry.out" | od -An -tu1 | tr -s ' ' | sed 's/^ //;s
# Not taken, and not fatal either. A screen is a poor place to stop the machine: a program # Not taken, and not fatal either. A screen is a poor place to stop the machine: a program
# that asked for something impossible still has the screen it had. # that asked for something impossible still has the screen it had.
{ prologue; port 0x31 0x09; show 0x32; epilogue; } | run badmode || exit 1 { prologue; port 0x31 0x09; show 0x32; epilogue; } | run badmode || exit 1
GOT="$(head -c 1 "$BUILD/badmode.out" | od -An -tu1 | tr -d ' ')" GOT="$(said badmode)"
[ "$GOT" = "40" ] \ [ "$GOT" = "40" ] \
&& result ok "an impossible mode is not taken" "still 40 columns" \ && result ok "an impossible mode is not taken" "still 40 columns" \
|| result no "an impossible mode is not taken" "got $GOT" || result no "an impossible mode is not taken" "got $GOT"
@@ -347,42 +363,52 @@ papered scrolled 2 1 \
&& result ok "the row that came into view is clear" "not what was there a ring ago" \ && result ok "the row that came into view is clear" "not what was there a ring ago" \
|| result no "the row that came into view is clear" "something at 2,1" || result no "the row that came into view is clear" "something at 2,1"
# ---- The sequences the corpus already speaks ---- # ---- Cursor registers, in place of a protocol ----
# #
# Every program here that moves a cursor does it with ANSI escapes, because until there was # The console used to be given escape sequences and parse them. It is not a terminal and the
# a screen the thing on the other end was somebody's terminal. A controller that did not # screen is not on the other end of a serial line, so it takes registers instead: rows and
# understand them drew "[2J" on the screen and left the board underneath, which is what Snake # columns are written and READ BACK, which is the thing an escape sequence cannot do without
# did the first time it was run in a window. # sending a query and parsing a reply.
{ printf '#Program\nstart:\n'; say "A"; emit 27; say "[2J"; epilogue; } | run clearscreen || exit 1 { printf '#Program\nstart:\n'; say "A"; port 0x05 0x01; epilogue; } | run clearcommand || exit 1
papered clearscreen 2 1 \ papered clearcommand 2 1 \
&& result ok "ESC[2J clears the screen" "the letter is gone" \ && result ok "the clear command clears the screen" "the letter is gone" \
|| result no "ESC[2J clears the screen" "still inked at 2,1" || result no "the clear command clears the screen" "still inked at 2,1"
{ printf '#Program\nstart:\n'; emit 10; emit 10; say "A"; emit 27; say "[H"; say "A" { printf '#Program\nstart:\n'
emit 10; emit 10; say "A"
port 0x03 0x00; port 0x04 0x00
say "A"
epilogue epilogue
} | run home || exit 1 } | run cursorhome || exit 1
inked home 2 1 \ inked cursorhome 2 1 \
&& result ok "ESC[H goes back to the corner" "the second letter is at row 0" \ && result ok "the cursor goes where it is put" "row 0, column 0" \
|| result no "ESC[H goes back to the corner" "nothing at 2,1" || result no "the cursor goes where it is put" "nothing at 2,1"
inked home 2 17 \ inked cursorhome 2 17 \
&& result ok "and leaves what was drawn alone" "the first is still on row 2" \ && result ok "and leaves what was drawn alone" "the first is still on row 2" \
|| result no "and leaves what was drawn alone" "nothing at 2,17" || result no "and leaves what was drawn alone" "nothing at 2,17"
{ printf '#Program\nstart:\n'; emit 27; say "[3;5H"; say "A"; epilogue; } | run position || exit 1 { printf '#Program\nstart:\n'; port 0x03 0x02; port 0x04 0x04; say "A"; epilogue
inked position 34 17 \ } | run cursorput || exit 1
&& result ok "ESC[3;5H puts the cursor there" "row 3, column 5, counting from one" \ inked cursorput 34 17 \
|| result no "ESC[3;5H puts the cursor there" "nothing at 34,17" && result ok "row and column are counted from zero" "row 2, column 4" \
|| result no "row and column are counted from zero" "nothing at 34,17"
# A sequence nobody implemented should leave no marks, which is what a real terminal does # Readable, which is the point of them being registers. Three characters put the cursor at
# with one it does not know. Drawing it would be worse than ignoring it. # column 3, and asking says so.
{ printf '#Program\nstart:\n'; emit 27; say "[9m"; say "A"; epilogue; } | run unknownseq || exit 1 { printf '#Program\nstart:\n'; say "AAA"; show 0x04; show 0x03; epilogue; } | run cursorread || exit 1
inked unknownseq 2 1 \ GOT="$(said cursorread)"
&& result ok "an unknown sequence is swallowed" "the letter after it is at cell 0" \ [ "$GOT" = "65 65 65 3 0" ] \
|| result no "an unknown sequence is swallowed" "nothing at 2,1" && result ok "and the cursor can be read back" "column 3, row 0" \
papered unknownseq 10 1 \ || result no "and the cursor can be read back" "got \"$GOT\""
&& result ok "and leaves nothing behind" "cell 1 is untouched" \
|| result no "and leaves nothing behind" "something at 10,1" # A cursor asked to go off the screen has an obvious place to be, and stopping the machine
# over one would be a poor trade.
{ printf '#Program\nstart:\n'; port 0x04 0xFF; show 0x04; epilogue; } | run cursorclamp || exit 1
GOT="$(said cursorclamp)"
[ "$GOT" = "39" ] \
&& result ok "a cursor past the edge is clamped" "column 39, the last one" \
|| result no "a cursor past the edge is clamped" "got $GOT"
# And what scrolled off the top is still in the map, which is scrollback nothing had to keep. # And what scrolled off the top is still in the map, which is scrollback nothing had to keep.
{ printf '#Program\nstart:\n' { printf '#Program\nstart:\n'