CosmOS pre-alpha and launchable application versions of old programs.

This commit is contained in:
Anachronaut
2026-08-17 15:31:49 -04:00
parent eff6902bcf
commit 91c9d49d1b
66 changed files with 5612 additions and 160 deletions
+1 -1
View File
@@ -5,5 +5,5 @@ Assembler
SplitBit
SplitDisk
CLAUDE.md
resume
claudeResume.sh
codexResume.sh
+84
View File
@@ -0,0 +1,84 @@
; A Fibonacci number generating program that uses two bytes to store the value.
#Include services.asm
#Program
#Base 0x2000
start:
; Swap ValueB and ValueA.
; First, store ValueA on the stack.
SETD ValueA
LDA
PSHA
INCD
LDA
PSHA
; Now copy ValueB into AB.
SETD ValueB
LDA ; High byte
INCD
LDB ; Low byte
; Now save it back to ValueA
SETD ValueA
STA ; High byte
INCD
STB ; Low byte.
; Now retrieve value A from the stack and store it in ValueB.
POPB
POPA
SETD ValueB
STA
INCD
STB
; Print ValueA.
SETD ValueA
LDA
CALL printByteHex
INCD
LDA
CALL printByteHex
CALL blankSpace
; Now add ValueA and ValueB, and store the result in ValueA.
; Add the low bytes of ValueA and ValueB
SETD ValueB
INCD
LDA
SETD ValueA
INCD
LDB
CCF
ADD
; Store the result in ValueA.
STQ
; Now add the high bytes of ValueA and ValueB.
DECD
LDB
SETD ValueB
LDA
ADD
; If this addition overflows, we're done.
BRC end
; Otherwise, store the result in ValueA.
SETD ValueA
STQ
; And branch back to the beginning of the loop.
BRI start
end:
CALL lineFeed
SWI osExit
#Data
#Base 0x1000
ValueA:
; Low byte, high byte.
0x00 0x01
ValueB:
; Low byte, high byte.
0x00 0x00
#Include print.asm
+140
View File
@@ -0,0 +1,140 @@
; A Fibonacci number generating program that uses four bytes to store the value.
#Include services.asm
#Program
#Base 0x2000
start:
; Swap ValueB and ValueA.
; First, store ValueA on the stack.
SETD ValueA
LDA
PSHA
INCD
LDA
PSHA
INCD
LDA
PSHA
INCD
LDA
PSHA
; Next, store ValueB on the stack.
SETD ValueB
LDA
PSHA
INCD
LDA
PSHA
INCD
LDA
PSHA
INCD
LDA
PSHA
; Then pop ValueB into ValueA.
SETD ValueA
INCD INCD INCD
POPA
STA
DECD
POPA
STA
DECD
POPA
STA
DECD
POPA
STA
; Then pop ValueA into ValueB.
SETD ValueB
INCD INCD INCD
POPA
STA
DECD
POPA
STA
DECD
POPA
STA
DECD
POPA
STA
; Print ValueA.
SETD ValueA
INCD INCD INCD
LDA
CALL printByteHex
DECD
LDA
CALL printByteHex
DECD
LDA
CALL printByteHex
DECD
LDA
CALL printByteHex
CALL blankSpace
; Now add ValueA and ValueB, and store the result in ValueA.
; Add the lowest bytes of ValueA and ValueB.
SETD ValueB
LDB
SETD ValueA
LDA
ADD
; Store it in ValueA's lowest byte.
STQ
; Add the second lowest bytes of ValueA and ValueB.
SETD ValueB
INCD
LDB
SETD ValueA
INCD
LDA
ADD
; Store it in ValueA's second lowest byte.
STQ
; Add the second highest bytes of ValueA and ValueB.
SETD ValueB
INCD INCD
LDB
SETD ValueA
INCD INCD
LDA
ADD
; Store it in ValueA's third lowest byte.
STQ
; Add the highest bytes of ValueA and ValueB.
SETD ValueB
INCD INCD INCD
LDB
SETD ValueA
INCD INCD INCD
LDA
ADD
; If this addition overflows, we're done.
BRC end
; Otherwise, store the result in ValueA's highest byte.
STQ
; And branch back to the beginning of the loop.
BRI start
end:
CALL lineFeed
;HALT
SWI osExit
#Data
#Base 0x1000
ValueA:
; Lowest byte ... Highest byte.
0x01 0x00 0x00 0x00
ValueB:
; Lowest byte ... Highest byte.
0x00 0x00 0x00 0x00
#Include print.asm
+43
View File
@@ -0,0 +1,43 @@
; A Fibonacci number generating program that uses only one byte to store the value.
#Include services.asm
#Program
#Base 0x2000
start:
; Load our initial values into A and B.
INIA 0x00
CALL printByteDecimal
CALL blankSpace
; Move the value into B.
PSHA
POPB
; Load the next starting value into A.
INIA 0x01
CALL printByteDecimal
CALL blankSpace
loop:
ADD ; Add the values together.
BRC end ; If the value overflows, we're done.
; Copy A into B
PSHA
POPB
; Copy Q into A
PSHQ
POPA
; Print A.
CALL printByteDecimal
CALL blankSpace
BRI loop ; Loop again.
end:
CALL lineFeed
;HALT
SWI osExit ; Return to CosmOS.
#Data
#Base 0x1000
#Include print.asm
+388
View File
@@ -0,0 +1,388 @@
; 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 0x2000
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
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 0x1000
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
+230
View File
@@ -0,0 +1,230 @@
; The 16-bit segmented sieve rewritten for SplitBit's four-Data-Pointer ISA.
;
; This deliberately implements the same algorithm and emits the same text as
; 16bitSegmentedSieve.asm, making the two versions useful as a direct comparison.
; DP0 walks PrimeStates, DP1 holds Page, DP2 walks Segment, and volatile DP3
; marks multiples. CALL preserves the first three pointers automatically.
#Include services.asm
#Program
#Base 0x2000
start:
RSTA
SETD.1 Page
STA.1
nextPage:
SETD.2 Segment
RSTA
RSTB
clearSegment:
STB.2
INCD.2
INCA
BRA segmentCleared
BRI clearSegment
segmentCleared:
; Zero and one are not prime.
LDA.1
BRA excludeZeroAndOne
BRI markSegment
excludeZeroAndOne:
SETD.2 Segment
INIA 0x01
STA.2
INCD.2
STA.2
markSegment:
SETD.0 PrimeStates
INIA 0d54
primeLoop:
CALL processPrime
DPUP.0 0d03
DECA
BRA scanSegment
BRI primeLoop
scanSegment:
SETD.2 Segment
RSTA
scanLoop:
LDB.2
BRB emitPrime
scanNext:
INCD.2
INCA
BRA advancePage
BRI scanLoop
emitPrime:
CALL printCandidateHex
BRI scanNext
advancePage:
LDA.1
INCA
STA.1
BRA finished
BRI nextPage
finished:
CALL lineFeed
SWI osExit ; Return to CosmOS.
; DP0 points at a PrimeStates entry. CALL restores it on return.
processPrime:
INCD.0
LDA.0
LDB.1
XOR
BRQ primeIsActive
RET
primeIsActive:
; B is the prime and A its current offset.
DECD.0
LDB.0
DPUP.0 0d02
LDA.0
; DP3 = Segment + offset. Only this one initial pointer copy needs the Stack.
SETD.3 Segment
PSHB
PSHD.3
POPB
CCF
ADD
PSHQ
POPD.3
POPB
markPrimeLoop:
INIA 0x01
STA.3
; Add the prime to DP3's low byte. A carry crosses into the next window.
PSHD.3
POPA
CCF
ADD
PSHQ
POPD.3
BRC primeFinished
BRI markPrimeLoop
primeFinished:
; DP0 is on the offset byte; advance the saved high byte and save Q as
; the wrapped offset for the following page.
DECD.0
LDA.0
INCA
STA.0
INCD.0
STQ.0
RET
printCandidateHex:
PSHA
LDA.1
CALL printByteHex
POPA
CALL printByteHex
CALL blankSpace
RET
#Data
#Base 0x1000
; Segment has to begin on a page boundary, and now says so itself rather than relying on
; whatever happens to have been assembled before it. The marking loop adds the prime to
; the low byte of DP3 and treats the carry out as the end of the page, so it only finds
; the right boundary if the window starts on one.
;
; The whole window is written out here so that Page and PrimeStates begin after it.
#Align 0x100
Segment:
0x00
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
Page:
0x00
PrimeStates:
0x02 0x00 0x04
0x03 0x00 0x09
0x05 0x00 0x19
0x07 0x00 0x31
0x0B 0x00 0x79
0x0D 0x00 0xA9
0x11 0x01 0x21
0x13 0x01 0x69
0x17 0x02 0x11
0x1D 0x03 0x49
0x1F 0x03 0xC1
0x25 0x05 0x59
0x29 0x06 0x91
0x2B 0x07 0x39
0x2F 0x08 0xA1
0x35 0x0A 0xF9
0x3B 0x0D 0x99
0x3D 0x0E 0x89
0x43 0x11 0x89
0x47 0x13 0xB1
0x49 0x14 0xD1
0x4F 0x18 0x61
0x53 0x1A 0xE9
0x59 0x1E 0xF1
0x61 0x24 0xC1
0x65 0x27 0xD9
0x67 0x29 0x71
0x6B 0x2C 0xB9
0x6D 0x2E 0x69
0x71 0x31 0xE1
0x7F 0x3F 0x01
0x83 0x43 0x09
0x89 0x49 0x51
0x8B 0x4B 0x79
0x95 0x56 0xB9
0x97 0x59 0x11
0x9D 0x60 0x49
0xA3 0x67 0xC9
0xA7 0x6C 0xF1
0xAD 0x74 0xE9
0xB3 0x7D 0x29
0xB5 0x7F 0xF9
0xBF 0x8E 0x81
0xC1 0x91 0x81
0xC5 0x97 0x99
0xC7 0x9A 0xB1
0xD3 0xAD 0xE9
0xDF 0xC2 0x41
0xE3 0xC9 0x49
0xE5 0xCC 0xD9
0xE9 0xD4 0x11
0xEF 0xDF 0x21
0xF1 0xE2 0xE1
0xFB 0xF6 0x19
#Include print.asm
+74
View File
@@ -0,0 +1,74 @@
; This is an implementation of The Sieve of Eratosthenes that finds all the primes between 2 and 255.
#Include services.asm
#Program
#Base 0x2000
start:
; Search the list until we find a prime.
SETD DataTop
CCF ; Clear the carry flag. In later cycles, the carry flag will be set at the end of the next loop. We'll want it cleared.
RSTA
RSTB
findPrimeLoop:
LDB ; Load an element into B.
BRB foundPrime ; If it's zero, it's a prime.
INCA ; Increment A, our index.
INCD ; Increment the Data Pointer.
BRA end ; If A becomes zero, we've looked through the whole list without finding another prime.
BRI findPrimeLoop ; Keep searching for the next prime.
foundPrime:
; If we've found a prime, we should print it and mark it off the list so we don't print it again.
CALL printByteDecimal ; A contains our prime, so we can just call the print subroutine.
CALL blankSpace ; Put a space afterward to keep things easy to read.
INIB 0x01 ; Set B to 1.
STB ; Mark this prime off the list.
markMultiples:
; Now, we mark each multiple of this prime as nonprime until we reach the end of the list.
PSHD ; Save the Data Pointer to the stack.
POPB ; Pop its low byte into B.
ADD ; Add them together.
PSHQ ; Store the result back onto the stack.
POPD ; Pop the modified address into the Data Pointer.
INIB 0x01 ; Set B to 1.
STB ; Store B to mark the value as nonprime.
BRC start ; If the previous add overflowed, the next nonprime is outside the range of our list, so start over with a new prime.
BRI markMultiples ; Otherwise, loop again to mark the next multiple as nonprime.
end:
CALL lineFeed ; Print a linefeed to make it look nice.
SWI osExit ; The program is done, we found all the primes!
#Data
#Base 0x1000
; The table of our prime candidates. It has to begin on a page boundary: marking walks
; the pointer's low byte and treats the carry out as running off the end of the table,
; which only finds the right end if the table starts on one.
#Align 0x100
DataTop:
0x01 0x01 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00
#Include print.asm
+61
View File
@@ -0,0 +1,61 @@
; A program for CosmOS to load and run.
;
; It carries no library of its own and no vector table. Everything it can do it asks the
; system for, by name, through services.asm, which the system includes too. The order of
; the names in that one file is what gives them their numbers, so neither side has a
; number written down anywhere and the two cannot disagree about them.
;
; Compare it with Programs/loadable/hello.asm, which is the same idea one step earlier:
; that one talks to the console port itself and stops with HALT, because when it was
; written there was no system to ask and nowhere to give the machine back to.
;
; It is assembled for where it will live. #Base says so, and that makes the assembler
; write it out as a loadable program rather than as a boot image. Nothing relocates
; anything, so those addresses have to be the ones CosmOS puts it at.
#Include services.asm
#Program
#Base 0x2000 ; Above the system, which keeps below here.
greet:
SETD.0 Opening
SWI osPrintString
SETD.0 Question
SWI osPrintString
SETD.0 Answer
INIB 0d31
SWI osReadLine
SETD.0 Hello
SWI osPrintString
SETD.0 Answer
SWI osPrintString
SETD.0 Ending
SWI osPrintString
; Give the machine back. The system takes its Stack back at this point, so everything
; this program pushed goes with it.
SWI osExit
#Data
#Base 0x1000 ; And its data above the system's.
Opening:
"a program, loaded off a disk, running on the system that loaded it
"
Question:
"what should I call you? "
Hello:
"hello, "
Ending:
". that is all I do.
"
; Thirty one characters and the zero byte that ends them.
Answer:
#Reserve 0d32
+30
View File
@@ -0,0 +1,30 @@
; This is a basic hello world program for the SplitBit CPU.
; We'll create a loop that outputs each byte of our string to Output 0, the text console.
; Include the system services so we can return.
#Include services.asm
#Program
#Base 0x2000 ; Change two:
SETD hello ; Change three
Start:
LDA ; Load a byte of the string into A.
BRA End ; If A is zero, branch out of the loop.
OUTA 0x00 ; Output the value in A to Port 0, the text console.
INCD ; Increment the Data Pointer to the next byte of the string.
BRI Start ; Branch immediately to the start of the loop.
End:
INIA 0x0A ; We'll load a linefeed into A and output it to make it look nice.
OUTA 0x00 ; Output it to the text console.
;HALT ; Terminate the program.
; Instead, let's call osExit to return the system nicely. Fourth change.
SWI osExit
#Data
#Base 0x1000 ; Five, adjust the base of the data segment.
hello: ; Throw a label here so we can explicitly point at this data. Six, actually.
"Hello, World!"
Binary file not shown.
Binary file not shown.
+1
View File
@@ -0,0 +1 @@
Hello, SplitBit!
+388
View File
@@ -0,0 +1,388 @@
; console.asm
; Talking to the console.
;
; This is the modern replacement for print.asm, which was written for a machine with one
; Data Pointer and no vector table. The old one is left where it is, because the programs
; that include it still work and are meant to keep working.
;
; Two things are different here, and both are deliberate.
;
; There is no branch at the top. print.asm begins with BRI start, so that a program
; including it arrives at its own entry point instead of falling into the library. That
; was the only way to do it before the Boot vector existed. A program including this file
; says where it begins in its own Vector Segment:
;
; #Vectors
; Boot start
;
; And every routine here names the Data Pointer it works through rather than assuming the
; only one. A pointer handed in is DP0. Nothing here disturbs what the caller left in
; DP3, which is the one a return survives in.
;
; What a routine gives back is in Q, because Q and DP3 are the only things a RET does not
; put back the way it found them.
;
; Written by Anachronaut
#Program
; ---- Characters and strings ----
; A line feed.
newLine:
INIA 0x0A
OUTA 0x00
RET
; DP0 names a string ending in a zero byte. Prints it.
printString:
LDA.0
BRA printStringDone
OUTA 0x00
INCD.0
BRI printString
printStringDone:
RET
; A holds how many spaces to print. None is a fair answer, and prints nothing.
printSpaces:
BRA printSpacesDone
INIB 0x20
printSpacesLoop:
OUTB 0x00
DECA
BNA printSpacesLoop
printSpacesDone:
RET
; ---- Hexadecimal ----
; A holds a byte. Prints it as two hexadecimal digits, high one first.
;
; A and B are a circular shift register sixteen bits long, so rotating right four times
; with B empty walks the high nybble down into place and parks the low one in B. The call
; between the two halves puts A and B back as they were, which is what lets the second
; rotation find the low nybble still waiting.
printByteHex:
RSTB
SHR SHR SHR SHR
CALL printHexDigit
RSTA
SHL SHL SHL SHL
CALL printHexDigit
RET
; DP0 names two bytes, most significant first, the way every number on a SplitBit disk is
; stored. Prints them as four hexadecimal digits.
printWordHex:
LDA.0
CALL printByteHex
INCD.0
LDA.0
CALL printByteHex
RET
; A holds a nybble. Prints the one character that stands for it.
printHexDigit:
INIB 0d10
CCF
SUB
BRC printDecimalDigit ; Under ten, so it is a plain digit.
INIB 0x37 ; 'A' is ten, so this is the offset that gets there.
CCF
ADD
OUTQ 0x00
RET
; A holds a digit from zero to nine. Prints it.
printDecimalDigit:
INIB 0x30
CCF
ADD
OUTQ 0x00
RET
; ---- Decimal ----
; A holds a byte. Prints it in decimal, without leading zeroes.
printByteDecimal:
SETD.0 ConsoleValue
STA.0
SETD.1 ConsoleLeading
RSTA
STA.1 ; Nothing has been printed yet.
INIA 0d100
CALL printBytePlace
INIA 0d10
CALL printBytePlace
; Whatever is left is the ones, and it prints whether or not it is a zero, because a
; number has to show at least one digit.
SETD.0 ConsoleValue
LDA.0
CALL printDecimalDigit
RET
; A holds a power of ten. Counts how many times it comes out of ConsoleValue, prints that
; as a digit, and leaves the remainder behind. A leading zero prints nothing.
printBytePlace:
SETD.2 ConsoleBytePower
STA.2
SETD.0 ConsoleValue
SETD.1 ConsoleCount
RSTA
STA.1
printBytePlaceLoop:
LDA.0
LDB.2
CCF
SUB
BRC printBytePlaceDone ; It went below zero, so it does not come out again.
STQ.0
LDA.1
INCA
STA.1
BRI printBytePlaceLoop
printBytePlaceDone:
LDA.1
BNA printBytePlaceShow
; The digit is a zero, which only prints if something has been printed before it.
SETD.2 ConsoleLeading
LDA.2
BRA printBytePlaceQuiet
RSTA
printBytePlaceShow:
CALL printDecimalDigit
SETD.2 ConsoleLeading
INIA 0x01
STA.2
printBytePlaceQuiet:
RET
; DP0 names a two byte number, most significant byte first. Prints it in decimal, without
; leading zeroes. Sixty five thousand five hundred and thirty five is the largest thing it
; can be handed, which is the whole of an address, so nothing overflows this.
printWordDecimal:
SETD.1 ConsoleWord
CALL consoleCopyWord
SETD.1 ConsoleLeading
RSTA
STA.1
SETD.0 ConsoleTenThousand
CALL printWordPlace
SETD.0 ConsoleThousand
CALL printWordPlace
SETD.0 ConsoleHundred
CALL printWordPlace
SETD.0 ConsoleTen
CALL printWordPlace
; What is left is under ten, so it is in the low byte and it is the last digit.
SETD.0 ConsoleWord
INCD.0
LDA.0
CALL printDecimalDigit
RET
; DP0 names a power of ten, two bytes of it. The same counting as printBytePlace, done
; sixteen bits wide.
printWordPlace:
SETD.1 ConsolePower
CALL consoleCopyWord
SETD.1 ConsoleCount
RSTA
STA.1
printWordPlaceLoop:
CALL consoleTakePower
BNQ printWordPlaceDone
SETD.1 ConsoleCount
LDA.1
INCA
STA.1
BRI printWordPlaceLoop
printWordPlaceDone:
SETD.1 ConsoleCount
LDA.1
BNA printWordPlaceShow
SETD.1 ConsoleLeading
LDA.1
BRA printWordPlaceQuiet
RSTA
printWordPlaceShow:
CALL printDecimalDigit
SETD.1 ConsoleLeading
INIA 0x01
STA.1
printWordPlaceQuiet:
RET
; Takes ConsolePower out of ConsoleWord, if it comes out at all. Q is zero if it did, and
; then ConsoleWord is the smaller for it. If it did not, ConsoleWord is left alone.
;
; The subtraction is done into a spare word rather than in place, because whether it fits
; is not known until the high half is done, and by then an in place low half would already
; have been spent.
;
; The low half clears the Carry Flag first and the high half does not: the borrow the low
; half leaves behind is exactly what the high half has to subtract as well. Nothing
; between them disturbs it, since only the arithmetic instructions and CCF touch it.
consoleTakePower:
SETD.0 ConsoleWord
INCD.0
SETD.1 ConsolePower
INCD.1
LDA.0
LDB.1
CCF
SUB
SETD.2 ConsoleSpare
INCD.2
STQ.2
SETD.0 ConsoleWord
SETD.1 ConsolePower
LDA.0
LDB.1
SUB
BRC consoleTakeNothing
SETD.2 ConsoleSpare
STQ.2
SETD.0 ConsoleSpare
SETD.1 ConsoleWord
CALL consoleCopyWord
RSTA
RSTB
CCF
ADD ; Q is zero: it came out.
RET
consoleTakeNothing:
RSTA
INIB 0x01
CCF
ADD ; Q is one: it did not.
RET
; Two bytes from DP0 to DP1, most significant first.
consoleCopyWord:
LDA.0
STA.1
INCD.0
INCD.1
LDA.0
STA.1
RET
; ---- Reading ----
; DP0 names a buffer and B says how many characters it holds, not counting the zero byte
; that ends it. Reads a line from the console into it. Q is how long the line turned out
; to be.
;
; A line longer than the buffer is cut short, and the rest of it is read and thrown away
; rather than left to turn up as the next line.
;
; ConsoleEndOfInput is set if the console ran out instead of ending a line. That is a
; different thing from an empty line, and a program that reads until there is no more has
; to be able to tell them apart.
readLine:
SETD.1 ConsoleRoom
STB.1
SETD.1 ConsoleLength
RSTA
STA.1
SETD.1 ConsoleEndOfInput
STA.1
readLineNext:
INA 0x00
INIB 0x0A
CCF
SUB
BRQ readLineDone ; A line feed ends the line. A is still the character.
INIB 0xFF
CCF
SUB
BRQ readLineEnd ; There is no more to be had.
; Is there room for it? A still holds the character, so it is put somewhere safe while
; the counting is done.
SETD.1 ConsoleChar
STA.1
SETD.1 ConsoleLength
LDA.1
SETD.1 ConsoleRoom
LDB.1
CCF
SUB
BRQ readLineNext ; Full. Read on, and drop what comes.
SETD.1 ConsoleChar
LDA.1
STA.0
INCD.0
SETD.1 ConsoleLength
LDA.1
INCA
STA.1
BRI readLineNext
readLineEnd:
SETD.1 ConsoleEndOfInput
INIA 0x01
STA.1
readLineDone:
RSTA
STA.0 ; The zero byte that ends it.
SETD.1 ConsoleLength
LDA.1
RSTB
CCF
ADD ; Q is how long the line is.
RET
#Data
; ---- What readLine keeps while it works ----
ConsoleRoom:
0x00
ConsoleLength:
0x00
ConsoleChar:
0x00
; Set when the console ran out rather than ending a line. Cleared at the start of every
; readLine, so it always describes the last line read.
ConsoleEndOfInput:
0x00
; ---- What the number routines keep while they work ----
; Whether any digit has been printed yet, which is what decides if a zero is a leading
; one or a real one.
ConsoleLeading:
0x00
ConsoleCount:
0x00
ConsoleValue:
0x00
ConsoleBytePower:
0x00
ConsolePower:
0x00 0x00
ConsoleWord:
0x00 0x00
ConsoleSpare:
0x00 0x00
; The powers of ten, written the way every number here is written: most significant byte
; first.
ConsoleTenThousand:
0x27 0x10
ConsoleThousand:
0x03 0xE8
ConsoleHundred:
0x00 0x64
ConsoleTen:
0x00 0x0A
+781
View File
@@ -0,0 +1,781 @@
; cosmos.asm
; CosmOS, and the shell that is most of it.
;
; The machine boots into this. It registers what the hardware brought, mounts whatever
; disk is attached, and then reads lines and does what they say until there is no more
; typing to be had.
;
; ---- Where things live ----
;
; The system keeps to the bottom of both memories, and everything above is for whatever
; it is running:
;
; Program Memory 0x0000 - 0x1FFF the system
; 0x2000 - a loaded program's code
; Data Memory 0x0000 - 0x0FFF the system
; 0x1000 - a loaded program's data
;
; Nothing enforces that. Nothing can: the fence guards a range, and this is a convention
; about which range belongs to whom rather than a rule about what may be touched. The
; assembler prints both segment sizes, and they are what to watch.
;
; A program is staged at 0x8000 while it is being loaded, which is inside the region a
; loaded program will own. That is safe because nothing is running during a load, and it
; is where a big program can be read without the system reserving the room for good.
;
; ---- What it can do ----
;
; dir List what is on the disk.
; load Read a program off the disk and put it where it asks to go.
; run Start the program that was loaded.
; help Say what these are.
; exit Stop.
;
; dump is next. The dispatch below is a chain of comparisons, which is the right shape for
; five commands and the wrong shape for twenty; when it grows, the table that
; dispatchTest.asm demonstrates is where it should go.
;
; Written by Anachronaut
#Include console.asm
#Include text.asm
#Include sbfs.asm
#Include services.asm
#Program
boot:
SETD.0 Banner
CALL printString
CALL newLine
; Find out whether there is a filesystem to talk to. Doing this once at boot rather than
; once per command means a disk swapped underneath us is not noticed, which is honest
; for a machine whose disk is a file named on the command line.
CALL sbfsMount
SETD.0 DiskReady
BNQ bootNoDisk
INIA 0x01
STA.0
BRI prompt
bootNoDisk:
RSTA
STA.0
SETD.0 NoDisk
CALL printString
CALL newLine
; ---- The loop ----
prompt:
SETD.0 PromptText
CALL printString
SETD.0 CommandLine
INIB 0d63
CALL readLine
; Running out of typing is how this ends. It is not the same as an empty line, which is
; just somebody pressing return, and the shell should sit there when that happens.
SETD.0 ConsoleEndOfInput
LDA.0
BNA quitRanOut
SETD.0 CommandLine
CALL textSplit
; An empty line asks for nothing.
SETD.0 CommandLine
LDA.0
BRA prompt
SETD.0 CommandLine
SETD.1 DirName
CALL textSame
BRQ doDir
SETD.0 CommandLine
SETD.1 LoadName
CALL textSame
BRQ doLoad
SETD.0 CommandLine
SETD.1 RunName
CALL textSame
BRQ doRun
SETD.0 CommandLine
SETD.1 DumpName
CALL textSame
BRQ doDump
SETD.0 CommandLine
SETD.1 HelpName
CALL textSame
BRQ doHelp
SETD.0 CommandLine
SETD.1 ExitName
CALL textSame
BRQ quit
; Nothing matched. Saying which word was not understood is worth the four instructions:
; it tells somebody who mistyped what they actually typed.
SETD.0 Unknown
CALL printString
SETD.0 CommandLine
CALL printString
CALL newLine
BRI prompt
; Running out of console leaves the cursor part way along a line, because there was no
; return at the end to move it on. Somebody who typed "exit" has already pressed one, and
; a second would only leave a blank line behind.
quitRanOut:
CALL newLine
quit:
SETD.0 Farewell
CALL printString
CALL newLine
HALT
; ---- dir ----
;
; Walks the directory and prints what is in it. A free entry in the middle of a directory
; is stepped over by the walk, so what comes out is the files and nothing else.
doDir:
SETD.0 DiskReady
LDA.0
BRA dirNoDisk
RSTA
SETD.0 DirSeen
STA.0
CALL sbfsFirst
BRI dirCheck
dirStep:
CALL sbfsNext
dirCheck:
BNQ dirDone
SETD.0 DirSeen
LDA.0
INCA
STA.0
SETD.0 SbfsName
CALL printString
SETD.0 SbfsName
CALL nameWidth
MVQA
CALL printSpaces
; A file's length is its block count times 256 plus its tail, which is the block count
; in the high byte and the tail in the low one. Nothing has to multiply anything.
SETD.0 SbfsFileBlocks
DPUP.0 0d01
LDA.0
SETD.1 DirSize
STA.1
SETD.0 SbfsFileTail
LDA.0
SETD.1 DirSize
INCD.1
STA.1
SETD.0 DirSize
CALL printWordDecimal
CALL newLine
BRI dirStep
dirDone:
SETD.0 DirSeen
LDA.0
CALL printByteDecimal
SETD.0 FilesText
CALL printString
CALL newLine
BRI prompt
dirNoDisk:
SETD.0 NoDisk
CALL printString
CALL newLine
BRI prompt
; DP0 names a string. Q is how many spaces pad it out to twenty four columns. A name
; already that long gets one space, so that it cannot run into the number after it.
nameWidth:
INIA 0d24
SETD.1 WidthLeft
STA.1
widthLoop:
LDA.0
BRA widthDone
SETD.1 WidthLeft
LDA.1
DECA
STA.1
BRA widthFloor
INCD.0
BRI widthLoop
widthFloor:
INIA 0d1
SETD.1 WidthLeft
STA.1
widthDone:
SETD.1 WidthLeft
LDA.1
RSTB
CCF
ADD
RET
; ---- load ----
;
; Reads a program off the disk and puts it where its header asks to go. Nothing relocates
; anything: the addresses in the header are the ones the program was built for, and it
; would not work anywhere else.
;
; The whole file is staged at 0x8000 first and then blitted into place, because where the
; pieces belong is not known until the header has been read, and the header is in the file.
doLoad:
SETD.0 DiskReady
LDA.0
BRA loadNoDisk
SETD.1 TextRest
LDD.0.1
LDA.0
BRA loadNothingNamed
CALL sbfsFind
BNQ loadMissing
SETD.1 0x80 0x00
CALL sbfsRead
BNQ loadUnreadable
; "SBEX", or this is not a program. Without this, loading a text file would put nonsense
; into Program Memory and then jump into the middle of it.
SETD.0 0x80 0x00
SETD.2 ExecMagic
INIA 0d4
SETD.1 LoadCount
STA.1
loadMagicLoop:
LDA.0
LDB.2
XOR
BNQ loadNotProgram
INCD.0
INCD.2
LDA.1
DECA
STA.1
BNA loadMagicLoop
SETD.0 0x80 0x00
DPUP.0 0d04
LDA.0
INIB 0d1
XOR
BNQ loadWrongVersion
; The code. It comes from the staging area just past the sixteen byte header, and goes
; wherever the header says, in Program Memory, which the instruction set cannot write
; and the controller can.
INIA 0d1
OUTA 0xE0 ; SourceBank: Data Memory, where the file was staged.
INIA 0x80
OUTA 0xE1
INIA 0d16
OUTA 0xE2 ; 0x8010, the first byte after the header.
RSTA
OUTA 0xE3 ; DestBank: Program Memory.
SETD.0 0x80 0x00
DPUP.0 0d06
LDA.0
OUTA 0xE4
INCD.0
LDA.0
OUTA 0xE5
SETD.0 0x80 0x00
DPUP.0 0d10
LDA.0
OUTA 0xE6
INCD.0
LDA.0
OUTA 0xE7
INIA 0x01
OUTA 0xE8 ; Blit.
; Then the data. A blit leaves its addresses past whatever it touched, so the source is
; already sitting on the first byte of the data and only the destination changes.
INIA 0d1
OUTA 0xE3 ; DestBank: Data Memory.
SETD.0 0x80 0x00
DPUP.0 0d12
LDA.0
OUTA 0xE4
INCD.0
LDA.0
OUTA 0xE5
SETD.0 0x80 0x00
DPUP.0 0d14
LDA.0
OUTA 0xE6
INCD.0
LDA.0
OUTA 0xE7
INIA 0x01
OUTA 0xE8 ; Blit.
; Where it starts. Written out by hand rather than through a routine, because a routine
; could not hand two bytes back: CALL puts A, B and the first three pointers back the
; way it found them.
SETD.0 0x80 0x00
DPUP.0 0d08
LDA.0
SETD.1 LoadedEntry
STA.1
INCD.0
INCD.1
LDA.0
STA.1
INIA 0x01
SETD.0 LoadedOk
STA.0
SETD.0 LoadedText
CALL printString
SETD.0 LoadedEntry
CALL printWordHex
CALL newLine
BRI prompt
loadNoDisk:
SETD.0 NoDisk
BRI loadComplain
loadNothingNamed:
SETD.0 LoadWhat
BRI loadComplain
loadMissing:
SETD.0 NoSuchFile
BRI loadComplain
loadUnreadable:
SETD.0 Unreadable
BRI loadComplain
loadNotProgram:
SETD.0 NotProgram
BRI loadComplain
loadWrongVersion:
SETD.0 WrongVersion
loadComplain:
CALL printString
CALL newLine
BRI prompt
; ---- run ----
;
; Hands the machine to whatever was loaded. Where the Stack is now is written down first,
; because the program is not going to unwind anything it pushes and the exit handler has
; to be able to put the Stack back.
doRun:
SETD.0 LoadedOk
LDA.0
BRA runNothing
MVSD.0
SETD.1 SystemStack
STD.0.1
; The entry address is a number until BRD makes it a place. DP3 is the one to build it
; in, because it is the pointer nothing puts back.
SETD.1 LoadedEntry
LDD.3.1
BRD.3
runNothing:
SETD.0 NothingLoaded
CALL printString
CALL newLine
BRI prompt
; ---- The services ----
;
; These are what a loaded program is allowed to ask for. The names and their numbers come
; from services.asm, which the programs include as well, so neither side writes a number
; down and the two cannot disagree about them.
;
; A handler arrives with the caller's registers exactly as they were: an interrupt frame
; is pushed, not cleared. So the pointer a program put in DP0 is still there to be used.
handlePrintString:
CALL printString
RETI
handleReadLine:
CALL readLine
RETI
; Giving the machine back. This is the one place MVDS earns its keep. The program's Stack,
; and the frame this very interrupt arrived on, are both abandoned where they lie, because
; nothing is going to return through either of them.
;
; Which is exactly why this cannot RETI. Its return address is on the Stack it just walked
; away from, so it branches to the prompt instead.
handleExit:
SETD.1 SystemStack
LDD.0.1
MVDS.0
; The console goes back to line mode whatever the program left it in. A program that
; wanted keys is expected to put it back itself, but one that stopped early, or forgot,
; would otherwise hand back a shell with no echo and no backspace, and the shell has no
; way to find out that happened. Writing line mode when it is already in line mode costs
; a byte out of a port and does nothing, which is the right price for not having to know.
RSTA
OUTA 0x02
SETD.0 Finished
CALL printString
CALL newLine
BRI prompt
; ---- dump ----
;
; dump Sixty four more bytes, carrying on from the last one.
; dump <where> From the start of that bank.
; dump <where> <addr> From there.
;
; <where> is program, data, or a bank number in hexadecimal. That the CPU cannot read
; Program Memory and this can is the whole point: the instruction set has no way to look
; at itself, and the controller does, so a monitor is possible at all only through it.
doDump:
SETD.1 TextRest
LDD.0.1
LDA.0
BRA dumpGo ; Nothing said, so carry on from where the last one stopped.
; Which bank. The two that always exist have names, because typing "program" is what
; somebody means and 0 is what the machine calls it.
CALL textSplit
SETD.1 ProgramWord
CALL textSame
BRQ dumpBankProgram
SETD.1 DataWord
CALL textSame
BRQ dumpBankData
CALL textHexWord
BNQ dumpBadWhere
SETD.0 TextValue
INCD.0
LDA.0
BRI dumpSetBank
dumpBankProgram:
RSTA
BRI dumpSetBank
dumpBankData:
INIA 0d1
dumpSetBank:
SETD.0 DumpBank
STA.0
; And where in it. Naming a bank without an address means the start of it, which is the
; only answer that does not depend on what was asked for last time.
RSTA
SETD.0 DumpAt
STA.0
INCD.0
STA.0
SETD.1 TextRest
LDD.0.1
LDA.0
BRA dumpCheckBank
CALL textHexWord
BNQ dumpBadWhere
SETD.0 TextValue
LDA.0
SETD.1 DumpAt
STA.1
SETD.0 TextValue
INCD.0
LDA.0
SETD.1 DumpAt
INCD.1
STA.1
dumpCheckBank:
; Is there such a bank? Asking the controller for a bank that is not there is refused,
; and a refusal nobody catches stops the machine, which is a poor answer to a typing
; mistake. The bank table says what exists, and it lives in bank 2.
;
; Bank n's record starts at n times eight. A and B are a shift register sixteen bits
; wide, so putting the number in the low half and rotating left three times multiplies
; it by eight without anything falling off the top: the most it can reach is 2040.
RSTA
SETD.0 DumpBank
LDB.0
SHL SHL SHL
SETD.0 DumpRecord
STA.0
INCD.0
STB.0
INIA 0d2
OUTA 0xE0 ; SourceBank: the controller's own memory.
SETD.0 DumpRecord
LDA.0
OUTA 0xE1
INCD.0
LDA.0
OUTA 0xE2
INA 0xE9 ; The flags byte of that bank's record.
INIB 0x01
AND
BRQ dumpNoBank ; The present bit is down, so nothing is there.
dumpGo:
INIA 0d4
SETD.0 DumpRows
STA.0
dumpRow:
SETD.0 DumpAt
CALL printWordHex
INIA 0d2
CALL printSpaces
; Point the controller at the row. Reading the Data port takes a byte and steps the
; source on, so the whole row is one instruction repeated.
SETD.0 DumpBank
LDA.0
OUTA 0xE0
SETD.0 DumpAt
LDA.0
OUTA 0xE1
INCD.0
LDA.0
OUTA 0xE2
; Sixteen bytes, kept as they go past so that they can be shown twice.
INIA 0d16
SETD.0 DumpCount
STA.0
SETD.1 DumpBytes
dumpByte:
INA 0xE9
STA.1
CALL printByteHex
INIA 0x20
OUTA 0x00
INCD.1
SETD.0 DumpCount
LDA.0
DECA
STA.0
BNA dumpByte
; The same sixteen again, as characters. Anything that is not printable shows as a dot,
; because a control character sent to the console would move the cursor and ruin the
; shape of the dump.
INIA 0x20
OUTA 0x00
INIA 0d16
SETD.0 DumpCount
STA.0
SETD.1 DumpBytes
dumpChar:
LDA.1
INIB 0x20
CCF
SUB
BRC dumpDot ; Below a space.
INIB 0x7F
CCF
SUB
BNC dumpDot ; Delete, or above it.
OUTA 0x00
BRI dumpCharNext
dumpDot:
INIA 0x2E
OUTA 0x00
dumpCharNext:
INCD.1
SETD.0 DumpCount
LDA.0
DECA
STA.0
BNA dumpChar
CALL newLine
; Sixteen further along, carrying into the high byte if the low one wrapped.
SETD.0 DumpAt
INCD.0
LDA.0
INIB 0d16
CCF
ADD
STQ.0
BNC dumpRowNext
SETD.0 DumpAt
LDA.0
INCA
STA.0
dumpRowNext:
SETD.0 DumpRows
LDA.0
DECA
STA.0
BNA dumpRow
BRI prompt
dumpBadWhere:
SETD.0 DumpUsage
CALL printString
CALL newLine
BRI prompt
dumpNoBank:
SETD.0 NoSuchBank
CALL printString
CALL newLine
BRI prompt
; ---- help ----
doHelp:
SETD.0 HelpText
CALL printString
CALL newLine
SETD.0 HelpMoreText
CALL printString
CALL newLine
BRI prompt
#Data
Banner:
"CosmOS"
PromptText:
"> "
NoDisk:
"no filesystem on the disk"
Unknown:
"I do not know: "
Farewell:
"halted"
FilesText:
" files"
; Two strings rather than one, because a string literal stops at 255 characters and each
; one carries its own zero byte, so they are printed in turn rather than joined.
HelpText:
"dir list what is on the disk
load <file> read a program off the disk
run start what was loaded"
HelpMoreText:
"dump sixty four bytes of memory, and again for more
dump <program|data|bank> <address>
help this
exit stop"
DumpUsage:
"dump <program|data|bank> <address>"
NoSuchBank:
"there is no such bank"
ProgramWord:
"program"
DataWord:
"data"
DumpName:
"dump"
ExecMagic:
"SBEX"
LoadWhat:
"load what?"
NoSuchFile:
"no such file"
Unreadable:
"could not read it"
NotProgram:
"not a program"
WrongVersion:
"a version I do not know"
LoadedText:
"loaded, starting at "
NothingLoaded:
"nothing is loaded"
Finished:
"finished"
DirName:
"dir"
LoadName:
"load"
RunName:
"run"
HelpName:
"help"
ExitName:
"exit"
DiskReady:
0x00
LoadedOk:
0x00
LoadedEntry:
0x00 0x00
LoadCount:
0x00
; Where the monitor is looking, so that a bare 'dump' can carry on from it.
DumpBank:
0x00
DumpAt:
0x00 0x00
DumpRows:
0x00
DumpCount:
0x00
DumpRecord:
0x00 0x00
DumpBytes:
#Reserve 0d16
; Where the system's Stack was when it handed the machine to a program. Kept below the
; region a program owns, so that a program has to go looking to break it.
SystemStack:
0x00 0x00
DirSeen:
0x00
DirSize:
0x00 0x00
WidthLeft:
0x00
; Sixty three characters and the zero byte that ends them.
CommandLine:
#Reserve 0d64
#Vectors
Boot boot
osPrintString handlePrintString
osReadLine handleReadLine
osExit handleExit
@@ -1,6 +1,10 @@
; sbfs.asm
; Reading the SplitBit Filesystem.
;
; This belongs to CosmOS, which is the thing that needs it. Programs outside CosmOS may
; include it, and the test programs do, but when CosmOS becomes a repository of its own
; this file goes with it and anything left behind takes a copy.
;
; The other implementation of this format is SplitDisk on the host. Nothing is shared
; between them but the written specification, so anything that changes here has to change
; there in the same breath.
@@ -182,6 +186,167 @@ sbfsFindFound:
ADD ; Q is zero: found.
RET
; ---- Walking the directory ----
;
; sbfsFind answers a question about one name. Listing what is on a disk is a different
; job: it needs the directory walked rather than searched. sbfsFirst starts a walk and
; sbfsNext steps it, and each of them stops on an entry that is in use, skipping the free
; ones on the way past.
;
; Q is zero while there is an entry to look at. When it is, SbfsName holds the name with
; a zero byte after it, and SbfsFileStart, SbfsFileBlocks and SbfsFileTail describe the
; file exactly the way sbfsFind leaves them. Q is something else when the directory is
; finished, and also when a block could not be read, which are the same answer to the
; question "is there another one" and different answers to "why not".
;
; A walk keeps a directory block in SbfsBuffer between calls, so anything else that goes
; to the disk in the middle of one ends it. Take what is wanted out of an entry before
; asking the disk for anything else.
sbfsFirst:
SETD.0 SbfsDirStart
SETD.1 SbfsWalkBlock
CALL sbfsCopyWord
SETD.0 SbfsDirBlocks
DPUP.0 0d01
LDA.0
SETD.1 SbfsWalkLeft
STA.1 ; Only the low byte, the same as sbfsFind.
; Nothing is loaded yet. Saying the block in hand is empty makes the scan fetch the
; first one, so there is one way into a block rather than two.
RSTA
SETD.0 SbfsWalkCount
STA.0
BRI sbfsWalkScan
sbfsNext:
; The pointer and the count were stepped when the last entry was handed out, so there
; is nothing to do here but carry on looking.
BRI sbfsWalkScan
sbfsWalkScan:
; Anything left in the block in hand?
SETD.0 SbfsWalkCount
LDA.0
BNA sbfsWalkEntry
; No. Take the next directory block, if the directory has one.
SETD.0 SbfsWalkLeft
LDA.0
BRA sbfsWalkEnd
DECA
STA.0
SETD.0 SbfsWalkBlock
SETD.1 SbfsBlock
CALL sbfsCopyWord
CALL sbfsReadBlock
BRQ sbfsWalkLoaded
RET ; The read failed, and Q says so.
sbfsWalkLoaded:
SETD.1 SbfsBuffer
CALL sbfsBufferOut
; That one is in hand now, so the walk's block number moves on to the following one.
SETD.0 SbfsWalkBlock
CALL sbfsStepWord
; Eight entries to a block, starting at the top of the buffer.
SETD.0 SbfsBuffer
SETD.1 SbfsWalkPointer
STD.0.1
INIA 0d8
SETD.0 SbfsWalkCount
STA.0
BRI sbfsWalkScan
sbfsWalkEntry:
; Where this entry is. Kept in memory rather than in DP3, so that a walk does not
; quietly take the one pointer a caller can carry things across a call in.
SETD.1 SbfsWalkPointer
LDD.2.1
SETD.1 SbfsWalkAt
STD.2.1
; Step past it now, so that the walk has moved on whether this one is taken or skipped.
; Getting that wrong in only one of the two paths is how a walk repeats an entry
; forever, and it repeats the interesting ones rather than the boring ones.
SETD.0 SbfsWalkCount
LDA.0
DECA
STA.0
PSHD.2
POPD.0
DPUP.0 0d32
SETD.1 SbfsWalkPointer
STD.0.1
; Is it a file? A free entry is not one, and there can be free entries in the middle of
; a directory, so this is a skip rather than a stop.
LDA.2
INIB 0x01
AND
BRQ sbfsWalkScan
CALL sbfsWalkTake
RSTA
RSTB
CCF
ADD ; Q is zero: here is an entry.
RET
sbfsWalkEnd:
RSTA
INIB 0d1
CCF
ADD ; Q is one: the directory is finished.
RET
; Takes the pieces out of the entry the walk stopped on. Everything lands in memory,
; because that is the only place a subroutine can leave anything.
sbfsWalkTake:
SETD.1 SbfsWalkAt
LDD.0.1
DPUP.0 0d01
SETD.1 SbfsFileStart
CALL sbfsCopyWord
SETD.1 SbfsWalkAt
LDD.0.1
DPUP.0 0d03
SETD.1 SbfsFileBlocks
CALL sbfsCopyWord
SETD.1 SbfsWalkAt
LDD.0.1
DPUP.0 0d05
LDA.0
SETD.1 SbfsFileTail
STA.1
; The name. Twenty two bytes of it, padded with zeroes rather than terminated, so it is
; copied into a buffer with a twenty third byte that nothing ever writes. That byte is
; what makes a name that fills the field into a string that can be printed.
SETD.1 SbfsWalkAt
LDD.0.1
DPUP.0 0d06
SETD.1 SbfsName
INIA 0d22
SETD.2 SbfsCount
STA.2
sbfsWalkName:
LDA.0
STA.1
INCD.0
INCD.1
SETD.2 SbfsCount
LDA.2
DECA
STA.2
BNA sbfsWalkName
RET
; Compares the name in the entry at DP2 with the one kept in SbfsWanted. Q is zero if
; they are the same. Names are padded with zeroes rather than terminated, so a name that
; fills the field has no terminator to look for, which is why the count is what stops it.
@@ -864,6 +1029,24 @@ SbfsCount:
SbfsLeft:
0x00
; ---- What a walk through the directory keeps between calls ----
SbfsWalkBlock:
0x00 0x00
SbfsWalkLeft:
0x00
SbfsWalkCount:
0x00
SbfsWalkPointer:
0x00 0x00
SbfsWalkAt:
0x00 0x00
; Twenty three bytes for a name of twenty two, so that the last one is a zero nothing
; ever writes over and the name is always a string.
SbfsName:
#Reserve 0d23
SbfsWanted:
#Reserve 0d22
+19
View File
@@ -0,0 +1,19 @@
; The services the system offers, named and numbered.
;
; Both sides include this. The system follows it with handlers for the ones it implements.
; A program that only calls them includes this and nothing else, and can then say them by
; name, because a line with a name and nothing after it declares what a vector is called
; and what number it has without claiming to implement it.
;
; The order here is what fixes the numbers, and it is fixed in one file, so the two sides
; cannot disagree about them and nobody has to write a number down. Adding a service goes
; at the end: putting one in the middle would renumber everything after it, and any
; program already assembled against the old numbers would call the wrong thing.
;
; Written by Anachronaut
#Vectors
osPrintString ; DP0 names a string. Prints it.
osReadLine ; DP0 names somewhere to put a line read from the console.
osExit ; Give the machine back to the system.
+230
View File
@@ -0,0 +1,230 @@
; text.asm
; Picking a line of typing apart.
;
; A shell reads a line and has to decide what was asked for. That is two jobs: cutting the
; first word off the line, and telling whether a word is the one being looked for. There
; is nothing else here, because there is nothing else a command line needs yet.
;
; Written by Anachronaut
#Program
; DP0 names a line ending in a zero byte. Cuts the first word off it, in place, by writing
; a zero byte over the space that ends the word. DP0 is unchanged, because a RET puts it
; back, so afterwards DP0 names just the first word.
;
; Where the rest of the line begins goes in TextRest, with any spaces between skipped. A
; line with only one word on it leaves TextRest naming that line's zero byte, which reads
; as an empty argument rather than as a missing one, and is the same thing here.
textSplit:
LDA.0
BRA textSplitHere ; The line ended, so the whole of it was one word.
INIB 0x20
CCF
SUB
BRQ textSplitCut
INCD.0
BRI textSplit
textSplitCut:
RSTA
STA.0 ; The space becomes the end of the word.
INCD.0
textSplitSkip:
LDA.0
BRA textSplitHere
INIB 0x20
CCF
SUB
BNQ textSplitHere ; Something that is not a space: the rest starts here.
INCD.0
BRI textSplitSkip
textSplitHere:
SETD.1 TextRest
STD.0.1
RET
; DP0 and DP1 name strings ending in zero bytes. Q is zero if they are the same.
;
; The two ending together is what makes them the same. Comparing until one of them ends
; would call "dir" and "dirty" the same word, which is the kind of thing a shell gets
; wrong once and confusingly.
textSame:
LDA.0
LDB.1
CCF
SUB
BNQ textDiffer
LDA.0
BRA textAlike ; Equal, and both of them zero: they ended together.
INCD.0
INCD.1
BRI textSame
textAlike:
RSTA
RSTB
CCF
ADD ; Q is zero: the same.
RET
textDiffer:
RSTA
INIB 0d1
CCF
ADD ; Q is one: not the same.
RET
; DP0 names text. Reads hexadecimal digits off the front of it into TextValue, most
; significant byte first. Q is zero if there was at least one digit to read.
;
; Digits past the fourth push the earlier ones off the top rather than being refused,
; which is what typing over an address does on every monitor there has ever been.
textHexWord:
RSTA
SETD.1 TextValue
STA.1
INCD.1
STA.1
SETD.1 TextDigits
STA.1
textHexLoop:
LDA.0
CALL textHexDigit
PSHQ
POPA
INIB 0xFF
CCF
SUB
BRQ textHexEnd ; Not a digit, so the number stopped before it.
CALL textHexShift
SETD.1 TextDigits
LDA.1
INCA
STA.1
INCD.0
BRI textHexLoop
textHexEnd:
SETD.1 TextDigits
LDA.1
BRA textHexNothing
RSTA
RSTB
CCF
ADD ; Q is zero: there was a number.
RET
textHexNothing:
RSTA
INIB 0d1
CCF
ADD ; Q is one: there was not.
RET
; A holds the digit just read. Moves TextValue up by one place and puts the digit in the
; hole that leaves.
;
; A and B are a circular shift register sixteen bits long, so rotating them left four
; times multiplies the pair by sixteen. What fell off the top of the high byte comes round
; into the bottom of the low one, which is exactly the nybble the new digit wants, so it
; is masked away first.
textHexShift:
SETD.1 TextDigit
STA.1
SETD.0 TextValue
LDA.0
INCD.0
LDB.0
SHL SHL SHL SHL
SETD.0 TextValue
STA.0 ; The high byte is finished.
PSHB
POPA
INIB 0xF0
AND
MVQA
SETD.1 TextDigit
LDB.1
OR
SETD.0 TextValue
INCD.0
STQ.0
RET
; A holds a character. Q is what it is worth as a hexadecimal digit, or 0xFF if it is not
; one. Upper and lower case both count, because nobody wants to be told which they meant.
;
; Everything below works from the distance above '0', which is why the letters are tested
; at seventeen and thirty two rather than at anything recognisable.
textHexDigit:
INIB 0x30
CCF
SUB
BRC textHexNo ; Below '0'.
MVQA
INIB 0d10
CCF
SUB
BNC textHexUpper ; Ten or more above '0', so not 0 to 9.
RSTB
CCF
ADD ; Q is the digit itself.
RET
textHexUpper:
INIB 0d17
CCF
SUB
BRC textHexNo ; Between '9' and 'A'.
MVQA
INIB 0d6
CCF
SUB
BNC textHexLower ; Past 'F'.
INIB 0d10
CCF
ADD
RET
textHexLower:
INIB 0d32
CCF
SUB
BRC textHexNo ; Between 'F' and 'a'.
MVQA
INIB 0d6
CCF
SUB
BNC textHexNo ; Past 'f'.
INIB 0d10
CCF
ADD
RET
textHexNo:
RSTA
INIB 0xFF
CCF
ADD
RET
#Data
; Where the rest of the line begins, after textSplit has taken a word off the front.
TextRest:
0x00 0x00
; What textHexWord read, and what it needs while reading it.
TextValue:
0x00 0x00
TextDigits:
0x00
TextDigit:
0x00
Binary file not shown.
+7 -7
View File
@@ -1,16 +1,16 @@
; A program meant to be loaded off a disk rather than booted from.
;
; It is assembled for where it will live. Reserving the front of each segment puts the
; first thing in it at a known address, and everything after that follows, so the labels
; inside are already right for the place the loader will put it. Nothing relocates
; anything: this works because the addresses here and the addresses in its header say the
; same thing.
; It is assembled for where it will live. Giving each segment a base says where it goes,
; so the labels inside are already right for the place the loader will put it, and the
; assembler writes it out as a loadable program rather than as a boot image. Nothing
; relocates anything: this works because the addresses here and the addresses in its
; header say the same thing.
;
; The loader keeps below both of these, which is the whole of the arrangement.
#Program
#Reserve 0x2000 ; This program's code lives from 0x2000.
#Base 0x2000 ; This program's code lives from 0x2000.
hello:
SETD.0 HelloText
@@ -27,7 +27,7 @@ helloDone:
#Data
#Reserve 0x1000 ; And its data from 0x1000.
#Base 0x1000 ; And its data from 0x1000.
HelloText:
"loaded off a disk, with a string and a loop of its own"
+40 -2
View File
@@ -13,12 +13,14 @@ EMU ?= ../SplitBit
BUILD ?= build
# Libraries are included by bare name, so the assembler is told where to find them.
INCLUDES = -I Libraries
# CosmOS owns the filesystem library and the service names, so it is a place to look too.
INCLUDES = -I Libraries -I CosmOS/Source
# The programs worth building. Files in Libraries/ are left out because they have no
# entry point of their own, and the ones in testPrograms/ are covered by 'make test'
# in the parent directory.
PROGRAMS = \
CosmOS/Source/cosmos.asm \
hello.asm \
printHello.asm \
inputTest.asm \
@@ -46,6 +48,42 @@ $(BUILD)/%.bin: %.asm
run-%: $(BUILD)/%.bin
$(EMU) $<
# ---- CosmOS ----
#
# make cosmos Assemble the system and everything it can load.
# make cosmos-disk ... and put the loadable programs on a disk image.
# make run-cosmos ... and boot the machine with that disk in the drive.
#
# Programs in Apps/ say where they live with #Base, so the assembler writes them out as
# loadable programs rather than as boot images. They are named .sbx to keep that
# difference visible: a .bin is something the machine boots, a .sbx is something a
# running system loads.
DISKTOOL ?= ../SplitDisk
COSMOS = $(BUILD)/CosmOS/Source/cosmos.bin
APPS = $(patsubst CosmOS/Apps/%.asm,$(BUILD)/CosmOS/Apps/%.sbx,$(wildcard CosmOS/Apps/*.asm))
COSMOS_DISK = $(BUILD)/cosmos.img
DEPENDENCIES += $(APPS:.sbx=.d)
$(BUILD)/CosmOS/Apps/%.sbx: CosmOS/Apps/%.asm
@mkdir -p $(@D)
$(ASM) $(INCLUDES) -M $(@:.sbx=.d) -o $@ $<
cosmos: $(COSMOS) $(APPS)
# Made from scratch every time, so that what is on it is what is in Apps/ now and not
# also whatever used to be.
$(COSMOS_DISK): $(APPS)
@mkdir -p $(@D)
rm -f $@
$(DISKTOOL) format $@ 64 2
@for app in $(APPS); do $(DISKTOOL) put $@ $$app; done
cosmos-disk: $(COSMOS_DISK)
run-cosmos: $(COSMOS) $(COSMOS_DISK)
$(EMU) --disk $(COSMOS_DISK) $(COSMOS)
clean:
rm -rf $(BUILD)
@@ -53,4 +91,4 @@ clean:
# reassembles every program that includes it.
-include $(DEPENDENCIES)
.PHONY: all clean
.PHONY: all clean cosmos cosmos-disk run-cosmos
+140
View File
@@ -0,0 +1,140 @@
; Exercises the console's status and control ports.
;
; The console answers on three ports now: 0x00 for bytes as it always did, 0x01 for what
; it is doing, and 0x02 to say which mode it should be in. The point of the mode is that
; the data port never changed, so this checks the old behaviour is still there as much as
; it checks the new behaviour arrived.
;
; What the status bits mean:
; bit 0 READY reading the data port will not have to wait
; bit 1 ENDED input has run out for good
; bit 2 KEYMODE the console is in key mode
;
; This runs with input from a file rather than a terminal, so key mode has no terminal to
; put into another state and the mode bit is the only thing that changes. That is on
; purpose: the same program has to work either way, and a test that needed a terminal
; could not run here at all.
#Include console.asm
#Program
start:
SETD.0 Banner
CALL printString
CALL newLine
; ---- What mode does it start in ----
;
; Nothing has been written to the control port, so this is how the machine comes up, and
; every program written before these ports existed runs in exactly this.
SETD.0 AtStartLabel
CALL printString
CALL showStatus
; ---- Into key mode ----
INIA 0x01
OUTA 0x02
SETD.0 KeyModeLabel
CALL printString
CALL showStatus
; ---- Read what is waiting ----
;
; Reading until ENDED rather than until a count, which is the thing that could not be
; done before: 0xFF from the data port used to mean both "the byte 0xFF" and "there is
; no more", and nothing could tell those apart.
SETD.0 ReadLabel
CALL printString
CALL newLine
readLoop:
INA 0x01
INIB 0x02 ; ENDED
AND
BNQ readDone
INA 0x00
OUTA 0x00 ; Nothing echoes in key mode, so the program does it.
BRI readLoop
readDone:
CALL newLine
; ---- The status at the end of input ----
;
; READY is set as well as ENDED, because a read does answer at once. It just answers
; 0xFF forever. ENDED is what says so.
SETD.0 AtEndLabel
CALL printString
CALL showStatus
; ---- Back to line mode ----
RSTA
OUTA 0x02
SETD.0 LineModeLabel
CALL printString
CALL showStatus
HALT
; Prints the status byte as hex and then names the bits that are up, so that a change in
; the output says which bit moved rather than only that the number is different.
showStatus:
INA 0x01
PSHA
CALL printByteHex
INIA 0x20
OUTA 0x00
POPA
PSHA
INIB 0x01
AND
BRQ showNotReady
SETD.0 ReadyWord
CALL printString
showNotReady:
POPA
PSHA
INIB 0x02
AND
BRQ showNotEnded
SETD.0 EndedWord
CALL printString
showNotEnded:
POPA
INIB 0x04
AND
BRQ showNotKeys
SETD.0 KeysWord
CALL printString
showNotKeys:
CALL newLine
RET
#Data
Banner:
"console mode ports"
AtStartLabel:
"at start: "
KeyModeLabel:
"key mode: "
AtEndLabel:
"at the end: "
LineModeLabel:
"line mode: "
ReadLabel:
"what came in:"
ReadyWord:
"ready "
EndedWord:
"ended "
KeysWord:
"keys "
#Vectors
Boot start
+201
View File
@@ -0,0 +1,201 @@
; Exercises console.asm, the modern console library.
;
; Every routine in it is called here with the cases that are easy to get wrong: a zero,
; the largest thing that fits, the boundary either side of a carry, and a leading zero
; that should not print. The reading half is driven by consoleTest.in, which ends with a
; line longer than the buffer so that the truncation and the swallowing of the rest are
; both shown rather than assumed.
;
; There is no BRI at the top of console.asm, so this program says where it begins in its
; Vector Segment. That is the whole of the arrangement the old library needed a branch for.
#Include console.asm
#Program
start:
SETD.0 Banner
CALL printString
CALL newLine
; ---- Hexadecimal ----
SETD.0 HexLabel
CALL printString
RSTA
CALL printByteHex
INIA 0x20
OUTA 0x00
INIA 0x0F
CALL printByteHex
INIA 0x20
OUTA 0x00
INIA 0xA5
CALL printByteHex
INIA 0x20
OUTA 0x00
INIA 0xFF
CALL printByteHex
CALL newLine
SETD.0 WordHexLabel
CALL printString
SETD.0 Zero
CALL printWordHex
INIA 0x20
OUTA 0x00
SETD.0 Thousand
CALL printWordHex
INIA 0x20
OUTA 0x00
SETD.0 Largest
CALL printWordHex
CALL newLine
; ---- Decimal, one byte wide ----
; Zero has to print a digit, and a hundred has to not print its tens as a blank.
SETD.0 ByteLabel
CALL printString
RSTA
CALL printByteDecimal
CALL blank
INIA 0d7
CALL printByteDecimal
CALL blank
INIA 0d42
CALL printByteDecimal
CALL blank
INIA 0d100
CALL printByteDecimal
CALL blank
INIA 0d255
CALL printByteDecimal
CALL newLine
; ---- Decimal, two bytes wide ----
; Ten thousand and one is the case that catches a place that stops counting too soon,
; and the three zeroes in the middle of it are the case that catches a leading zero
; test applied after the first digit.
SETD.0 WordLabel
CALL printString
SETD.0 Zero
CALL printWordDecimal
CALL blank
SETD.0 Nine
CALL printWordDecimal
CALL blank
SETD.0 Ten
CALL printWordDecimal
CALL blank
SETD.0 TwoFiveFive
CALL printWordDecimal
CALL blank
SETD.0 Thousand
CALL printWordDecimal
CALL blank
SETD.0 TenThousandOne
CALL printWordDecimal
CALL blank
SETD.0 Largest
CALL printWordDecimal
CALL newLine
; ---- Spaces ----
; None of them is a fair number to ask for, and has to print nothing at all.
SETD.0 SpaceLabel
CALL printString
INIA 0x7C
OUTA 0x00
RSTA
CALL printSpaces
INIA 0x7C
OUTA 0x00
INIA 0d4
CALL printSpaces
INIA 0x7C
OUTA 0x00
CALL newLine
; ---- Reading ----
SETD.0 ReadLabel
CALL printString
CALL newLine
readLoop:
SETD.0 LineBuffer
INIB 0d16
CALL readLine
SETD.0 LineLength
STQ.0
; Running out is not an empty line, and this is where the difference shows.
SETD.0 ConsoleEndOfInput
LDA.0
BNA readDone
INIA 0x5B
OUTA 0x00
SETD.0 LineBuffer
CALL printString
INIA 0x5D
OUTA 0x00
CALL blank
SETD.0 LineLength
LDA.0
CALL printByteDecimal
CALL newLine
BRI readLoop
readDone:
SETD.0 Ended
CALL printString
CALL newLine
HALT
; One space. Used between the numbers so that each line reads as a list.
blank:
INIA 0x20
OUTA 0x00
RET
#Data
Banner:
"console test"
HexLabel:
"byte hex: "
WordHexLabel:
"word hex: "
ByteLabel:
"byte dec: "
WordLabel:
"word dec: "
SpaceLabel:
"spaces: "
ReadLabel:
"lines read:"
Ended:
"end of input"
Zero:
0x00 0x00
Nine:
0x00 0x09
Ten:
0x00 0x0A
TwoFiveFive:
0x00 0xFF
Thousand:
0x03 0xE8
TenThousandOne:
0x27 0x11
Largest:
0xFF 0xFF
LineLength:
0x00
LineBuffer:
#Reserve 0d17
#Vectors
Boot start
@@ -0,0 +1,22 @@
; A program that bases one segment and forgets the other.
;
; This is the shape of a real bug: the original Fib-8, brought over to CosmOS as an
; application, based its code at 0x2000 but got its Data Segment from an included library
; and never based it. The three bytes went to 0x0000, on top of the console's own
; variables, and it appeared to work because those three bytes are scratch that readLine
; rewrites before every use. A larger data segment would have reached the decimal table
; and the mounted filesystem behind it, and failed somewhere else entirely.
;
; Nothing relocates, so the assembler is the last place to catch it.
#Program
#Base 0x2000
start:
HALT
#Data
value:
0x00
+132
View File
@@ -0,0 +1,132 @@
; Walking the directory of a SplitBit filesystem.
;
; sbfsFind answers a question about one name. This is the other thing a directory is for:
; being listed. The disk was made by SplitDisk, so the order and contents here are what
; the other implementation of the format wrote, not what this one assumed.
;
; The cases that matter are all on this disk already. There are twelve files, which is
; more than the eight an entry block holds, so the walk has to cross from the first
; directory block into the second. aName22CharactersLong! fills the name field exactly and
; so has no zero byte to end it, which is what the twenty third byte of SbfsName is for.
; empty.txt has no blocks at all.
;
; The count at the end is the proof that the walk stopped where the directory did. A walk
; that stepped its pointer only when it took an entry would loop forever on a free one,
; and one that stepped only when it skipped would hand out every eighth file.
;
; Correct output is the twelve files with their sizes, then how many there were.
#Include console.asm
#Include sbfs.asm
#Program
start:
CALL sbfsMount
BRQ mounted
SETD.0 NoMount
CALL printString
CALL newLine
HALT
mounted:
; Nothing found yet.
RSTA
SETD.0 Seen
STA.0
CALL sbfsFirst
BRI walkCheck
walkStep:
CALL sbfsNext
walkCheck:
BNQ walkDone
SETD.0 Seen
LDA.0
INCA
STA.0
SETD.0 SbfsName
CALL printString
; Line the sizes up, so a name that fills the field and a short one both read cleanly.
SETD.0 SbfsName
CALL nameWidth
MVQA
CALL printSpaces
; A file is its blocks times 256 plus its tail, which is the block count in the high
; byte and the tail in the low one. Nothing has to multiply anything.
SETD.0 SbfsFileBlocks
DPUP.0 0d01
LDA.0
SETD.1 Size
STA.1
SETD.0 SbfsFileTail
LDA.0
SETD.1 Size
INCD.1
STA.1
SETD.0 Size
CALL printWordDecimal
CALL newLine
BRI walkStep
walkDone:
SETD.0 Total
CALL printString
SETD.0 Seen
LDA.0
CALL printByteDecimal
CALL newLine
HALT
; DP0 names a string. Q is how many spaces would pad it out to twenty four characters.
; A name that is already that long gets one space, because none at all would run the
; name into the number.
nameWidth:
INIA 0d24
SETD.1 Padding
STA.1
widthLoop:
LDA.0
BRA widthDone
SETD.1 Padding
LDA.1
DECA
STA.1
BRA widthFloor ; It is already too long to pad.
INCD.0
BRI widthLoop
widthFloor:
INIA 0d1
SETD.1 Padding
STA.1
widthDone:
SETD.1 Padding
LDA.1
RSTB
CCF
ADD
RET
#Data
NoMount:
"no filesystem on that disk"
Total:
"files: "
Seen:
0x00
Padding:
0x00
Size:
0x00 0x00
#Vectors
Boot start
+210
View File
@@ -0,0 +1,210 @@
; Tests MVDS, which copies a Data Pointer into the Stack Pointer.
;
; This is the dangerous one. Moving the Stack Pointer abandons everything below the new
; position: return addresses, saved registers, interrupt frames, all of it. Nothing is
; unwound, because moving the Stack does not move what is on it.
;
; It is here for one job, and that job is what this program acts out. A system that runs
; other programs and takes the machine back afterwards cannot do so without it. A program
; that gives up part way through leaves whatever it pushed behind, and nothing is ever
; going to return and tidy it away. Without MVDS the Stack only ever moves downward, a
; little further with every program run, and a shell cannot outlive many of them.
;
; The other half of the story is that moving the Stack does not destroy what was on it.
; A routine that puts the Stack Pointer back where it found it returns perfectly normally,
; because the frame was only stepped away from. Both halves are acted out here, and the
; nesting case with them: two routines that each borrow a Stack, one inside the other,
; each keeping its own saved Stack Pointer on the Stack it borrowed. A fixed location in
; Data Memory would fail exactly there, and would fail silently.
;
; Both sequences are the ones the Programming Manual prints under "The Stack Pointer,
; Set By Hand", so if either example ever stops working this test says so.
;
; Correct output is:
; stack reclaim test
; at start: FFFF
; spent: FFF7
; reclaimed: FFFF
; still good: 33
; borrowed: 7 and 9
; back home: FFFF
#Include console.asm
#Program
start:
SETD.0 Banner
CALL printString
CALL newLine
; Where the Stack is before anything has touched it. Read first, so that no call has
; had a chance to move it.
MVSD.0
SETD.1 SavedStack
STD.0.1
SETD.0 StartLabel
CALL printString
SETD.0 SavedStack
CALL printWordHex
CALL newLine
; Spend some Stack and never give it back. This is not a subroutine, because a
; subroutine that pushed without popping would take its own return address with it.
; It stands in for a program that stops half way through.
INIA 0x11
PSHA PSHA PSHA PSHA
PSHA PSHA PSHA PSHA
MVSD.0
SETD.1 Spent
STD.0.1
SETD.0 SpentLabel
CALL printString
SETD.0 Spent
CALL printWordHex
CALL newLine
; Take it back. Nothing pops those eight bytes: they are simply no longer on the Stack,
; which is the whole of what reclaiming means.
SETD.1 SavedStack
LDD.0.1
MVDS.0
MVSD.0
SETD.1 Reclaimed
STD.0.1
SETD.0 ReclaimedLabel
CALL printString
SETD.0 Reclaimed
CALL printWordHex
CALL newLine
; A Stack Pointer in the right place is not proof that the Stack works. Push something
; through it and get it back, so that the reclaimed Stack is shown to be usable and not
; merely well positioned.
SETD.0 GoodLabel
CALL printString
INIA 0d33
PSHA
RSTA
POPA
CALL printByteDecimal
CALL newLine
; ---- Moving the Stack and coming back from it ----
; borrower runs on a Stack of its own and returns with a plain RET, which only works
; because putting the Stack Pointer back leaves the frame exactly as CALL left it.
CALL borrower
SETD.0 BorrowedLabel
CALL printString
SETD.0 OuterResult
LDA.0
CALL printByteDecimal
SETD.0 AndText
CALL printString
SETD.0 InnerResult
LDA.0
CALL printByteDecimal
CALL newLine
; The Stack has to be back where it started, or the nesting quietly lost track of it.
MVSD.0
SETD.1 Reclaimed
STD.0.1
SETD.0 HomeLabel
CALL printString
SETD.0 Reclaimed
CALL printWordHex
CALL newLine
HALT
; Borrows a Stack, works on it, calls something that does the same thing again, then puts
; the Stack back and returns. The old Stack Pointer goes onto the borrowed Stack rather
; than into a fixed place in memory, which is what makes the nesting safe: the inner
; routine keeps its own copy on its own Stack and cannot tread on this one.
borrower:
MVSD.3
SETD.0 OuterTop
MVDS.0
PSHD.3
INIA 0d7
PSHA
RSTA
POPA
SETD.1 OuterResult
STA.1
CALL innerBorrower
POPD.3
MVDS.3
RET
; The same again, one level down and on a Stack of its own.
innerBorrower:
MVSD.3
SETD.0 InnerTop
MVDS.0
PSHD.3
INIA 0d9
PSHA
RSTA
POPA
SETD.1 InnerResult
STA.1
POPD.3
MVDS.3
RET
#Data
Banner:
"stack reclaim test"
StartLabel:
"at start: "
SpentLabel:
"spent: "
ReclaimedLabel:
"reclaimed: "
GoodLabel:
"still good: "
BorrowedLabel:
"borrowed: "
AndText:
" and "
HomeLabel:
"back home: "
SavedStack:
0x00 0x00
Spent:
0x00 0x00
Reclaimed:
0x00 0x00
OuterResult:
0x00
InnerResult:
0x00
; Two places a Stack can live, sixty four bytes each. The label is on the last byte of
; each, because a Stack grows downward from wherever it is set.
OuterStack:
#Reserve 0d63
OuterTop:
0x00
InnerStack:
#Reserve 0d63
InnerTop:
0x00
#Vectors
Boot start
+204
View File
@@ -0,0 +1,204 @@
; Exercises text.asm, which is how CosmOS understands what was typed at it.
;
; The cases here are the ones that decide whether a shell feels right or feels broken.
; textSame has to say that "dir" and "dirty" are different words, or every command would
; match its own prefix. textSplit has to give an empty rest rather than a missing one when
; there is no argument, so that "load" with nothing after it is a question the shell can
; answer. And textHexWord has to take upper and lower case alike, refuse things that only
; look like numbers, and push digits off the top rather than refusing a fifth one.
;
; Correct output is:
; split: [dump] [program 2000]
; split: [dir] []
; same: yes no no no
; hex: 2000 00FF 00FF BEEF FFFF 0000
; 0 is a fine way to begin a string
; no number here
#Include console.asm
#Include text.asm
#Program
start:
; ---- Splitting ----
SETD.0 LineOne
CALL textSplit
SETD.0 SplitLabel
CALL printString
INIA 0x5B
OUTA 0x00
SETD.0 LineOne
CALL printString
INIA 0x5D
OUTA 0x00
CALL blank
INIA 0x5B
OUTA 0x00
SETD.1 TextRest
LDD.0.1
CALL printString
INIA 0x5D
OUTA 0x00
CALL newLine
; One word on its own. The rest has to be empty rather than absent.
SETD.0 LineTwo
CALL textSplit
SETD.0 SplitLabel
CALL printString
INIA 0x5B
OUTA 0x00
SETD.0 LineTwo
CALL printString
INIA 0x5D
OUTA 0x00
CALL blank
INIA 0x5B
OUTA 0x00
SETD.1 TextRest
LDD.0.1
CALL printString
INIA 0x5D
OUTA 0x00
CALL newLine
; ---- Comparing ----
; The same, then longer, then shorter, then different. The middle two are what a
; comparison that stopped at the first ending would get wrong.
SETD.0 SameLabel
CALL printString
SETD.0 WordDir
SETD.1 WordDir2
CALL textSame
CALL sayQ
SETD.0 WordDir
SETD.1 WordDirty
CALL textSame
CALL sayQ
SETD.0 WordDirty
SETD.1 WordDir2
CALL textSame
CALL sayQ
SETD.0 WordDir
SETD.1 WordRun
CALL textSame
CALL sayQ
CALL newLine
; ---- Reading numbers ----
SETD.0 HexLabel
CALL printString
SETD.0 HexPlain
CALL showHex
SETD.0 HexUpper
CALL showHex
SETD.0 HexLower
CALL showHex
SETD.0 HexMixed
CALL showHex
SETD.0 HexTooMany
CALL showHex
SETD.0 HexStops
CALL showHex
CALL newLine
; A string may begin with anything, including a zero. The assembler strips the quotes
; before it decides what a token is, so a string starting with a zero looked exactly
; like a malformed literal and was rejected as one. This line is here so that it cannot
; go back to being rejected quietly.
SETD.0 ZeroString
CALL printString
CALL newLine
; Something that is not a number at all has to be refused rather than read as zero.
SETD.0 HexNone
CALL textHexWord
BRQ hexUnexpected
SETD.0 NoNumber
CALL printString
CALL newLine
HALT
hexUnexpected:
SETD.0 Unexpected
CALL printString
CALL newLine
HALT
; DP0 names text. Reads a number from it and prints what came out.
showHex:
CALL textHexWord
SETD.0 TextValue
CALL printWordHex
CALL blank
RET
; Prints yes if Q is zero and no if it is not. Q survives a RET, which is what lets the
; answer get this far.
sayQ:
BNQ sayNo
SETD.0 YesText
CALL printString
RET
sayNo:
SETD.0 NoText
CALL printString
RET
blank:
INIA 0x20
OUTA 0x00
RET
#Data
SplitLabel:
"split: "
SameLabel:
"same: "
HexLabel:
"hex: "
YesText:
"yes "
NoText:
"no "
NoNumber:
"no number here"
ZeroString:
"0 is a fine way to begin a string"
Unexpected:
"a number was found where there is none"
; textSplit writes over the space it cuts at, so these are written out rather than shared.
LineOne:
"dump program 2000"
LineTwo:
"dir"
WordDir:
"dir"
WordDir2:
"dir"
WordDirty:
"dirty"
WordRun:
"run"
HexPlain:
"2000"
HexUpper:
"FF"
HexLower:
"ff"
HexMixed:
"bEeF"
HexTooMany:
"12FFFF"
HexStops:
"0 and more"
HexNone:
"zebra"
#Vectors
Boot start
+47
View File
@@ -119,6 +119,45 @@ void writeDependencyFile(const char *dependencyPath, const char *outputPath) {
fclose(file);
}
// A program that bases one segment and not the other is a mistake the assembler is the
// last place to catch. Nothing relocates, so the unbased half keeps the addresses it was
// given, which are addresses from zero up, and the loader puts it there: on top of
// whatever the system keeps at the bottom of memory. It does not fail at load time and it
// does not fail at the jump. It fails later, somewhere else, as corruption.
//
// Only a segment with something in it can land on anything, so an empty one says nothing.
// A base of zero that was actually asked for is left alone, which is how a program says
// it meant it.
void checkSegmentBases(const char *fileName) {
if (!programIsLoadable()) {
return; // A boot image. Both segments begin at zero because that is where they go.
}
const char *segmentName[3];
segmentName[PROGRAM] = "Program";
segmentName[DATA] = "Data";
int segmentEnd[3];
segmentEnd[PROGRAM] = programLength;
segmentEnd[DATA] = dataLength;
const int segments[2] = { PROGRAM, DATA };
for (int i = 0; i < 2; i++) {
int mine = segments[i], other = segments[1 - i];
int content = segmentEnd[mine] - segmentBase(mine);
if (segmentBaseWasGiven(mine) || content <= 0 || !segmentBaseWasGiven(other)) {
continue;
}
fprintf(stderr, RED "Error: The %s Segment is based at 0x%04X, but the %s Segment\n"
" has %d byte%s at 0x0000 and was never given a #Base.\n"
" Half a program loaded at zero lands on whatever is already there.\n" RESET,
segmentName[other], segmentBase(other), segmentName[mine],
content, content == 1 ? "" : "s");
printf(" File: %s\n", fileName);
printf(" Say \"#Base 0x0000\" in the %s Segment if that is what you meant.\n", segmentName[mine]);
exit(1);
}
}
int main(int argc, char *argv[]) {
static struct option long_options[] = {
{"output", required_argument, 0, 'o'},
@@ -185,7 +224,15 @@ int main(int argc, char *argv[]) {
// the buffers are filled, because SWI needs the number its vector was given.
populateVectorTable(intermediateArray, index);
fillInVectorReferences(intermediateArray, index);
// The buffers are filled from wherever each segment is based, so that a byte's place
// in the buffer is the address it will have. For a boot image both bases are zero and
// this changes nothing.
programLength = segmentBase(PROGRAM);
dataLength = segmentBase(DATA);
populateOutputBuffers(intermediateArray, index, Program, &programLength, Data, &dataLength);
// After the buffers, because how much a segment actually holds is not known until it
// has been filled, and an empty segment is not a mistake.
checkSegmentBases(fileName);
if (!outputFileName) {
outputFileName = createOutputFileName(fileName);
}
+50 -4
View File
@@ -13,6 +13,30 @@
int debug = 0;
static uint16_t segmentBases[3];
static int basesGiven = 0;
// Per segment, because a base of zero that was asked for and a base of zero that was
// never mentioned are different things, and only the second one is likely a mistake.
static int baseGiven[3];
void setSegmentBase(int segment, uint16_t base) {
segmentBases[segment] = base;
baseGiven[segment] = 1;
basesGiven = 1;
}
uint16_t segmentBase(int segment) {
return segmentBases[segment];
}
int segmentBaseWasGiven(int segment) {
return baseGiven[segment];
}
int programIsLoadable(void) {
return basesGiven;
}
void toUppercase(char *str) {
for (int i = 0; str[i]; i++) {
str[i] = toupper(str[i]);
@@ -37,6 +61,10 @@ int checkIfKeyword(intermediateElement *currentElement) {
// the number that follows. The file that needs the boundary is then the
// file that asks for it, rather than relying on whatever came before.
return KEYWORD_ALIGN;
} else if (strcmp(currentElement->token, "#Base") == 0) {
// Says where this segment is loaded, which makes the program a loadable one
// rather than a boot image.
return KEYWORD_BASE;
} else if (strcmp(currentElement->token, "#Reserve") == 0) {
// Puts down the number of zero bytes that follows, so that a label can
// stand for a region rather than just its first byte.
@@ -172,7 +200,9 @@ int checkIfLiteralValue(intermediateElement *currentElement) {
return 1;
}
uint16_t readCount(intermediateElement *currentElement, const char *what) {
// Shared by readCount and readAddress, which differ only in whether zero is an answer.
// A count of nothing is a typo; an address of zero is the bottom of memory.
static uint16_t readNumber(intermediateElement *currentElement, const char *what, long least) {
const char *token = currentElement->token;
int base;
const char *baseName;
@@ -202,14 +232,22 @@ uint16_t readCount(intermediateElement *currentElement, const char *what) {
}
}
long value = strtol(digits, NULL, base);
if (value < 1 || value > 0xFFFF) {
fprintf(stderr, RED "Error: %s was given \"%s\". It has to be at least 1 and no more than 0xFFFF.\n" RESET, what, token);
if (value < least || value > 0xFFFF) {
fprintf(stderr, RED "Error: %s was given \"%s\". It has to be at least %ld and no more than 0xFFFF.\n" RESET, what, token, least);
printf(" File: %s at line %d.\n", currentElement->fileName, currentElement->lineNumber);
exit(1);
}
return (uint16_t)value;
}
uint16_t readCount(intermediateElement *currentElement, const char *what) {
return readNumber(currentElement, what, 1);
}
uint16_t readAddress(intermediateElement *currentElement, const char *what) {
return readNumber(currentElement, what, 0);
}
int checkIfLabel(intermediateElement *currentElement) {
char *token = currentElement->token;
int length = strlen(token);
@@ -261,7 +299,15 @@ int readToken(intermediateElement *currentElement, FILE *file, int *lineNumber)
if (i < (int)(sizeof(buffer) - 1)) {
buffer[i++] = c;
} else {
fprintf(stderr, "Error: String literal too long.\n");
// Say the limit and where it was met. A string long enough to reach this
// is usually several lines of help text, and "too long" on its own leaves
// somebody counting characters to find out by how much.
fprintf(stderr, RED "Error: String literal longer than %d characters.\n"
" Every string carries its own zero byte, so two written in a row"
" are two strings\n rather than one long one. Give each its own"
" label and print them one after another.\n" RESET,
(int)(sizeof(buffer) - 1));
printf(" File: %s at line %d.\n", currentElement->fileName, *lineNumber);
exit(1);
}
}
+28
View File
@@ -40,6 +40,7 @@
#define KEYWORD_VECTORS 4
#define KEYWORD_ALIGN 5
#define KEYWORD_RESERVE 6
#define KEYWORD_BASE 7
// Destination values.
#define NOWHERE 0
@@ -86,4 +87,31 @@ int readToken(intermediateElement *currentElement, FILE *file, int *lineNumber);
// ever emitted as a byte, so there is no reason to hold them to a byte's range.
uint16_t readCount(intermediateElement *currentElement, const char *what);
// The same, but zero is allowed. #Base takes one of these: a segment deliberately based
// at the bottom of memory is a thing a program is entitled to say, and saying it out loud
// is how it is told apart from a segment nobody based at all.
uint16_t readAddress(intermediateElement *currentElement, const char *what);
// ---- Where a segment is based ----
//
// A program that says nothing about this is a boot image: both its segments begin at
// zero, and the machine loads them there. A program that gives either segment a base is
// meant to be loaded somewhere else, so it is written out as a loadable program instead,
// with its addresses in front of it and none of the space below them in the file.
//
// Nothing relocates anything, so the base a program is assembled for has to be the one it
// is loaded at.
void setSegmentBase(int segment, uint16_t base);
uint16_t segmentBase(int segment);
// Whether this particular segment was given one. A segment left at zero because nobody
// said otherwise cannot be told from one deliberately based at zero by its value alone,
// and the difference is what the mismatch check below is about.
int segmentBaseWasGiven(int segment);
// Whether either segment was given one, which is what decides the kind of file written.
int programIsLoadable(void);
#endif
+2
View File
@@ -76,6 +76,7 @@ Instruction instruction_set[] = {
{0x4A, "LDD"},
{0x4B, "STD"},
{0x4C, "MVSD"},
{0x4D, "MVDS"},
// Output Operations:
{0xD0, "OUTQ"},
{0xD1, "OUTA"},
@@ -121,6 +122,7 @@ int dataPointerOperands(uint8_t opcode) {
case 0x48: // DPUP
case 0x49: // DPDN
case 0x4C: // MVSD
case 0x4D: // MVDS
return 1;
default:
return 0;
+40 -2
View File
@@ -155,6 +155,9 @@ int loadFile(intermediateElement **intermediateArray, char *fileName, int *inter
// fileName is a path already resolved and recorded by the caller, and the copy
// the include list owns, so element fileNames can safely point at it.
int status = NOWHERE;
// A base has to come before anything else in its segment, so this remembers whether
// that segment has had anything put in it yet.
static int segmentUsed[3] = {0, 0, 0};
int lineNumber = 1; // Line numbers start at 1.
// Open the file.
FILE *file = fopen(fileName, "r");
@@ -229,6 +232,34 @@ int loadFile(intermediateElement **intermediateArray, char *fileName, int *inter
// Set the state to DATA so we mark additional tokens for inclusion into Data Memory.
status = DATA;
break;
case KEYWORD_BASE: {
if (status != PROGRAM && status != DATA) {
fprintf(stderr, RED "Error: #Base outside the Program or Data Segment.\n There is no segment for it to be the base of.\n" RESET);
printf(" File: %s at line %d.\n", fileName, lineNumber);
exit(1);
}
if (segmentUsed[status]) {
fprintf(stderr, RED "Error: #Base after something is already in the segment.\n"
" A base says where the whole segment begins, so it has to come first.\n" RESET);
printf(" File: %s at line %d.\n", fileName, lineNumber);
exit(1);
}
(*intermediateIndex)++;
if (!readToken(&(*intermediateArray)[*intermediateIndex], file, &lineNumber)) {
fprintf(stderr, RED "Error: #Base without an address.\n" RESET);
exit(1);
}
(*intermediateArray)[*intermediateIndex].fileName = fileName;
(*intermediateArray)[*intermediateIndex].lineNumber = lineNumber;
setSegmentBase(status, readAddress(&(*intermediateArray)[*intermediateIndex], "#Base"));
(*intermediateArray)[*intermediateIndex].type = KEYWORD;
(*intermediateArray)[*intermediateIndex].byteLength = 0;
(*intermediateArray)[*intermediateIndex].destination = NOWHERE;
(*intermediateArray)[*intermediateIndex - 1].destination = NOWHERE;
(*intermediateArray)[*intermediateIndex - 1].byteLength = 0;
(*intermediateIndex)++;
continue;
}
case KEYWORD_ALIGN:
case KEYWORD_RESERVE: {
// Both take a count, and both only make sense somewhere that has a
@@ -288,8 +319,11 @@ int loadFile(intermediateElement **intermediateArray, char *fileName, int *inter
printf(" File: %s at line %d.\n", fileName, lineNumber);
exit(1);
}
// Next, check if it's a literal value.
} else if (checkIfLiteralValue(&(*intermediateArray)[*intermediateIndex])) {
// Next, check if it's a literal value. A string is never one, however it begins:
// the quotes are gone by now, so a string starting with a zero looks exactly like
// a malformed literal and used to be rejected as one.
} else if ((*intermediateArray)[*intermediateIndex].type != STRING
&& checkIfLiteralValue(&(*intermediateArray)[*intermediateIndex])) {
// We should check to make sure we have a destination for it.
if (status == NOWHERE) {
fprintf(stderr, RED "Error: Attempting to write a value to nowhere!\n Did you forget to use the #Program or #Data keyword?\n" RESET);
@@ -320,6 +354,10 @@ int loadFile(intermediateElement **intermediateArray, char *fileName, int *inter
}
}
(*intermediateArray)[*intermediateIndex].destination = status;
if ((status == PROGRAM || status == DATA)
&& (*intermediateArray)[*intermediateIndex].type != KEYWORD) {
segmentUsed[status] = 1;
}
(*intermediateIndex)++;
}
return 0;
+129 -12
View File
@@ -14,6 +14,7 @@
#include "secondPass.h"
#include "Assm-util.h"
#include "assembly.h"
#include "sbex.h"
int debugSecondPass = 0;
@@ -62,8 +63,11 @@ void addLabel(char *labelName, uint16_t address, int type, const char *fileName,
}
void populateLabelTable(intermediateElement *intermediateArray, int arraySize) {
int programCount = 0;
int dataCount = 0;
// Counting starts at the base, so a label in a program built to live somewhere else
// already holds the address it will have once it is there. Nothing relocates
// anything, which is exactly why this has to be right at assembly time.
int programCount = segmentBase(PROGRAM);
int dataCount = segmentBase(DATA);
// Loop through the array, if there's a label definition, add it to the label list.
for (int i = 0; i < arraySize ; i++) {
// How many zeroes an #Align comes to depends on where the cursor has reached,
@@ -159,6 +163,16 @@ static void vectorError(const char *message, intermediateElement *element) {
exit(1);
}
// Whether the token at b is written on the same line as the one at a, and so belongs to
// the same entry. A line is what tells a name with a handler apart from a name on its own.
static int sameLine(intermediateElement *intermediateArray, int a, int b) {
if (a < 0 || b < 0) {
return 0;
}
return intermediateArray[a].lineNumber == intermediateArray[b].lineNumber
&& intermediateArray[a].fileName == intermediateArray[b].fileName;
}
// The next token belonging to the Vector Segment, or -1 if the segment has run out.
static int nextVectorToken(intermediateElement *intermediateArray, int arraySize, int from) {
for (int i = from; i < arraySize; i++) {
@@ -169,12 +183,28 @@ static int nextVectorToken(intermediateElement *intermediateArray, int arraySize
return -1;
}
static void addVector(char *name, uint8_t index, uint16_t base, uint16_t handler, intermediateElement *element) {
// A declared vector has a name and a number but no handler, so nothing goes into the
// table for it. It exists so that a program can name a service it calls without claiming
// to implement it, which is what lets one file be included by both sides.
// Where a vector of this name already is, or -1. A name can be met twice: once where it
// is declared and once where somebody supplies its handler.
static int findVector(const char *name) {
for (int i = 0; i < vectorArrayCount; i++) {
if (vectorArray[i].name && strcmp(vectorArray[i].name, name) == 0) {
return i;
}
}
return -1;
}
static void addVector(char *name, uint8_t index, uint16_t base, uint16_t handler,
int declaredOnly, intermediateElement *element) {
if (vectorArrayCount >= MAX_VECTORS) {
vectorError("Too many vectors defined.", element);
}
for (int i = 0; i < vectorArrayCount; i++) {
if (vectorArray[i].index == index && vectorArray[i].base == base) {
if (vectorArray[i].index == index && vectorArray[i].base == base
&& !vectorArray[i].declaredOnly && !declaredOnly) {
fprintf(stderr, RED "Error: That vector already has a handler.\n" RESET);
printf("File: %s at line %d.\n", element->fileName, element->lineNumber);
exit(1);
@@ -189,6 +219,7 @@ static void addVector(char *name, uint8_t index, uint16_t base, uint16_t handler
vectorArray[vectorArrayCount].index = index;
vectorArray[vectorArrayCount].base = base;
vectorArray[vectorArrayCount].handler = handler;
vectorArray[vectorArrayCount].declaredOnly = declaredOnly;
vectorArrayCount++;
}
@@ -228,7 +259,26 @@ void populateVectorTable(intermediateElement *intermediateArray, int arraySize)
int handlerToken = nextVectorToken(intermediateArray, arraySize, portToken + 1);
uint16_t handler = resolveHandler(intermediateArray, handlerToken, "Device");
addVector(NULL, intermediateArray[portToken].byteValue, HARDWARE_VECTOR_BASE,
handler, &intermediateArray[i]);
handler, 0, &intermediateArray[i]);
i = nextVectorToken(intermediateArray, arraySize, handlerToken + 1);
continue;
}
int handlerToken = nextVectorToken(intermediateArray, arraySize, i + 1);
int hasHandler = sameLine(intermediateArray, i, handlerToken);
int already = findVector(token);
if (already >= 0) {
// Met before. A handler now is somebody implementing what was declared
// earlier, which is how one shared file can serve both sides.
if (!hasHandler) {
vectorError("That vector is declared more than once.", &intermediateArray[i]);
}
if (!vectorArray[already].declaredOnly) {
vectorError("That vector already has a handler.", &intermediateArray[i]);
}
vectorArray[already].handler = resolveHandler(intermediateArray, handlerToken, token);
vectorArray[already].declaredOnly = 0;
i = nextVectorToken(intermediateArray, arraySize, handlerToken + 1);
continue;
}
@@ -250,9 +300,15 @@ void populateVectorTable(intermediateElement *intermediateArray, int arraySize)
nextFreeVector++;
}
int handlerToken = nextVectorToken(intermediateArray, arraySize, i + 1);
if (!hasHandler) {
// Nothing follows it on the line, so this says what the vector is called and
// what number it has, and leaves implementing it to somebody else.
addVector(token, index, SOFTWARE_VECTOR_BASE, 0, 1, &intermediateArray[i]);
i = handlerToken;
continue;
}
uint16_t handler = resolveHandler(intermediateArray, handlerToken, token);
addVector(token, index, SOFTWARE_VECTOR_BASE, handler, &intermediateArray[i]);
addVector(token, index, SOFTWARE_VECTOR_BASE, handler, 0, &intermediateArray[i]);
i = nextVectorToken(intermediateArray, arraySize, handlerToken + 1);
}
}
@@ -440,6 +496,48 @@ void populateOutputBuffers(intermediateElement *intermediateArray, int arraySize
}
}
// A loadable program: sixteen bytes saying where it belongs, then the code and the data.
// The space below each base is not written out, because nothing needs to carry it: the
// header says where the bytes go and the loader puts them there.
static void writeLoadable(const char *outputFileName, uint8_t *Program, int programCount,
uint8_t *Data, int dataCount) {
uint16_t codeBase = segmentBase(PROGRAM);
uint16_t dataBase = segmentBase(DATA);
int codeLength = programCount - codeBase;
int dataLength = dataCount - dataBase;
if (codeLength < 0) codeLength = 0;
if (dataLength < 0) dataLength = 0;
FILE *outputFile = fopen(outputFileName, "wb");
if (!outputFile) {
fprintf(stderr, RED "Error: Could not open file \"%s\" for writing.\n" RESET, outputFileName);
exit(1);
}
uint8_t header[SBEX_HEADER_BYTES];
memset(header, 0, sizeof(header));
memcpy(header, SBEX_MAGIC, SBEX_MAGIC_BYTES);
header[SBEX_VERSION_AT] = SBEX_VERSION;
header[SBEX_CODE_AT] = (uint8_t)(codeBase >> 8);
header[SBEX_CODE_AT + 1] = (uint8_t)(codeBase & 0xFF);
// Where it starts is where it begins. A program that wants otherwise puts a branch
// at its first instruction, which costs three bytes and needs no format for it.
header[SBEX_ENTRY_AT] = (uint8_t)(codeBase >> 8);
header[SBEX_ENTRY_AT + 1] = (uint8_t)(codeBase & 0xFF);
header[SBEX_CODE_LEN_AT] = (uint8_t)(codeLength >> 8);
header[SBEX_CODE_LEN_AT + 1] = (uint8_t)(codeLength & 0xFF);
header[SBEX_DATA_AT] = (uint8_t)(dataBase >> 8);
header[SBEX_DATA_AT + 1] = (uint8_t)(dataBase & 0xFF);
header[SBEX_DATA_LEN_AT] = (uint8_t)(dataLength >> 8);
header[SBEX_DATA_LEN_AT + 1] = (uint8_t)(dataLength & 0xFF);
fwrite(header, 1, sizeof(header), outputFile);
fwrite(Program + codeBase, 1, (size_t)codeLength, outputFile);
fwrite(Data + dataBase, 1, (size_t)dataLength, outputFile);
fclose(outputFile);
printf("Successfully wrote SplitBit loadable program to \"%s\".\n", outputFileName);
printf(GREEN " Code: %d bytes at 0x%04X.\n Data: %d bytes at 0x%04X.\n Total size: %d bytes.\n" RESET,
codeLength, codeBase, dataLength, dataBase, SBEX_HEADER_BYTES + codeLength + dataLength);
}
void writeOutputFile(const char *outputFileName, uint8_t *Program, int programCount, uint8_t *Data, int dataCount) {
FILE *outputFile = fopen(outputFileName, "wb");
if (!outputFile) {
@@ -487,16 +585,35 @@ void writeOutputFile(const char *outputFileName, uint8_t *Program, int programCo
exit(1);
}
// A program with a base is one meant to be loaded, so it is written out with its
// addresses in front of it and nothing below them. A boot image carries the padding
// because the machine loads it at zero; a loadable one would only be carrying space
// it does not use.
if (programIsLoadable()) {
fclose(outputFile);
writeLoadable(outputFileName, Program, programCount, Data, dataCount);
return;
}
// The Vector Segment, only if the program named any. Leaving it out entirely is
// what lets a binary written before vectors existed still load: the reader treats
// the end of the file as an empty table rather than a missing one.
int installed = 0;
for (int i = 0; i < vectorArrayCount; i++) {
if (!vectorArray[i].declaredOnly) {
installed++;
}
}
int vectorBytes = 0;
if (vectorArrayCount > 0) {
if (installed > 0) {
fwrite("VEC", sizeof(char), SEGMENT_MARKER_LENGTH, outputFile);
vectorBytes = vectorArrayCount * VECTOR_ENTRY_FILE_BYTES;
vectorBytes = installed * VECTOR_ENTRY_FILE_BYTES;
fputc((vectorBytes >> 8) & 0xFF, outputFile);
fputc(vectorBytes & 0xFF, outputFile);
for (int i = 0; i < vectorArrayCount; i++) {
if (vectorArray[i].declaredOnly) {
continue;
}
uint16_t slot = vectorArray[i].base + (uint16_t)vectorArray[i].index * VECTOR_ENTRY_BYTES;
fputc((slot >> 8) & 0xFF, outputFile);
fputc(slot & 0xFF, outputFile);
@@ -508,10 +625,10 @@ void writeOutputFile(const char *outputFileName, uint8_t *Program, int programCo
fclose(outputFile);
printf("Successfully wrote SplitBit binary to \"%s\".\n", outputFileName);
printf(GREEN " Program Segment size: %d bytes.\n Data Segment size: %d bytes.\n" RESET, programCount, dataCount);
if (vectorArrayCount > 0) {
printf(GREEN " Vectors: %d.\n" RESET, vectorArrayCount);
if (installed > 0) {
printf(GREEN " Vectors: %d.\n" RESET, installed);
}
printf(GREEN " Total size: %d bytes.\n" RESET,
(programCount + dataCount + SPLITBIT_HEADER_BYTES
+ (vectorArrayCount > 0 ? SEGMENT_MARKER_LENGTH + SEGMENT_LENGTH_BYTES + vectorBytes : 0)));
+ (installed > 0 ? SEGMENT_MARKER_LENGTH + SEGMENT_LENGTH_BYTES + vectorBytes : 0)));
}
+1
View File
@@ -26,6 +26,7 @@ typedef struct {
uint8_t index; // Which vector in its table.
uint16_t base; // Which table: software or hardware.
uint16_t handler; // Where the handler ended up.
int declaredOnly; // Named and numbered, with nobody implementing it here.
} VectorEntry;
void freeLabelList();
-46
View File
@@ -1,46 +0,0 @@
// sbex.h
// The SplitBit loadable program format, version one.
//
// A program that is not the one the machine booted from has to say where it wants to
// live, because nothing relocates it. This is a header saying that, in front of the
// bytes themselves. It is the same idea as the load address on the front of a C64 .PRG,
// with room for the machine to ask a few more questions later.
//
// Two things read this: whatever builds one on the host, and the loader running on
// SplitBit. As with the filesystem, nothing is shared between them but the specification.
//
// All multi byte numbers are most significant byte first.
//
// 0 4 "SBEX"
// 4 1 Version
// 5 1 Reserved
// 6 2 Where the code goes in Program Memory
// 8 2 Where to start running, an address in Program Memory
// 10 2 How many bytes of code there are
// 12 2 Where the data goes in Data Memory
// 14 2 How many bytes of data there are
// 16 The code, then the data
//
// Sixteen bytes, so the code begins at a round offset and finding it is one step rather
// than an arithmetic. Nothing here relocates anything: the addresses are where the
// program was built to live, and putting it anywhere else would leave every branch and
// every SETD inside it pointing at the wrong place.
//
// Written by Anachronaut
#ifndef SBEX_H
#define SBEX_H
#define SBEX_MAGIC "SBEX"
#define SBEX_MAGIC_BYTES 4
#define SBEX_VERSION 1
#define SBEX_HEADER_BYTES 16
#define SBEX_VERSION_AT 4
#define SBEX_CODE_AT 6
#define SBEX_ENTRY_AT 8
#define SBEX_CODE_LEN_AT 10
#define SBEX_DATA_AT 12
#define SBEX_DATA_LEN_AT 14
#endif // SBEX_H
-58
View File
@@ -1,58 +0,0 @@
#!/usr/bin/env python3
"""Wraps an assembled SplitBit binary into a loadable program.
The assembler emits a boot image: a Program Segment that loads at zero and a Data Segment
that does the same. A program meant to be loaded somewhere else has to say where it goes,
which is what the SBEX header in front of it is for.
A program says where it lives by reserving the front of each segment, so the addresses
given here have to match the reserves in its source. Nothing checks that for you, and
nothing relocates anything if you get it wrong.
"""
import struct
import sys
def segments(raw):
at = 4 + 1 + 4 # magic, version, feature flags
assert raw[:4] == b"SPBT", "not a SplitBit binary"
assert raw[at:at + 3] == b"PRG"
plen = struct.unpack(">H", raw[at + 3:at + 5])[0]
program = raw[at + 5:at + 5 + plen]
at = at + 5 + plen
assert raw[at:at + 3] == b"DAT"
dlen = struct.unpack(">H", raw[at + 3:at + 5])[0]
return program, raw[at + 5:at + 5 + dlen]
def main():
if len(sys.argv) != 6:
sys.exit("usage: wrap.py <binary> <output> <code address> <data address> <entry>")
binary, output = sys.argv[1], sys.argv[2]
codeAt, dataAt, entry = (int(a, 0) for a in sys.argv[3:6])
program, data = segments(open(binary, "rb").read())
# Everything below the address a segment is placed at is the padding the reserve put
# there, and is not part of the program.
code = program[codeAt:]
values = data[dataAt:]
header = bytearray(16)
header[0:4] = b"SBEX"
header[4] = 1
struct.pack_into(">H", header, 6, codeAt)
struct.pack_into(">H", header, 8, entry)
struct.pack_into(">H", header, 10, len(code))
struct.pack_into(">H", header, 12, dataAt)
struct.pack_into(">H", header, 14, len(values))
with open(output, "wb") as out:
out.write(header)
out.write(code)
out.write(values)
print("%s: %d bytes of code at 0x%04X, %d of data at 0x%04X, entry 0x%04X"
% (output, len(code), codeAt, len(values), dataAt, entry))
main()
+17
View File
@@ -592,6 +592,23 @@ uint8_t executeOperation(uint8_t Instruction, CPURegisters *cpu) {
*target = cpu->StackPointer;
}
break;
case 0x4D: {
// MVDS - Copy the selected Data Pointer into the Stack Pointer.
//
// This one is dangerous and is meant to be used rarely. Moving the Stack
// under a running program abandons every return address on it, so a RET
// after this goes wherever the new Stack happens to say.
//
// It exists because a system that runs other programs has no other way to
// get its Stack back. A program that gives up part way through leaves
// whatever it pushed behind, and the interrupt frame that carried the
// request to stop is on there too. Without this the Stack only ever grows
// downward, one abandoned program at a time, and a shell cannot outlive
// many of them.
uint16_t *source = selectDataPointer(cpu);
cpu->StackPointer = *source;
}
break;
//
// Dx - Output Operations:
//
+4 -1
View File
@@ -111,7 +111,10 @@ int main (int argc, char *argv[]) {
if (options.debug) {
// Wait before advancing, not after, so that a keypress is what moves the
// machine on rather than something that happens once it already has.
getchar();
// Through the console rather than getchar, so that everything reading standard
// input reads it the same way and the console's pushback stays the only place
// a byte can be sitting.
consoleReadByte();
}
int cycles;
if (options.debug) {
+179 -3
View File
@@ -7,7 +7,167 @@
#include "../Assembler/assembly.h" // For the fault vector numbers.
#include "controller.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <signal.h>
#include <errno.h>
#include <termios.h>
#include <unistd.h>
#include <poll.h>
// ---- The console ----
//
// The console owns its own reading rather than going through getchar. stdio keeps a
// buffer, and the status port asks the operating system what is waiting; those two
// disagree the moment stdio has read ahead, and the status port would then swear nothing
// was there while a read returned instantly. One byte of pushback here is enough, because
// nothing needs to look further ahead than the byte it is about to take.
static int consoleKeyMode = 0;
static int consoleEnded = 0;
static int consolePushback = -1; // A byte already taken from the host, or -1.
static struct termios consoleSavedTerminal;
static int consoleTerminalSaved = 0;
void consoleRestore(void) {
if (consoleTerminalSaved) {
tcsetattr(STDIN_FILENO, TCSANOW, &consoleSavedTerminal);
consoleTerminalSaved = 0;
}
consoleKeyMode = 0;
}
// Restores the terminal and then dies the way it would have died anyway, so that the
// shell sees the signal it was expecting rather than a machine that exited quietly.
static void consoleSignalHandler(int signalNumber) {
consoleRestore();
signal(signalNumber, SIG_DFL);
raise(signalNumber);
}
static void consoleSetMode(uint8_t mode) {
int wantKeys = (mode & CONSOLE_MODE_KEY) != 0;
if (wantKeys == consoleKeyMode) {
return;
}
if (!wantKeys) {
consoleRestore();
return;
}
// Nothing to configure when input is not a terminal, but the mode is still recorded:
// a program asking the status port what mode it is in should be told what it asked
// for, whether or not there was a terminal to carry it out on.
consoleKeyMode = 1;
if (!isatty(STDIN_FILENO)) {
return;
}
if (!consoleTerminalSaved) {
if (tcgetattr(STDIN_FILENO, &consoleSavedTerminal) != 0) {
return;
}
consoleTerminalSaved = 1;
// Registered on the first use rather than at startup, so a run that never asks
// for key mode installs nothing at all.
atexit(consoleRestore);
signal(SIGINT, consoleSignalHandler);
signal(SIGTERM, consoleSignalHandler);
}
struct termios raw = consoleSavedTerminal;
raw.c_lflag &= (tcflag_t)~(ICANON | ECHO);
raw.c_cc[VMIN] = 1;
raw.c_cc[VTIME] = 0;
tcsetattr(STDIN_FILENO, TCSANOW, &raw);
}
// Everything already written is put where it can be seen before the machine asks the host
// anything. Standard output is line buffered on a terminal, so a prompt with no newline
// after it - "> " is exactly that, and exactly why this matters - would sit in the buffer
// while the machine waited for an answer to a question nobody had been shown.
//
// getchar used to do this by accident, because reading through stdio flushes the line
// buffered streams first. Reading with read() does not, so what was a side effect of the
// old way is done deliberately here.
static void consoleShowWhatIsWritten(void) {
fflush(stdout);
}
uint8_t consoleReadByte(void) {
if (consolePushback >= 0) {
uint8_t byte = (uint8_t)consolePushback;
consolePushback = -1;
return byte;
}
consoleShowWhatIsWritten();
unsigned char byte;
for (;;) {
ssize_t got = read(STDIN_FILENO, &byte, 1);
if (got == 1) {
return byte;
}
if (got == 0) {
// End of input. Still 0xFF, which is what getchar's EOF became when this was
// the only answer available, so nothing written against the old behaviour
// changes. The ENDED bit is the new way to know it was not a real byte.
consoleEnded = 1;
return 0xFF;
}
if (errno != EINTR) {
consoleEnded = 1;
return 0xFF;
}
// Interrupted before anything arrived, so ask again.
}
}
// Asking the host whether anything is waiting, and TAKING IT IF THERE IS. The byte goes
// into the pushback and the next read of the data port hands it over, so nothing is lost
// and no program can tell that it was fetched early.
//
// Fetching it early is what makes the answer worth having. The operating system will say a
// pipe is readable when what is waiting is the end of it, so asking without reading can
// only report that SOMETHING is there. Reading settles which: a byte, or the end. Without
// this, ENDED could not go up until a program had already read the 0xFF that stands for
// it, and every program would have to swallow one imaginary byte to find out there were
// none.
static void consoleFetch(void) {
if (consolePushback >= 0 || consoleEnded) {
return;
}
// Flushed here too. A program that draws something and then polls rather than reads is
// just as entitled to have the drawing appear, and it never reaches the read that
// would otherwise have flushed for it.
consoleShowWhatIsWritten();
struct pollfd waiting = { .fd = STDIN_FILENO, .events = POLLIN, .revents = 0 };
if (poll(&waiting, 1, 0) <= 0 || (waiting.revents & (POLLIN | POLLHUP)) == 0) {
return;
}
unsigned char byte;
ssize_t got = read(STDIN_FILENO, &byte, 1);
if (got == 1) {
consolePushback = byte;
} else if (got == 0) {
consoleEnded = 1;
}
// A read that failed for any other reason is left alone: the next attempt asks again,
// and an interrupted poll is not news.
}
static uint8_t consoleStatus(void) {
uint8_t status = consoleKeyMode ? CONSOLE_STATUS_KEYMODE : 0;
consoleFetch();
if (consoleEnded) {
// READY IS NOT SET HERE, although a read would answer immediately. The bit means
// "there is a byte to be had", and at the end of input there is not; what a read
// returns then is 0xFF standing in for nothing. A program looping while READY
// stops on its own at the end, which is the behaviour worth having, and one that
// wants to know why asks ENDED.
return status | CONSOLE_STATUS_ENDED;
}
if (consolePushback >= 0) {
status |= CONSOLE_STATUS_READY;
}
return status;
}
// One bit per port, so a device can ask for attention without anything having to poll
// it. Eight ports to the byte, low bit first.
@@ -233,6 +393,11 @@ static const DeviceRecord *deviceOnPort(uint8_t port) {
if (port >= CONTROLLER_PORT_BASE && port <= CONTROLLER_PORT_TOP) {
return &controllerRecord;
}
if (port > PORT_CONSOLE && port <= PORT_CONSOLE_TOP) {
// The status and control ports are the same device as the data port, which is the
// one in the table and the one that would raise a line if the console ever did.
return deviceOnPort(PORT_CONSOLE);
}
if (port > PORT_DISK && port <= PORT_DISK_TOP) {
// The base port is in the table proper, since that is the one that owns the
// memory and raises the line. The rest of the block reports the same device.
@@ -269,12 +434,17 @@ uint8_t OutputHandler(uint8_t DataByte, uint8_t Address) {
}
// This function sends the DataByte to the appropriate place based on the Port Address.
switch(Address) {
case PORT_CONSOLE:
case CONSOLE_DATA:
// If data is sent here, it should be written to STDOUT.
// For now, I'll implement this so it simply writes each byte out as it comes in.
// Later, I'll want to use a buffer for this for performance, probably.
putchar(DataByte);
break;
case CONSOLE_CONTROL: consoleSetMode(DataByte); break;
case CONSOLE_STATUS:
// 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.
break;
case DISK_BLOCK_HIGH: diskBlock = (uint16_t)(DataByte << 8) | (diskBlock & 0x00FF); break;
case DISK_BLOCK_LOW: diskBlock = (diskBlock & 0xFF00) | DataByte; break;
case DISK_COMMAND: diskCommand(DataByte); break;
@@ -319,9 +489,15 @@ uint8_t InputHandler(uint8_t Address) {
return controllerRead(Address);
}
switch(Address) {
case PORT_CONSOLE:
case CONSOLE_DATA:
// If data is sent here, it should be read from STDIN.
return getchar();
return consoleReadByte();
break;
case CONSOLE_STATUS: return consoleStatus();
case CONSOLE_CONTROL:
// Write only. Reading it gives zero rather than the mode, because the mode is
// a bit in the status port and one fact wants one place to live.
return 0;
break;
case DISK_BLOCK_HIGH: return (uint8_t)(diskBlock >> 8);
case DISK_BLOCK_LOW: return (uint8_t)(diskBlock & 0xFF);
+57 -1
View File
@@ -14,7 +14,15 @@
// Which port a device answers on is a property of the machine rather than of any
// program, so the numbers live here and everything else refers to them by name.
#define PORT_CONSOLE 0x00
// The console answers on three ports. The data port is the machine's oldest promise and
// 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.
#define PORT_CONSOLE 0x00
#define PORT_CONSOLE_TOP 0x02
#define CONSOLE_DATA 0x00
#define CONSOLE_STATUS 0x01
#define CONSOLE_CONTROL 0x02
#define PORT_TEST 0x10
#define PORT_REFUSE 0x11
#define PORT_MEMORY 0x12
@@ -30,6 +38,54 @@
#define DISK_STATUS 0x23
#define PORT_REGISTRY 0xFF
// ---- The console ----
//
// Two modes, chosen by the program through the control port. The console starts in LINE
// mode, which is what the machine has always done: the terminal holds what is typed until
// Return, and does the echoing and the backspacing on the way. Reading the data port waits
// for a whole line to be finished somewhere else and then hands it over a byte at a time.
//
// KEY mode turns that off. Keys arrive as they are pressed, and nothing echoes them, so a
// program that wants them seen has to send them back out itself. That is not a choice this
// machine is making; it is what asking the terminal to stop holding a line means, and the
// editing goes away with it. A program that wants keys is expected to want that.
//
// READING THE DATA PORT WAITS IN BOTH MODES. The status port is how a program declines to
// wait, and keeping that in one place means the data port means one thing everywhere. A
// read that sometimes blocked and sometimes did not, depending on state set somewhere
// else, is the kind of thing that works until it does not.
//
// KEY MODE ONLY REACHES THE TERMINAL when there is one. With input coming from a pipe
// there is nothing to put into another mode, and the status port answers by asking the
// operating system whether anything is waiting, which is true of a pipe with bytes in it.
#define CONSOLE_MODE_LINE 0x00
#define CONSOLE_MODE_KEY 0x01
// Set when there is a byte to be had. NOT set at the end of input, although a read would
// answer at once there: what it answers is 0xFF standing in for nothing, and calling that
// ready would make a loop that reads while READY spin on imaginary bytes forever. A loop
// like that now stops when the input does, which is what anybody writing one intends.
#define CONSOLE_STATUS_READY 0x01
// Set once input has run out for good. The data port still answers 0xFF, which is what it
// always did and what every program written before this expects, but 0xFF is also an
// ordinary byte and this bit is the only thing that can tell the difference.
#define CONSOLE_STATUS_ENDED 0x02
// Which mode the console is in, so that a program can put it back the way it found it
// rather than assuming it knows.
#define CONSOLE_STATUS_KEYMODE 0x04
// Puts the terminal back the way it was found. Registered with atexit and called from the
// signal handlers, because a machine that stops in key mode and does not undo it leaves
// the shell that started it unusable, which is a far worse failure than anything the
// program was doing.
void consoleRestore(void);
// One byte from the console, waiting if it has to. Everything that reads standard input
// goes through here: the emulator owns one byte of pushback, and stdio holding a buffer
// of its own behind that would make the status port lie about what is waiting.
uint8_t consoleReadByte(void);
// ---- Device classes ----
//
// What kind of thing is plugged into a port. Class 0 is not a device: reading an
+60 -2
View File
@@ -4,9 +4,9 @@ SplitBit assembly syntax is similar to many other assembler syntaxes. Whitespace
A semicolon, ';', denotes the start of a comment, anything beyond it on a line is disregarded by the assembler.
Special Keywords are denoted with hash marks, '#'. The Keywords are #Include, #Program, #Data, #Vectors, #Align, and #Reserve.
Special Keywords are denoted with hash marks, '#'. The Keywords are #Include, #Program, #Data, #Vectors, #Base, #Align, and #Reserve.
The first four say what kind of thing follows them. #Align and #Reserve are instructions to the assembler in the middle of a segment, and are described under Moving The Cursor Along.
The first four say what kind of thing follows them. #Base says where a segment is loaded, and is described under Programs Meant To Be Loaded. #Align and #Reserve are instructions to the assembler in the middle of a segment, and are described under Moving The Cursor Along.
SplitBit programs must have a Program Segment. You define the start of a program with the #Program Keyword.
SplitBit programs may have a Data Segment. You may define the start of the data with the #Data Keyword.
@@ -124,6 +124,53 @@ Both take a number written the way literals are, prefaced with 0x or 0d, but the
Both work in the Program Segment as well as the Data Segment, and both are an error anywhere else, because outside a segment there is no cursor to move.
## Programs Meant To Be Loaded:
A program assembled without saying anything about where it goes is a boot image. Both its segments begin at zero, which is where the machine puts them, and it is written out in the format the emulator loads.
A program that will be loaded by something else has to say where it belongs, because nothing relocates it. `#Base` says so, and it has to be the first thing in its segment:
```
#Program
#Base 0x2000 ; This program's code lives from 0x2000.
start:
...
#Data
#Base 0x1000 ; And its data from 0x1000.
Message:
"..."
```
Every label inside is then already the address it will have once the program is loaded, so a branch or a SETD written in it points at the right place. Giving either segment a base makes the whole program a loadable one, and the assembler writes it out with a header saying where its two pieces go, followed by the pieces themselves. The space below each base is not in the file: the header says where the bytes belong and the loader puts them there.
A program starts at its code base. One that wants to begin somewhere else puts a branch at its first instruction, which costs three bytes and needs nothing from the format.
The address a program is assembled for has to be the address it is loaded at. Nothing checks that, and nothing can fix it: a program put anywhere else has every branch and every SETD inside it pointing somewhere wrong.
### Both Segments Or Neither:
Base one segment and the assembler expects a base on the other, if the other holds anything. Forgetting the second one is refused:
```
Error: The Program Segment is based at 0x2000, but the Data Segment
has 3 bytes at 0x0000 and was never given a #Base.
Half a program loaded at zero lands on whatever is already there.
```
This is worth refusing rather than allowing, because the result runs. The unbased half keeps the addresses it was given, which count up from zero, and the loader puts it exactly there, on top of whatever the system keeps at the bottom of memory. Nothing fails at load and nothing fails at the jump. It fails later, somewhere else, as corruption of something that never went near the program that caused it.
It is easy to do by accident. A segment can come from an included library rather than from the program itself, and a `#Data` that arrives with `#Include print.asm` is just as unbased as one you wrote, while being much harder to notice missing.
A program that genuinely wants a segment at the bottom of memory says so:
```
#Data
#Base 0x0000 ; Meant, not forgotten.
```
`#Base` is the one directive that takes zero. `#Align` and `#Reserve` are counts, and a count of nothing is a typo, so they still require at least one.
## The Vector Segment:
A vector says where to go when something happens: the machine starting up, a program asking for a service, a device wanting attention, or the CPU meeting a byte it cannot decode. The Vector Segment says which of your routines belongs to which vector, and the assembler works out the rest.
@@ -141,6 +188,17 @@ Every line names a vector and then the label of the routine that handles it.
Device 0x10 diskReady
```
A line with a name and nothing after it declares the name and its number without installing anything. That is what lets one file be included by both the program that provides a service and the program that calls it: the shared file names them, the provider follows it with handlers, and a program that only calls them says them by name without pretending to implement them.
```
; services.asm, included by both sides
#Vectors
osPrint
osExit
```
Because the order is fixed in that one file, both sides give them the same numbers, and neither has to write a number down.
Five names already mean something:
| Name | Vector |
+137 -12
View File
@@ -28,7 +28,7 @@ It has ten registers:
- The Stack Pointer is a 16 bit pointer into the Data Memory.
- The SP points to the next free slot, not to the last thing pushed. It initializes at location 0xFFFF, so the first push writes there and the byte pushed last always sits one above the SP.
- The SP value is only modified by the push and pop instructions, by CALL and RET, and by an interrupt arriving or returning. It cannot be set by the programmer. MVSD copies it out without moving it.
- The SP is moved by the push and pop instructions, by CALL and RET, and by an interrupt arriving or returning. MVSD copies it out without moving it, and MVDS sets it outright. See The Stack Pointer, Set By Hand below before using MVDS.
- The Stack lives in Data Memory, so a Data Pointer can be aimed at it and used to read what is on it. MVSD is how a program finds out where to aim.
- The Status register is an 8 bit register whose various bits are used as flags. Only four of these flags are used in the current implementation.
@@ -130,7 +130,7 @@ If a device interrupts and its vector is empty, that is a fault: the machine sto
| Port | Device | Class |
| --- | --- | --- |
| 0x00 | The console. Writing sends a byte to standard output, reading takes one from standard input. | 0x02 |
| 0x00 - 0x02 | The console. See The Console below. Writing to 0x00 sends a byte to standard output, reading takes one from standard input. | 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 |
| 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 below. It interrupts on 0x20, its base port. | 0x13 |
@@ -338,6 +338,44 @@ A write the controller will not perform is a GuardViolation. There are two reaso
Both arrive at the instruction that asked, so a handler sees which one it was. A handler that means to carry on past it steps the saved address on by two, since the input and output instructions are an opcode and a port.
## The Console:
Port 0x00 is the oldest thing on this machine and it has not changed: writing sends a byte out, reading takes one in and waits until there is one. Every program ever written for SplitBit uses it that way and still does. What is new is that a program can now ask whether a read would have to wait, and can say what it wants a keypress to mean.
| Port | Register |
| --- | --- |
| 0x00 | Data. Writing sends a byte out, reading takes one in and waits for it. |
| 0x01 | Status. Bit 0 a byte is waiting, bit 1 input has ended, bit 2 the console is in key mode. |
| 0x02 | Mode. Writing 0x00 asks for line mode, 0x01 for key mode. |
### Two Kinds Of Input:
In **line mode**, which is how the machine starts, the terminal holds what is typed until Return and does the echoing and the backspacing on the way. A program reading the data port gets a finished line, one byte at a time. This is what the machine has always done and what a shell wants.
In **key mode** the terminal stops holding the line. Keys arrive as they are pressed, and nothing echoes them, so a program that wants them seen has to send them back out itself. The editing goes with the echo: there is no backspace, because backspace was the terminal's doing and the terminal is no longer involved. That is not a choice this machine makes, it is what asking for keys means, and a program that wants keys is expected to want it.
A program is expected to put the console back in line mode before it finishes. CosmOS also does it whenever a program returns, because a program that stops early would otherwise hand back a shell with no echo, and a shell has no way to find out that happened.
### Reading Without Waiting:
Reading the data port waits in **both** modes. The status port is how a program declines to wait, and keeping that in one place is deliberate: a read that sometimes blocked and sometimes did not, depending on a mode set somewhere else, would be a program that works until it does not.
So a simulation that should stop when somebody presses a key asks the status port between steps, and only reads the data port once it knows there is something to read:
```
INA 0x01
INIB 0x01 ; Bit 0: is a byte waiting?
AND
BRQ nobodyPressedAnything
INA 0x00 ; There is one, so this will not wait.
```
### The End Of Input:
Reading the data port when input has run out gives 0xFF, which is what it has always given and what programs written before any of this expect. But 0xFF is also an ordinary byte, and nothing could tell the two apart. Bit 1 of the status port is what tells them apart now.
Bit 0 is **not** set once input has ended, even though a read would answer immediately. The bit means a byte is there to be had, and at the end of input there is not. That way a loop that reads while bit 0 is set stops when the input does, instead of taking imaginary bytes forever.
## Storage:
The disk is a block device. It knows numbered blocks of 256 bytes and has never heard of a file. A filesystem is software this machine runs, not something done on its behalf: a disk that understood filenames would be the emulator doing the work while the machine pretended it had.
@@ -381,16 +419,20 @@ A disk error is not a fault. Faults on this machine mean it cannot continue, and
## Reading The Filesystem:
The disk knows blocks and nothing else, so a filesystem is software. Programs/Libraries/sbfs.asm reads one.
The disk knows blocks and nothing else, so a filesystem is software. Programs/CosmOS/Source/sbfs.asm reads one.
| Routine | Does |
| --- | --- |
| sbfsMount | Registers the disk's buffer as bank 3, reads the superblock, and checks the disk is one of ours. Q is zero if it is. |
| sbfsFind | DP0 names a file, ending in a zero byte. Q is zero if it was found, and then SbfsFileStart, SbfsFileBlocks and SbfsFileTail describe it. |
| sbfsRead | Reads the file that was found into Data Memory at DP1. Q is zero if it worked. |
| sbfsFirst | Starts a walk through the directory. Q is zero if there is an entry, and then SbfsName holds its name and the SbfsFile fields describe it. |
| sbfsNext | Steps the walk to the next entry in use. Q is zero if there was one. |
| sbfsCreate | Makes a file. DP0 names it, and SbfsFileBlocks with SbfsFileTail say how big it is. Q is zero if it was made, and then SbfsFileStart says where it went. |
| sbfsWriteFile | Writes the file that was made, from Data Memory at DP1. |
Finding a file and listing what is there are different jobs. sbfsFind searches for one name; sbfsFirst and sbfsNext walk the whole directory, stopping on each entry that is in use and stepping over the free ones. A walk keeps a directory block in SbfsBuffer between calls, so anything else that goes to the disk in the middle of one ends it: take what is wanted out of an entry before asking the disk for anything else.
A file's size is settled when it is made, because nothing can grow one afterwards. Files are laid down contiguously, so the block after a file usually belongs to somebody else. A program that does not know how much it will write has to guess high and accept the slack, or build its output elsewhere and make the file once the size is known.
Finding room is a walk through the directory rather than a lookup, because there is no allocation table. With files laid down contiguously the directory already says which blocks are spoken for, and a second copy of that would be a second thing to keep right. The free count in the superblock is kept up to date but it is a note rather than the truth: it can be worked out again from the directory, and the directory is the one to believe.
@@ -415,6 +457,31 @@ The same rule cuts the other way, which is easier to miss. Because DP3 is not pu
A label may only be defined once across a program and everything it includes, so a routine in one library cannot use a name that another has already taken.
## The Console Library:
Programs/CosmOS/Source/console.asm is the console library. It replaces print.asm, which was written for a machine with one Data Pointer and no vector table, and which is still there because the programs that include it still work.
| Routine | Does |
| --- | --- |
| newLine | Prints a line feed. |
| printString | DP0 names a string ending in a zero byte. Prints it. |
| printSpaces | A holds how many spaces to print. None is a fair answer, and prints nothing. |
| printByteHex | A holds a byte. Prints it as two hexadecimal digits. |
| printWordHex | DP0 names two bytes, most significant first. Prints them as four hexadecimal digits. |
| printHexDigit | A holds a nybble. Prints the one character that stands for it. |
| printDecimalDigit | A holds a digit from zero to nine. Prints it. |
| printByteDecimal | A holds a byte. Prints it in decimal, without leading zeroes. |
| printWordDecimal | DP0 names two bytes, most significant first. Prints them in decimal, without leading zeroes. |
| readLine | DP0 names a buffer and B says how many characters it holds. Reads a line into it. Q is how long the line turned out to be. |
Two things about it are different from the old library, and both are deliberate.
There is no branch at the top. print.asm begins with a BRI to a label called start, so that a program including it arrives at its own entry point rather than falling into the library. That was the only way to do it before the Vector Table existed, and it is why print.asm cannot be assembled on its own: the label it branches to is one only the including program defines. A program including console.asm says where it begins in its own Vector Segment instead, with a Boot line, and the library assembles by itself.
Every routine names the Data Pointer it works through rather than assuming there is only one. A pointer handed in is DP0, and nothing in the library disturbs DP3.
readLine cuts a line short if it is longer than the buffer, and then reads the rest of it and throws it away, so that what is left over does not turn up as the next line. ConsoleEndOfInput is set if the console ran out instead of ending a line, and it is cleared at the start of every call, so it always describes the last line read. That is a different thing from an empty line, and a program reading until there is no more has to be able to tell the two apart.
## Loading A Program From A Disk:
A program that was not the one the machine booted from carries sixteen bytes in front of it saying where it belongs.
@@ -431,7 +498,7 @@ A program that was not the one the machine booted from carries sixteen bytes in
| 14 | 2 | How many bytes of data there are. |
| 16 | | The code, and then the data. |
Programs/loader.asm reads one off a disk, puts the two pieces where the header asks, and jumps to the entry with BRD. Every part of that already existed: the filesystem finds the file, the memory controller writes Program Memory, and BRD turns an address worked out at run time into somewhere to go. The header is the only new thing.
Programs/loader.asm reads one off a disk, puts the two pieces where the header asks, and jumps to the entry with BRD. Every part of that already existed: the filesystem finds the file, the memory controller writes Program Memory, and BRD turns an address worked out at run time into somewhere to go. The header is the only new thing. Programs/CosmOS does the same as one of its commands, and then takes the machine back afterwards, which the standalone loader has no way to do.
The magic matters for the same reason it does everywhere else on this machine. Without it, loading a text file would put nonsense into Program Memory and then jump into it.
@@ -439,18 +506,21 @@ The magic matters for the same reason it does everywhere else on this machine. W
**Nothing relocates anything.** A program is put exactly where its header asks, and that has to be the address it was assembled for, or every branch and every SETD inside it points somewhere wrong.
A program is assembled for its address by reserving the front of each segment. #Reserve at the top of the Program Segment puts the first instruction after it at a known address, and the same in the Data Segment does it for the data, so every label inside is already right.
A program says where it lives with #Base, at the top of each segment. Every label inside is then resolved from there, so the addresses in the program and the addresses in its header say the same thing. Giving either segment a base is also what makes the assembler write the program out as a loadable one rather than as a boot image, carrying none of the empty space below it.
```
#Program
#Reserve 0x2000 ; This program's code lives from 0x2000.
#Base 0x2000 ; This program's code lives from 0x2000.
hello:
...
#Data
#Base 0x1000 ; And its data from 0x1000.
```
That is not general placement, since only the first thing in a segment can be put anywhere. It is exactly enough for a program that wants to live at one address, which is what a loadable program is.
That is not general placement: a base applies to a whole segment, and only the first thing in one can set it. It is exactly enough for a program that wants to live at one address, which is what a loadable program is. See the Assembler Manual.
The loader keeps its own code and data below the addresses the loaded program claims. That is an arrangement between the two of them rather than anything the machine enforces, and it is the part that a real operating system would have to do properly.
Whoever does the loading keeps its own code and data below the addresses the loaded program claims. That is an arrangement between the two of them rather than anything the machine enforces. Programs/CosmOS is where that arrangement is written down as a memory map and kept to.
## Refusing:
@@ -474,9 +544,63 @@ If nothing is installed at Vector 2, the CPU sets the Fault Flag and the Halt Fl
Stopping matters because the alternative is worse. A byte that means nothing is almost always a sign that execution has wandered into data, or that a program was built for a machine with instructions this one does not have. Stepping over it and carrying on turns a clear failure into a program that appears to run and quietly does the wrong thing.
## The Stack Pointer, Set By Hand:
MVDS copies a Data Pointer into the Stack Pointer. It is the most dangerous instruction on this machine, and it is here for one job.
Everything a program is in the middle of doing lives on the Stack. Moving the Stack Pointer does not move any of it, and does not destroy any of it either: it steps away from it. Every return address, every saved register and every interrupt frame stays exactly where it was in Data Memory, and the Stack Pointer is simply no longer looking at it. A RET taken while the Stack Pointer is somewhere else does not go back to whoever called: it reads two bytes from wherever the Stack Pointer now points and branches there. If those bytes are a string, execution lands in the middle of it.
That is not a bug to be worked around. It is what moving the Stack means, and it is why nothing else on this machine can do it.
The job it is here for is reclaiming the Stack from a program that has stopped running. A system that loads other programs and takes the machine back afterwards has a problem without it. A program that gives up part way through leaves everything it pushed behind, and the interrupt frame carrying its request to stop is on there too. Nothing unwinds any of that, because the program is not going to return. Without MVDS the Stack only ever moves downward, a little more with every program run, and a shell cannot outlive many of them.
The pattern is to write the Stack Pointer down before giving the machine away and put it back afterwards:
```
; Before handing control to a program, remember where the Stack was.
MVSD.0
SETD.1 SavedStack
STD.0.1
; ... the program runs, and eventually asks to stop ...
; Taking the machine back. The Stack is ours again, and everything the
; program left on it is gone.
SETD.1 SavedStack
LDD.0.1
MVDS.0
```
### Coming Back From It:
A routine that moves the Stack and then puts the Stack Pointer back exactly where it found it can return in the ordinary way. The return address and the frame were never destroyed, only stepped away from, and RET or RETI finds them precisely as they were. Nothing special is needed for this to work, but one thing is required for it to keep working: nothing done while the Stack was elsewhere may reach back over those bytes. A borrowed Stack has to be somewhere the old one is not, and it has to be far enough away that pushing on it cannot walk into the old one.
A routine that moves the Stack and **leaves it moved** is the other case, and that one cannot return at all, because its return address is on the Stack it walked away from. This is not a limitation to work around either. It is the entire point when the reason for moving is that whoever owned that Stack is not coming back. A handler for a service meaning "give the machine back" restores the system's Stack and then branches to the prompt, because there is nowhere left to return to.
### Where To Keep The Old One:
This is the awkward part. A fixed location in Data Memory is the obvious place to write the old Stack Pointer down, and it works until two routines that do this are nested. The inner one overwrites the outer one's copy, the outer one restores the inner one's Stack, and it never gets home. Nothing about that failure points back at the cause.
The answer that nests is to keep the old Stack Pointer on the borrowed Stack itself. Move first, then push it, and pop it back before returning. Each nesting keeps its own copy, on its own Stack, and no two of them can collide:
```
MVSD.3 ; Where the Stack is now.
SETD.0 BorrowedTop ; The highest address of somewhere a Stack can live,
MVDS.0 ; because the Stack grows downward from here.
PSHD.3 ; The old Stack Pointer, kept on the borrowed Stack.
; ... work, with the borrowed Stack under us ...
POPD.3
MVDS.3 ; And put it back.
RET ; Which now finds the frame exactly as it was left.
```
One last thing. An interrupt arriving while the Stack Pointer is somewhere unusual builds its frame there, so anywhere it is set has to be somewhere a Stack can actually live. And a system holding a saved Stack Pointer for a program it is running has to keep it somewhere that program cannot reach, or a program that scribbles over it takes the machine down with it on the way out.
## Naming a Data Pointer:
Sixteen instructions work through a Data Pointer. Each of them carries a selector byte immediately after its opcode, naming which Data Pointer it means. LDD and STD move a pointer through a pointer, so they carry two selectors, the first naming the pointer being moved and the second naming the pointer that addresses it.
Seventeen instructions work through a Data Pointer. Each of them carries a selector byte immediately after its opcode, naming which Data Pointer it means. LDD and STD move a pointer through a pointer, so they carry two selectors, the first naming the pointer being moved and the second naming the pointer that addresses it.
The selector is a full byte, but only enough of it is read to choose among the Data Pointers the machine has. A selector larger than the highest numbered pointer wraps around rather than being rejected, so it is the assembler's job to refuse to write one.
@@ -547,7 +671,7 @@ Q is where every ALU result lands, and Q is not itself an ALU operand, so MVQA a
| 35 | POPB | 1 | Reads the location referenced by the Stack Pointer from Data Memory into B then increments the Stack Pointer. |
| 36 | POPD | 2 | Restores the named Data Pointer from the stack, increments the Stack Pointer by two. |
### Data Operations: 13 Instructions
### Data Operations: 14 Instructions
| Hex Code | Mnemonic | Bytes | Description |
| -- | ---- | -- | -- |
| 40 | INCD | 2 | Increments the named Data Pointer. |
@@ -562,7 +686,8 @@ Q is where every ALU result lands, and Q is not itself an ALU operand, so MVQA a
| 49 | DPDN | 3 | Offsets the named Data Pointer down by the value of the byte following the selector. |
| 4A | LDD | 3 | Loads the first named Data Pointer from the two bytes of Data Memory addressed by the second, most significant byte first. |
| 4B | STD | 3 | Stores the first named Data Pointer into the two bytes of Data Memory addressed by the second, most significant byte first. |
| 4C | MVSD | 2 | Copies the Stack Pointer into the named Data Pointer. The Stack Pointer itself is unchanged and still cannot be written. |
| 4C | MVSD | 2 | Copies the Stack Pointer into the named Data Pointer. The Stack Pointer itself is unchanged. |
| 4D | MVDS | 2 | Copies the named Data Pointer into the Stack Pointer, moving the Stack. Read The Stack Pointer, Set By Hand before using it. |
BRD is the only branch whose destination is not written into the program. Every other branch carries the address it goes to, fixed when the program was assembled; BRD takes it from a Data Pointer, which is what makes a table of addresses something a program can dispatch through rather than only read. Together with LDD it turns the Data Segment into somewhere a program can keep a list of places to go.
@@ -589,7 +714,7 @@ LDD and STD are how a program follows an address it has stored, rather than one
## Input and Output In the Emulator:
Port 0 is the console: writing sends a byte to standard output and reading takes one from standard input. Everything else this machine has is listed under Devices above, and a program that wants to know what is actually there asks the bus registry rather than assuming.
Port 0 is the console: writing sends a byte to standard output and reading takes one from standard input. It answers on two more ports than that, which are described under The Console above and which a program can ignore entirely if all it wants is to read and write bytes. Everything else this machine has is listed under Devices above, and a program that wants to know what is actually there asks the bus registry rather than assuming.
### Example Program: Hello World
```
+54 -3
View File
@@ -63,15 +63,65 @@ for m in re.finditer(r'^### (.+?) Operations: (\d+) Instructions?$', pm, re.M):
problems.append("the manual says %s has %d instructions, and it has %d"
% (name, claimed, match[0]))
# ---- How many instructions carry a Data Pointer selector ----
#
# The manual says this as a word rather than a figure, and it is the sort of number that
# goes stale quietly: adding an instruction that takes a selector leaves the sentence
# looking perfectly reasonable and wrong. dataPointerOperands is the list, so it is the
# one to believe.
words = {12: "Twelve", 13: "Thirteen", 14: "Fourteen", 15: "Fifteen", 16: "Sixteen",
17: "Seventeen", 18: "Eighteen", 19: "Nineteen", 20: "Twenty"}
selectors = asmc[asmc.index("int dataPointerOperands"):asmc.index("uint8_t getOpcode")]
taking = len(re.findall(r'^\s*case 0x[0-9A-Fa-f]{2}:', selectors, re.M))
said = re.search(r'^([A-Z][a-z]+) instructions work through a Data Pointer\.', pm, re.M)
if not said:
problems.append("the manual no longer says how many instructions take a Data Pointer")
elif said.group(1) != words.get(taking):
problems.append("the manual says %s instructions work through a Data Pointer, and %d do"
% (said.group(1).lower(), taking))
# ---- Every device class in the header has a row in the Devices table ----
#
# The table says which ports a device answers on and what class it reports. Adding a
# device, or widening one from a single port to a block, leaves the table looking perfectly
# reasonable and describing a machine that no longer exists. The classes are the part that
# can be checked against the source without teaching this script how ports are laid out:
# every class the header defines except DEVICE_NONE is something a program can find on the
# bus, so every one of them has to be findable in the manual too.
ioh = read("Source/Emulator/io.h")
classes = {name: int(value, 16)
for name, value in re.findall(r'^#define (DEVICE_[A-Z_]+)\s+(0x[0-9A-Fa-f]{2})$',
ioh, re.M)
if name not in ("DEVICE_NONE",)}
if "## Devices:" not in pm:
problems.append("the Programming Manual has lost its Devices table")
else:
table = pm.split("## Devices:")[1].split("\n## ")[0]
listed = {int(m, 16) for m in re.findall(r'\|\s*(0x[0-9A-Fa-f]{2})\s*\|\s*$', table, re.M)}
for name, value in sorted(classes.items(), key=lambda pair: pair[1]):
if value not in listed:
problems.append("%s (0x%02X) is a device class and has no row in the Devices"
" table" % (name, value))
# ---- Every directive the assembler knows is written down ----
for directive in sorted(set(re.findall(r'"(#[A-Za-z]+)"', util))):
if directive not in am:
problems.append("%s is a directive and is not in the Assembler Manual" % directive)
# ---- Every routine the manual promises exists ----
for library, names in [("Programs/Libraries/sbfs.asm", re.findall(r'\| (sbfs[A-Za-z]+) \|', pm))]:
#
# The first column of the table in each of these sections names something the library has
# to define. A routine renamed in the source and not in the manual is caught here, which
# is what keeps the tables a description rather than a memory.
for heading, library in [("## Reading The Filesystem:", "Programs/CosmOS/Source/sbfs.asm"),
("## The Console Library:", "Programs/CosmOS/Source/console.asm")]:
if heading not in pm:
problems.append("the Programming Manual has lost its \"%s\" section"
% heading.strip("# :"))
continue
section = pm.split(heading)[1].split("\n## ")[0]
defined = set(re.findall(r'^([a-zA-Z][A-Za-z0-9]*):', read(library), re.M))
for name in names:
for name in re.findall(r'^\| ([a-z][A-Za-z0-9]*) \|', section, re.M):
if name not in defined:
problems.append("the manual lists %s, which %s does not define" % (name, library))
@@ -111,7 +161,8 @@ for heading in ["## An Example SplitBit Assembly Program:",
with tempfile.TemporaryDirectory() as work:
asm = os.path.join(work, "example.asm")
open(asm, "w").write(example)
built = subprocess.run(["./Assembler", "-I", "Programs/Libraries", asm,
built = subprocess.run(["./Assembler", "-I", "Programs/Libraries",
"-I", "Programs/CosmOS/Source", asm,
"-o", os.path.join(work, "example.bin")],
capture_output=True)
if built.returncode != 0:
+10
View File
@@ -0,0 +1,10 @@
console mode ports
at start: 01 ready
key mode: 05 ready keys
what came in:
hi
at the end: 06 ended keys
line mode: 02 ended
Execution halted after 891 cycles.
[exit 0]
+14
View File
@@ -0,0 +1,14 @@
console test
byte hex: 00 0F A5 FF
word hex: 0000 03E8 FFFF
byte dec: 0 7 42 100 255
word dec: 0 9 10 255 1000 10001 65535
spaces: || |
lines read:
[hello] 5
[] 0
[SplitBit] 8
[a line that is f] 16
end of input
Execution halted after 6064 cycles.
[exit 0]
+25
View File
@@ -0,0 +1,25 @@
CosmOS
> dir list what is on the disk
load <file> read a program off the disk
run start what was loaded
dump sixty four bytes of memory, and again for more
dump <program|data|bank> <address>
help this
exit stop
> greeting.txt 17
filler1.txt 8
filler2.txt 8
filler3.txt 8
filler4.txt 8
filler5.txt 8
filler6.txt 8
filler7.txt 8
filler8.txt 8
across.txt 700
empty.txt 0
aName22CharactersLong! 22
12 files
> > I do not know: frobnicate
> halted
Execution halted after 12376 cycles.
[exit 0]
+23
View File
@@ -0,0 +1,23 @@
CosmOS
> dump <program|data|bank> <address>
> there is no such bank
> loaded, starting at 2000
> 2000 47 00 10 00 18 10 47 00 10 44 18 10 47 00 10 7A G.....G..D..G..z
2010 27 1F 18 11 47 00 10 5D 18 10 47 00 10 7A 18 10 '...G..]..G..z..
2020 47 00 10 65 18 10 18 12 00 00 00 00 00 00 00 00 G..e............
2030 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
> 2040 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
2050 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
2060 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
2070 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
> 1000 61 20 70 72 6F 67 72 61 6D 2C 20 6C 6F 61 64 65 a program, loade
1010 64 20 6F 66 66 20 61 20 64 69 73 6B 2C 20 72 75 d off a disk, ru
1020 6E 6E 69 6E 67 20 6F 6E 20 74 68 65 20 73 79 73 nning on the sys
1030 74 65 6D 20 74 68 61 74 20 6C 6F 61 64 65 64 20 tem that loaded
> 0000 01 FF 00 00 00 00 00 00 01 FF 00 00 00 00 00 00 ................
0010 03 FF 08 00 00 00 00 00 01 20 01 00 00 00 00 00 ......... ......
0020 00 FF 00 00 00 00 00 00 00 FF 00 00 00 00 00 00 ................
0030 00 FF 00 00 00 00 00 00 00 FF 00 00 00 00 00 00 ................
> halted
Execution halted after 23498 cycles.
[exit 0]
+9
View File
@@ -0,0 +1,9 @@
CosmOS
> loaded, starting at 2000
> Hello, World!
finished
> Hello, World!
finished
> halted
Execution halted after 2668 cycles.
[exit 0]
+873
View File
@@ -0,0 +1,873 @@
CosmOS
> loaded, starting at 2000
>  #
#
###

# #
##
#

#
# #
##

#
##
##

#
#
###

# #
##
#

#
# #
##

#
##
##

#
#
###

# #
##
#

#
# #
##

#
##
##

#
#
###

# #
##
#

#
# #
##

#
##
##

#
#
###

# #
##
#

#
# #
##

#
##
##

#
#
###

# #
##
#

#
# #
##

#
##
##

#
#
###

# #
##
#

#
# #
##

#
##
##

#
#
###

# #
##
#

#
# #
##

#
##
##

#
#
###

# #
##
#

#
# #
##

#
##
##

#
#
###

# #
##
#

#
# #
##

#
##
##

#
#
###

# #
##
#

#
# #
##

#
##
##

#
#
###

# #
##
#

#
# #
##

#
##
##

#
#
###

# #
##
#

#
# #
##

#
#
##

#
##

##
##
the board has settled
finished
>
halted
Execution halted after 11671206 cycles.
[exit 0]
+25
View File
@@ -0,0 +1,25 @@
CosmOS
> loaded, starting at 2000
>  #
#
###
stopped
finished
> >
halted
Execution halted after 26219 cycles.
[exit 0]
+7
View File
@@ -0,0 +1,7 @@
CosmOS
no filesystem on the disk
> no filesystem on the disk
>
halted
Execution halted after 636 cycles.
[exit 0]
+20
View File
@@ -0,0 +1,20 @@
CosmOS
> nothing is loaded
> greet.sbx 210
hello.sbx 52
Life.sbx 1411
notes.txt 21
4 files
> load what?
> no such file
> not a program
> loaded, starting at 2000
> a program, loaded off a disk, running on the system that loaded it
what should I call you? hello, Anachronaut. that is all I do.
finished
> a program, loaded off a disk, running on the system that loaded it
what should I call you? hello, Claude. that is all I do.
finished
> halted
Execution halted after 11647 cycles.
[exit 0]
+15
View File
@@ -0,0 +1,15 @@
greeting.txt 17
filler1.txt 8
filler2.txt 8
filler3.txt 8
filler4.txt 8
filler5.txt 8
filler6.txt 8
filler7.txt 8
filler8.txt 8
across.txt 700
empty.txt 0
aName22CharactersLong! 22
files: 12
Execution halted after 9498 cycles.
[exit 0]
+9
View File
@@ -0,0 +1,9 @@
stack reclaim test
at start: FFFF
spent: FFF7
reclaimed: FFFF
still good: 33
borrowed: 7 and 9
back home: FFFF
Execution halted after 1054 cycles.
[exit 0]
+8
View File
@@ -0,0 +1,8 @@
split: [dump] [program 2000]
split: [dir] []
same: yes no no no
hex: 2000 00FF 00FF BEEF FFFF 0000
0 is a fine way to begin a string
no number here
Execution halted after 2730 cycles.
[exit 0]
+1
View File
@@ -0,0 +1 @@
hi
+4
View File
@@ -0,0 +1,4 @@
hello
SplitBit
a line that is far longer than sixteen characters
+5
View File
@@ -0,0 +1,5 @@
help
dir
frobnicate
exit
+8
View File
@@ -0,0 +1,8 @@
dump nonsense
dump 9 0000
load greet.sbx
dump program 2000
dump
dump data 1000
dump 2 0000
exit
+4
View File
@@ -0,0 +1,4 @@
load hello.sbx
run
run
exit
+2
View File
@@ -0,0 +1,2 @@
load Life.sbx
run
+3
View File
@@ -0,0 +1,3 @@
load Life.sbx
run
q
+1
View File
@@ -0,0 +1 @@
dir
+11
View File
@@ -0,0 +1,11 @@
run
dir
load
load nosuch.sbx
load notes.txt
load greet.sbx
run
Anachronaut
run
Claude
exit
+30 -2
View File
@@ -45,10 +45,38 @@ for i in 1 2 3 4 5 6 7 8; do "$TOOL" put "$DISKS/sbfs.img" "filler$i.txt" >/dev/
# A disk with a loadable program on it. The program is assembled here rather than kept as
# bytes, so that what gets loaded is always built from the source beside it.
"$TOOL" format "$DISKS/load.img" 64 2 >/dev/null
"$ROOT/Assembler" "$ROOT/Programs/loadable/hello.asm" -o "$WORK/hello.bin" >/dev/null
python3 "$ROOT/Source/DiskTool/wrap.py" "$WORK/hello.bin" "$WORK/hello.sbx" 0x2000 0x1000 0x2000 >/dev/null
# The assembler writes a loadable program itself, because the source says where it goes.
"$ROOT/Assembler" "$ROOT/Programs/loadable/hello.asm" -o "$WORK/hello.sbx" >/dev/null
"$TOOL" put "$DISKS/load.img" "$WORK/hello.sbx" >/dev/null
# A disk for CosmOS. greet.sbx asks the system for everything it does rather than talking
# to the hardware itself, so loading and running it exercises the whole path: the loader,
# the vector table, the service handlers, and giving the machine back at the end.
#
# hello.sbx is the opposite case, and that is why it is here: it is the original
# hello.asm, written before any of this existed, and it still writes straight to port
# 0x00 rather than calling osPrintString. A program is allowed to reach past the system
# to the hardware, so something has to check that one still gives the machine back.
#
# notes.txt is there so that loading something that is not a program can be tried too.
"$TOOL" format "$DISKS/cosmos.img" 32 1 >/dev/null
"$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" \
"$ROOT/Programs/CosmOS/Apps/greet.asm" -o "$WORK/greet.sbx" >/dev/null
"$TOOL" put "$DISKS/cosmos.img" "$WORK/greet.sbx" >/dev/null
# Assembled to a different working name so it cannot tread on load.img's hello.sbx above,
# then put under the name the shell asks for.
"$ROOT/Assembler" -I "$ROOT/Programs/CosmOS/Source" \
"$ROOT/Programs/CosmOS/Apps/hello.asm" -o "$WORK/appHello.sbx" >/dev/null
"$TOOL" put "$DISKS/cosmos.img" "$WORK/appHello.sbx" hello.sbx >/dev/null
# Life.sbx is the one that had to be taught to stop. It runs to a still life and returns
# on its own, so the test needs no cycle limit: whether it ends is the thing being checked
# and a limit would hide the answer by supplying one.
"$ROOT/Assembler" -I "$ROOT/Programs/Libraries" -I "$ROOT/Programs/CosmOS/Source" \
"$ROOT/Programs/CosmOS/Apps/Life.asm" -o "$WORK/Life.sbx" >/dev/null
"$TOOL" put "$DISKS/cosmos.img" "$WORK/Life.sbx" >/dev/null
printf 'this is not a program' > notes.txt
"$TOOL" put "$DISKS/cosmos.img" notes.txt >/dev/null
# A disk of its own for the writing test, with one file already on it so that what it
# writes has to be placed somewhere that does not tread on what is there.
"$TOOL" format "$DISKS/write.img" 32 1 >/dev/null
+86 -1
View File
@@ -69,6 +69,10 @@ blitTest | testPrograms/blitTest.asm | run | -
# Reading a filesystem that the host tool wrote. The two are separate implementations of
# one written format, so this is where any drift between them would show.
sbfsReadTest | testPrograms/sbfsReadTest.asm | run | - | - | disks/sbfs.img
# Walking the directory rather than searching it, which is what listing a disk needs.
# Twelve files is more than the eight an entry block holds, so the walk has to cross into
# the second directory block to see them all.
sbfsWalkTest | testPrograms/sbfsWalkTest.asm | run | - | - | disks/sbfs.img
# Writing a filesystem, then reading back what was written. The disk starts with a file
# on it, so allocation has to find room rather than start at the beginning.
@@ -110,8 +114,12 @@ registryTest | testPrograms/registryTest.asm | run | -
# ---- Moving the cursor along ----
paddingTest | testPrograms/paddingTest.asm | run | - | -
# ---- Finding the Stack ----
# ---- Finding the Stack, and moving it ----
stackPointerTest | testPrograms/stackPointerTest.asm | run | - | -
# MVDS, the dangerous one: a system taking its Stack back from a program that stopped
# without unwinding. The sequence is the one the Programming Manual prints under "The
# Stack Pointer, Set By Hand", so the manual's example cannot quietly stop working.
stackReclaimTest | testPrograms/stackReclaimTest.asm | run | - | -
# ---- The Interrupt Flag ----
# Nothing reads the flag yet. This checks that setting and clearing it leaves
@@ -144,6 +152,73 @@ swiFaultTest | testPrograms/swiFaultTest.asm | run | -
# where, and exits non zero, rather than stepping over it and carrying on.
faultTest | testPrograms/faultTest.asm | run | - | -
# ---- The modern console library ----
# Every routine in console.asm, called with the cases that are easy to get wrong: a zero,
# the largest thing that fits, and a leading zero that should not print. The reading half
# is given a line longer than its buffer, so truncation and the swallowing of the rest
# are shown rather than assumed.
consoleTest | testPrograms/consoleTest.asm | run | consoleTest.in | -
# The console's status and control ports. Input here is a file rather than a terminal, so
# key mode has no terminal to change and only the mode bit moves: that is deliberate, since
# a program has to behave the same either way and a test needing a terminal could not run.
# What it pins down is that READY is clear at the end of input while ENDED is set, so a
# loop reading while READY stops on its own instead of taking imaginary bytes forever.
consoleModeTest | testPrograms/consoleModeTest.asm | run | consoleModeTest.in | -
# Picking a typed line apart, which is how the shell understands anything. Includes a
# string beginning with a zero: the assembler strips the quotes before deciding what a
# token is, so such a string looked like a malformed literal and was refused.
textTest | testPrograms/textTest.asm | run | - | -
# ---- CosmOS ----
# The system and its shell, driven by a script of commands. This is the first thing that
# uses the machine as a machine rather than exercising one part of it: it boots, mounts a
# disk written by the host tool, reads lines, and walks a directory to answer 'dir'.
cosmos | CosmOS/Source/cosmos.asm | run | cosmos.in | - | disks/sbfs.img
# The same with nothing attached. A shell that only works with a disk in the drive is not
# finished, so the empty machine is a case in its own right rather than an accident.
cosmosNoDisk | CosmOS/Source/cosmos.asm | run | cosmosNoDisk.in | -
# Loading a program and running it, which is the whole machine at once: the filesystem
# finds it, the controller writes it into Program Memory, the vector table carries its
# requests back to the system, and MVDS takes the Stack back when it stops. Every way
# load can refuse is tried first, and run is asked for twice, so the Stack being reclaimed
# rather than merely abandoned is what makes the second one work.
cosmosRun | CosmOS/Source/cosmos.asm | run | cosmosRun.in | - | disks/cosmos.img
# The monitor. It reads Program Memory, which the instruction set cannot do at all, so it
# works only through the controller. The targets are chosen to be stable: a loaded
# program's code and data, and the bank table, rather than the system's own code, which
# would churn whenever any library changed.
#
# Dumping the bank table is worth having on its own. It is the machine describing itself,
# and it shows the disk buffer that sbfsMount registered as bank 3 at boot.
cosmosDump | CosmOS/Source/cosmos.asm | run | cosmosDump.in | - | disks/cosmos.img
# The original hello.asm, brought over as an application. It is not much of a program,
# but it is the one that talks to the hardware directly: it writes to port 0x00 instead
# of calling osPrintString, so it is the case where a program reaches past the system and
# the system has to get control back anyway. run is asked for twice for the same reason
# it is in cosmosRun, and here it also says the Stack comes back from a program that never
# entered a service handler at all.
cosmosHello | CosmOS/Source/cosmos.asm | run | cosmosHello.in | - | disks/cosmos.img
# Life, which is the one program that had no end state to reach. It stops when the board
# settles, and the glider does settle: it crosses the field, hits the dead border, and
# collapses into a block at generation 54. NO CYCLE LIMIT ON PURPOSE. Whether it stops on
# its own is the whole point of the port, and a limit would answer that question for it.
#
# NOTHING IS TYPED AFTER run, and that is load bearing rather than tidy. Life polls the
# console between generations, and a byte sitting in the pipe is a byte waiting on the
# console as far as the machine is concerned - exactly as typing ahead at a terminal
# would be. An "exit" on the next line stops it at generation 1 and gets eaten. The shell
# ends the run by reaching the end of input instead, the way cosmosNoDisk does.
cosmosLife | CosmOS/Source/cosmos.asm | run | cosmosLife.in | - | disks/cosmos.img
# The other half of that: a key IS waiting, so it stops at once instead of at generation
# 54. The two together are what say the poll is reading the console rather than always
# answering the same way.
cosmosLifeKey | CosmOS/Source/cosmos.asm | run | cosmosLifeKey.in | - | disks/cosmos.img
# The programs CosmOS loads, checked on their own so that a failure here reads as "the app
# does not assemble" rather than as a broken disk image.
app-greet | CosmOS/Apps/greet.asm | assemble | - | -
app-hello | CosmOS/Apps/hello.asm | assemble | - | -
app-Life | CosmOS/Apps/Life.asm | assemble | - | -
# ---- Programs driven by console input ----
inputTest | inputTest.asm | run | inputTest.in | -
inputTestOld | testPrograms/inputTest.asm | run | inputTest.in | -
@@ -161,6 +236,12 @@ replCalculator | replCalculator.asm | run | replCalcu
16x16LifeModern | gameOfLife/16x16LifeModern.asm | run | - | 3000000
# ---- Libraries: no entry point, so only check that they assemble ----
# The CosmOS libraries assemble on their own, unlike print.asm below, which cannot: it
# begins with a branch to a label only the including program defines. That difference is
# the point of the rewrite, so both halves of it are recorded here.
lib-console | CosmOS/Source/console.asm | assemble | - | -
lib-text | CosmOS/Source/text.asm | assemble | - | -
lib-sbfs | CosmOS/Source/sbfs.asm | assemble | - | -
lib-int8 | Libraries/int8.asm | assemble | - | -
lib-int16 | Libraries/int16.asm | assemble | - | -
lib-int32 | Libraries/int32.asm | assemble | - | -
@@ -177,6 +258,10 @@ diagDuplicateVector | testPrograms/diagnostics/duplicateVector.asm | xfail | -
diagBareSWI | testPrograms/diagnostics/bareSWI.asm | xfail | - | -
diagAlignOutside | testPrograms/diagnostics/alignOutside.asm | xfail | - | -
diagBareAlign | testPrograms/diagnostics/bareAlign.asm | xfail | - | -
# One segment based and the other forgotten, which is how the ported Fib-8 put its data
# on top of the console's variables. It assembled and it ran, so nothing but the assembler
# was ever going to catch it.
diagUnbasedSegment | testPrograms/diagnostics/unbasedSegment.asm | xfail | - | -
# ---- Known breakages, recorded rather than ignored ----
# print.asm branches to 'start', which only the including program defines.
+5 -3
View File
@@ -111,11 +111,13 @@ check() {
assemble() {
# assemble <name> <source>; echoes the built binary path on success.
# Everything builds from Programs/ with Libraries/ on the include path, and the
# binary goes to Tests/build, so the source tree is never written to.
# Everything builds from Programs/ with Libraries/ and CosmOS/ on the include path,
# and the binary goes to Tests/build, so the source tree is never written to.
# CosmOS is there because it owns the filesystem library and the service names, which
# test programs outside it include.
local name="$1" src="$2"
local bin="$BUILD/$name.bin"
if ( cd "$PROGRAMS" && "$ASSEMBLER" -I Libraries -o "$bin" "$src" ) >"$BUILD/.assemble.log" 2>&1; then
if ( cd "$PROGRAMS" && "$ASSEMBLER" -I Libraries -I CosmOS/Source -o "$bin" "$src" ) >"$BUILD/.assemble.log" 2>&1; then
echo "$bin"
return 0
fi