SWI osExit takes a status in A, and the shell keeps it. Fifty eight exits across twenty three programs now say deliberately whether they worked: 25 did what they were asked, 24 did not, 9 were asked wrongly. Compare is the exception and says so - one there means the files differ, which is a result rather than a failure, the way diff has always had it. IN A RATHER THAN Q, which is not a departure from the rule that a service answers in Q. This one takes an ARGUMENT, the way osPrintNumber takes A and B, and it never returns to answer anything. A is free precisely because a return would have put it back - and Q is the ALU's output, so a small number costs four instructions there against one in A. The shell does not print it. A program that failed has already said so in words and a number beside that is noise, so osLastStatus hands it back and Status is the program that shows it. That indirection is the point: the number exists for the thing that cannot read words. MARKING THE EXITS FOUND A DEFECT ON THE FIRST RUN. Type and More printed why they had failed and then fell through into the success exit, reporting that all was well. Nobody had noticed, because while the only reader was a person, the person could see both the complaint and the claim. Two smaller things. Snake sets the console to line mode and then exits with zero, and the linter flagged the second RSTA as redundant - an exit status and a console mode, equal by accident, which is the class that must never be collapsed. And the README still taught answering by writing into the frame, three months of habit that SRET replaced yesterday; that section is gone and the one describing SRET stands in its place.
390 lines
8.1 KiB
NASM
390 lines
8.1 KiB
NASM
; Conway's Game of Life, as an application CosmOS can load and run.
|
|
;
|
|
; Ported from gameOfLife/16x16LifeModern.asm. The simulation is unchanged: the same
|
|
; interleaved 18 by 18 board with a dead border, the same four-Data-Pointer rewrite of
|
|
; the neighbor count, and the same glider.
|
|
;
|
|
; What had to change is when it stops. On the bare machine this program never stopped,
|
|
; because Life has no end state to reach and nothing was waiting for the machine back.
|
|
; Under CosmOS a program owns the console until it returns, so a program that never ends
|
|
; takes the shell with it. There is no HALT to fall into and no key that can interrupt
|
|
; it: console input is a blocking read, so a running program cannot ask whether anybody
|
|
; has pressed anything.
|
|
;
|
|
; So it stops on its own, two ways:
|
|
;
|
|
; IT SETTLES. commitBoard already walks the current and next state of every cell side
|
|
; by side, so it can notice for free whether any of them differed. When none did, the
|
|
; board has reached a state it will stay in forever and there is nothing left to show.
|
|
; The glider does reach one: it crosses the field, runs into the dead border, and
|
|
; collapses into a block in the corner at generation 54.
|
|
;
|
|
; IT RUNS OUT. Settling catches still lifes and extinction. It does not catch an
|
|
; oscillator - a blinker would flip back and forth forever and never be "unchanged" -
|
|
; so there is a generation limit behind it. It is not meant to be the answer. It is
|
|
; there so that no seed anybody tries later can take the shell down with it.
|
|
;
|
|
; Note that #Include print.asm comes at the END of this file rather than the beginning.
|
|
; print.asm opens with a branch to start, which is what a boot image wants at address
|
|
; zero; a loadable program wants its own first instruction at its code base instead.
|
|
|
|
#Include services.asm
|
|
|
|
#Program
|
|
|
|
#Base 0x4000
|
|
|
|
start:
|
|
CALL seedGlider
|
|
SETD.0 ClearScreen
|
|
CALL printString
|
|
SETD.3 GenerationsLeft
|
|
INIA 0xFF
|
|
STA.3
|
|
|
|
; Key mode, so that one keypress is one byte and stops it. In line mode the terminal
|
|
; holds what is typed until Return, so nothing arrives until then and "press any key"
|
|
; would really mean "press any key and then Return". It is put back before this program
|
|
; returns; CosmOS puts it back too, in case a program stops without doing so.
|
|
INIA 0x01
|
|
OUTA 0x02
|
|
|
|
generationLoop:
|
|
CALL renderBoard
|
|
CALL evolveBoard
|
|
CALL commitBoard
|
|
|
|
; Has anybody asked it to stop? The status port answers without waiting, which is the
|
|
; whole reason it exists: reading the data port here would stop the simulation dead
|
|
; until somebody typed something, which is the opposite of what is wanted.
|
|
;
|
|
; READY is clear at the end of input as well as when nothing has been typed, so running
|
|
; with input from a file - which is how the tests run it - never stops here. It runs to
|
|
; the still life instead, and that is the right answer in both places.
|
|
INA 0x01
|
|
INIB 0x01 ; READY
|
|
AND
|
|
BRQ lifeNoKey
|
|
INA 0x00 ; Take the key, so it is not left waiting for the shell.
|
|
BRI lifeStopped
|
|
lifeNoKey:
|
|
|
|
; commitBoard leaves the flag set if any cell differed from what replaced it. DP3 is
|
|
; pointed at it again rather than trusting what the call left behind: RET does not put
|
|
; DP3 back, so its value after a call is the callee's business and not a promise.
|
|
SETD.3 BoardChanged
|
|
LDA.3
|
|
BRA lifeSettled
|
|
|
|
SETD.3 GenerationsLeft
|
|
LDA.3
|
|
DECA
|
|
STA.3
|
|
BRA lifeRanOut
|
|
|
|
CALL frameDelay
|
|
BRI generationLoop
|
|
|
|
; The three ways it can be over. Each one only picks the words; the tidying up is the same
|
|
; for all of them and is written once, which is also how the console cannot be left in key
|
|
; mode down one path and not another.
|
|
lifeStopped:
|
|
SETD.0 StoppedText
|
|
BRI lifeEnd
|
|
|
|
lifeSettled:
|
|
SETD.0 SettledText
|
|
BRI lifeEnd
|
|
|
|
lifeRanOut:
|
|
SETD.0 RanOutText
|
|
|
|
lifeEnd:
|
|
RSTA
|
|
OUTA 0x02 ; Line mode, the way it was found.
|
|
CALL lineFeed
|
|
CALL printString ; DP0 still holds the words: CALL puts DP0 back.
|
|
CALL lineFeed
|
|
RSTA
|
|
SWI osExit
|
|
|
|
seedGlider:
|
|
SETD.0 Board
|
|
DPUP.0 0d42
|
|
INIA 0x01
|
|
STA.0
|
|
SETD.0 Board
|
|
DPUP.0 0d80
|
|
STA.0
|
|
SETD.0 Board
|
|
DPUP.0 0d112
|
|
STA.0
|
|
DPUP.0 0d02
|
|
STA.0
|
|
DPUP.0 0d02
|
|
STA.0
|
|
RET
|
|
|
|
renderBoard:
|
|
SETD.0 CursorHome
|
|
CALL printString
|
|
SETD.1 RowCount
|
|
SETD.2 ColCount
|
|
INIA 0d16
|
|
STA.1
|
|
SETD.0 Board
|
|
DPUP.0 0d38
|
|
|
|
renderRow:
|
|
INIA 0d16
|
|
STA.2
|
|
renderCell:
|
|
LDA.0
|
|
BRA renderDead
|
|
INIB 0x23
|
|
OUTB 0x00
|
|
BRI renderCellDone
|
|
renderDead:
|
|
INIB 0x20
|
|
OUTB 0x00
|
|
renderCellDone:
|
|
DPUP.0 0d02
|
|
LDA.2
|
|
DECA
|
|
STA.2
|
|
BRA renderRowDone
|
|
BRI renderCell
|
|
|
|
renderRowDone:
|
|
CALL lineFeed
|
|
DPUP.0 0d04
|
|
LDA.1
|
|
DECA
|
|
STA.1
|
|
BRA renderDone
|
|
BRI renderRow
|
|
renderDone:
|
|
RET
|
|
|
|
evolveBoard:
|
|
SETD.1 RowCount
|
|
SETD.2 ColCount
|
|
INIA 0d16
|
|
STA.1
|
|
SETD.0 Board
|
|
DPUP.0 0d38
|
|
|
|
evolveRow:
|
|
INIA 0d16
|
|
STA.2
|
|
evolveCellLoop:
|
|
CALL evolveCell
|
|
DPUP.0 0d02
|
|
LDA.2
|
|
DECA
|
|
STA.2
|
|
BRA evolveRowDone
|
|
BRI evolveCellLoop
|
|
|
|
evolveRowDone:
|
|
DPUP.0 0d04
|
|
LDA.1
|
|
DECA
|
|
STA.1
|
|
BRA evolveDone
|
|
BRI evolveRow
|
|
evolveDone:
|
|
RET
|
|
|
|
evolveCell:
|
|
CALL countNeighbors
|
|
MVQB ; B is the neighbor count from here down.
|
|
|
|
; Three neighbors always produces a live cell.
|
|
INIA 0d03
|
|
CCF
|
|
SUB
|
|
BRQ makeAlive
|
|
|
|
; Two neighbors preserve the current state.
|
|
INIA 0d02
|
|
CCF
|
|
SUB
|
|
BRQ preserveCell
|
|
|
|
makeDead:
|
|
RSTA
|
|
INCD.0
|
|
STA.0
|
|
DECD.0
|
|
RET
|
|
|
|
preserveCell:
|
|
LDA.0
|
|
BRA makeDead
|
|
|
|
makeAlive:
|
|
INIA 0x01
|
|
INCD.0
|
|
STA.0
|
|
DECD.0
|
|
RET
|
|
|
|
; Return the eight-neighbor sum in Q. One Stack round-trip copies DP0 into
|
|
; volatile DP3; MVQA then keeps the running total entirely in registers.
|
|
countNeighbors:
|
|
PSHD.0
|
|
POPD.3
|
|
RSTA
|
|
DPDN.3 0d38
|
|
|
|
LDB.3
|
|
CCF
|
|
ADD
|
|
MVQA
|
|
DPUP.3 0d02
|
|
|
|
LDB.3
|
|
CCF
|
|
ADD
|
|
MVQA
|
|
DPUP.3 0d02
|
|
|
|
LDB.3
|
|
CCF
|
|
ADD
|
|
MVQA
|
|
DPUP.3 0d32
|
|
|
|
LDB.3
|
|
CCF
|
|
ADD
|
|
MVQA
|
|
DPUP.3 0d04
|
|
|
|
LDB.3
|
|
CCF
|
|
ADD
|
|
MVQA
|
|
DPUP.3 0d32
|
|
|
|
LDB.3
|
|
CCF
|
|
ADD
|
|
MVQA
|
|
DPUP.3 0d02
|
|
|
|
LDB.3
|
|
CCF
|
|
ADD
|
|
MVQA
|
|
DPUP.3 0d02
|
|
|
|
LDB.3
|
|
CCF
|
|
ADD
|
|
RET
|
|
|
|
; Copies each cell's next state over its current one, and says whether any of them
|
|
; differed. The comparison is what the bare metal version did not need: it is one XOR
|
|
; on two bytes that are already in registers, in a loop that was already visiting every
|
|
; cell, which is why "has it settled" costs almost nothing to ask.
|
|
;
|
|
; DP3 holds the flag for the whole walk. commitBoard calls nothing, so nothing else can
|
|
; want DP3 while it works.
|
|
commitBoard:
|
|
SETD.3 BoardChanged
|
|
RSTA
|
|
STA.3
|
|
SETD.1 RowCount
|
|
SETD.2 ColCount
|
|
INIA 0d18
|
|
STA.1
|
|
SETD.0 Board
|
|
|
|
commitRow:
|
|
INIA 0d18
|
|
STA.2
|
|
commitCell:
|
|
LDB.0 ; The cell as it stands.
|
|
INCD.0
|
|
LDA.0 ; The cell as it is about to stand.
|
|
DECD.0
|
|
STA.0
|
|
XOR ; Q is zero only if those two were the same.
|
|
BRQ commitSame
|
|
INIA 0x01
|
|
STA.3 ; Something moved, so this is not the last generation.
|
|
commitSame:
|
|
DPUP.0 0d02
|
|
LDA.2
|
|
DECA
|
|
STA.2
|
|
BRA commitRowDone
|
|
BRI commitCell
|
|
|
|
commitRowDone:
|
|
LDA.1
|
|
DECA
|
|
STA.1
|
|
BRA commitDone
|
|
BRI commitRow
|
|
commitDone:
|
|
RET
|
|
|
|
frameDelay:
|
|
INIA 0xFF
|
|
delayOuter:
|
|
INIB 0xFF
|
|
delayInner:
|
|
DECB
|
|
BRB delayInnerDone
|
|
BRI delayInner
|
|
delayInnerDone:
|
|
DECA
|
|
BRA delayDone
|
|
BRI delayOuter
|
|
delayDone:
|
|
RET
|
|
|
|
#Data
|
|
|
|
#Base 0x2000
|
|
|
|
RowCount:
|
|
0x00
|
|
ColCount:
|
|
0x00
|
|
|
|
; Cleared at the top of every commitBoard and set by any cell that changed, so after a
|
|
; commit it describes that generation and no other.
|
|
BoardChanged:
|
|
0x00
|
|
; Counts down. One byte is enough for a limit that is not meant to be reached.
|
|
GenerationsLeft:
|
|
0x00
|
|
|
|
SettledText:
|
|
"the board has settled"
|
|
RanOutText:
|
|
"stopped: still changing after 255 generations"
|
|
StoppedText:
|
|
"stopped"
|
|
|
|
ClearScreen:
|
|
0x1B
|
|
"[2J"
|
|
CursorHome:
|
|
0x1B
|
|
"[H"
|
|
|
|
; 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
|
|
; means the assembler believes the board is one byte long: anything placed after it
|
|
; would land inside it, and nothing would say so. Reserving the region states how far
|
|
; it reaches, so a label added below here is safe.
|
|
Board:
|
|
#Reserve 0d648
|
|
|
|
#Include print.asm
|